arcgraph 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- arcgraph/__init__.py +41 -0
- arcgraph/_build_provenance.json +10 -0
- arcgraph/adapters/__init__.py +6 -0
- arcgraph/adapters/arq_adapter.py +369 -0
- arcgraph/adapters/celery_adapter.py +440 -0
- arcgraph/adapters/common.py +436 -0
- arcgraph/adapters/django_adapter.py +765 -0
- arcgraph/adapters/fastapi_adapter.py +415 -0
- arcgraph/adapters/logging_adapter.py +335 -0
- arcgraph/adapters/mcp_adapter.py +100 -0
- arcgraph/adapters/pydantic_adapter.py +290 -0
- arcgraph/adapters/pytest_adapter.py +221 -0
- arcgraph/adapters/pytest_collection.py +96 -0
- arcgraph/adapters/pytest_fixtures.py +371 -0
- arcgraph/adapters/pytest_types.py +251 -0
- arcgraph/adapters/registry.py +34 -0
- arcgraph/adapters/service_container_adapter.py +548 -0
- arcgraph/adapters/sqlalchemy_adapter.py +446 -0
- arcgraph/adapters/typer_click_adapter.py +292 -0
- arcgraph/analyzers/__init__.py +1 -0
- arcgraph/analyzers/bindings.py +782 -0
- arcgraph/analyzers/calls/__init__.py +6 -0
- arcgraph/analyzers/calls/analyzer.py +2921 -0
- arcgraph/analyzers/calls/constants.py +702 -0
- arcgraph/analyzers/calls/context.py +298 -0
- arcgraph/analyzers/calls/lexical.py +237 -0
- arcgraph/analyzers/coverage.py +826 -0
- arcgraph/analyzers/exports.py +46 -0
- arcgraph/analyzers/har_trace.py +323 -0
- arcgraph/analyzers/imports.py +190 -0
- arcgraph/analyzers/otel_trace.py +352 -0
- arcgraph/analyzers/precise_references.py +1199 -0
- arcgraph/analyzers/python_scopes.py +56 -0
- arcgraph/analyzers/runtime_trace.py +516 -0
- arcgraph/analyzers/similarity.py +430 -0
- arcgraph/analyzers/symbols.py +363 -0
- arcgraph/analyzers/type_unions.py +89 -0
- arcgraph/analyzers/types.py +2293 -0
- arcgraph/assets/__init__.py +1 -0
- arcgraph/assets/workbench/__init__.py +1 -0
- arcgraph/assets/workbench/index.html +2398 -0
- arcgraph/assets/workbench/vendor/LICENSE.d3.txt +15 -0
- arcgraph/assets/workbench/vendor/d3.v7.min.js +2 -0
- arcgraph/change/__init__.py +10 -0
- arcgraph/change/baseline.py +145 -0
- arcgraph/change/changed_regions.py +728 -0
- arcgraph/change/contracts.py +927 -0
- arcgraph/change/errors.py +88 -0
- arcgraph/change/evidence.py +289 -0
- arcgraph/change/graph_delta.py +1422 -0
- arcgraph/change/identities.py +1277 -0
- arcgraph/change/paths.py +234 -0
- arcgraph/change/pins.py +426 -0
- arcgraph/change/planner.py +504 -0
- arcgraph/change/protected_surfaces.py +328 -0
- arcgraph/change/provider.py +46 -0
- arcgraph/change/service.py +1030 -0
- arcgraph/change/source_provider.py +311 -0
- arcgraph/change/store.py +1138 -0
- arcgraph/change/trusted_runners.py +154 -0
- arcgraph/change/verdicts.py +141 -0
- arcgraph/change/verification_plan.py +113 -0
- arcgraph/change/verifier.py +579 -0
- arcgraph/core/__init__.py +1 -0
- arcgraph/core/assurance.py +343 -0
- arcgraph/core/binding_metrics.py +75 -0
- arcgraph/core/cleanup.py +701 -0
- arcgraph/core/confidence_profiles.py +108 -0
- arcgraph/core/evidence_manifest.py +862 -0
- arcgraph/core/expression_kinds.py +11 -0
- arcgraph/core/force_graph_export.py +1125 -0
- arcgraph/core/freshness.py +94 -0
- arcgraph/core/graph_store.py +1099 -0
- arcgraph/core/ids.py +206 -0
- arcgraph/core/language_tiers.py +150 -0
- arcgraph/core/merge.py +649 -0
- arcgraph/core/metadata.py +30 -0
- arcgraph/core/operation_lock.py +218 -0
- arcgraph/core/passes.py +142 -0
- arcgraph/core/payload_policy.py +708 -0
- arcgraph/core/precise_references.py +124 -0
- arcgraph/core/query_engine.py +3078 -0
- arcgraph/core/recovery.py +18 -0
- arcgraph/core/scanner.py +1454 -0
- arcgraph/core/schemas.py +700 -0
- arcgraph/core/semantic.py +263 -0
- arcgraph/core/semantic_metrics.py +619 -0
- arcgraph/core/sharing_retry.py +84 -0
- arcgraph/core/structural.py +260 -0
- arcgraph/core/target_resolver.py +486 -0
- arcgraph/core/type_metrics.py +67 -0
- arcgraph/core/unresolved_classification.py +471 -0
- arcgraph/core/utils.py +100 -0
- arcgraph/core/visual_contract.py +240 -0
- arcgraph/core/visual_slice_engine.py +933 -0
- arcgraph/interfaces/__init__.py +1 -0
- arcgraph/interfaces/agent_capabilities.py +493 -0
- arcgraph/interfaces/agent_help.py +313 -0
- arcgraph/interfaces/benchmark.py +548 -0
- arcgraph/interfaces/ci.py +1992 -0
- arcgraph/interfaces/cli.py +2799 -0
- arcgraph/interfaces/cli_change.py +690 -0
- arcgraph/interfaces/cli_feedback.py +176 -0
- arcgraph/interfaces/cli_ops.py +69 -0
- arcgraph/interfaces/cli_support.py +113 -0
- arcgraph/interfaces/cli_trial.py +42 -0
- arcgraph/interfaces/cli_visual.py +601 -0
- arcgraph/interfaces/cli_workspace.py +300 -0
- arcgraph/interfaces/client_setup.py +481 -0
- arcgraph/interfaces/docs.py +1255 -0
- arcgraph/interfaces/human_output.py +409 -0
- arcgraph/interfaces/local_state.py +577 -0
- arcgraph/interfaces/mcp_server.py +457 -0
- arcgraph/interfaces/mcp_tools.py +1209 -0
- arcgraph/interfaces/metrics.py +571 -0
- arcgraph/interfaces/ops.py +330 -0
- arcgraph/interfaces/precision.py +1670 -0
- arcgraph/interfaces/project_probe.py +75 -0
- arcgraph/interfaces/reports.py +1121 -0
- arcgraph/interfaces/stdio_encoding.py +29 -0
- arcgraph/interfaces/trace.py +592 -0
- arcgraph/interfaces/trial_feedback.py +519 -0
- arcgraph/interfaces/trial_setup.py +535 -0
- arcgraph/interfaces/visual_server.py +354 -0
- arcgraph/interfaces/visual_slice.py +168 -0
- arcgraph/interfaces/visual_smoke.py +666 -0
- arcgraph/interfaces/workbench.py +576 -0
- arcgraph/interfaces/workspace.py +1513 -0
- arcgraph/pipeline/__init__.py +1 -0
- arcgraph/pipeline/contracts.py +122 -0
- arcgraph/pipeline/external_frontend.py +451 -0
- arcgraph/pipeline/external_l3_frontends.py +368 -0
- arcgraph/pipeline/external_protocol.py +358 -0
- arcgraph/pipeline/frontends.py +437 -0
- arcgraph/pipeline/indexer.py +661 -0
- arcgraph/pipeline/openapi_frontend.py +708 -0
- arcgraph/pipeline/python_frontend.py +298 -0
- arcgraph/pipeline/reindexer.py +1333 -0
- arcgraph/pipeline/scip_frontend.py +726 -0
- arcgraph/pipeline/typescript_extractor/declarations.mjs +183 -0
- arcgraph/pipeline/typescript_extractor/edges.mjs +2794 -0
- arcgraph/pipeline/typescript_extractor/graph_primitives.mjs +189 -0
- arcgraph/pipeline/typescript_extractor/module_resolution.mjs +877 -0
- arcgraph/pipeline/typescript_extractor/runtime.mjs +56 -0
- arcgraph/pipeline/typescript_extractor/similarity.mjs +619 -0
- arcgraph/pipeline/typescript_extractor/similarity_runner.mjs +19 -0
- arcgraph/pipeline/typescript_extractor/sources.mjs +23 -0
- arcgraph/pipeline/typescript_extractor.mjs +177 -0
- arcgraph/pipeline/typescript_frameworks.py +947 -0
- arcgraph/pipeline/typescript_frontend.py +656 -0
- arcgraph/pipeline/typescript_references.mjs +167 -0
- arcgraph/providers/__init__.py +27 -0
- arcgraph/providers/context_provider.py +1258 -0
- arcgraph/providers/flow_payload.py +117 -0
- arcgraph/providers/memory_connector.py +216 -0
- arcgraph/providers/orchestrator.py +475 -0
- arcgraph/providers/relationship_payload.py +102 -0
- arcgraph/providers/risk_provider.py +843 -0
- arcgraph/providers/why_provider.py +349 -0
- arcgraph/version_info.py +246 -0
- arcgraph-0.1.0.dist-info/METADATA +676 -0
- arcgraph-0.1.0.dist-info/RECORD +165 -0
- arcgraph-0.1.0.dist-info/WHEEL +4 -0
- arcgraph-0.1.0.dist-info/entry_points.txt +2 -0
- arcgraph-0.1.0.dist-info/licenses/LICENSE +203 -0
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
"""Read-only baseline and current source providers for deterministic diffs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import ast
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
import hashlib
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
import re
|
|
10
|
+
import subprocess
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from arcgraph.change.contracts import (
|
|
14
|
+
SOURCE_NORMALIZATION_PROJECTION_VERSION,
|
|
15
|
+
SOURCE_PROVIDER_VERSION,
|
|
16
|
+
BaselineReference,
|
|
17
|
+
canonical_digest,
|
|
18
|
+
)
|
|
19
|
+
from arcgraph.change.errors import (
|
|
20
|
+
BaselineGitObjectMissing,
|
|
21
|
+
BaselineSourceIdentityMismatch,
|
|
22
|
+
BaselineSourceUnavailable,
|
|
23
|
+
)
|
|
24
|
+
from arcgraph.change.paths import NormalizedRepositoryPath, normalize_repository_path
|
|
25
|
+
|
|
26
|
+
_COMMIT = re.compile(r"^[0-9a-fA-F]{7,64}$")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True, slots=True)
|
|
30
|
+
class SourceNormalization:
|
|
31
|
+
raw_source_digest: str
|
|
32
|
+
normalized_implementation_digest: str | None
|
|
33
|
+
status: str
|
|
34
|
+
projection_version: str = SOURCE_NORMALIZATION_PROJECTION_VERSION
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True, slots=True)
|
|
38
|
+
class SourceDocument:
|
|
39
|
+
path: NormalizedRepositoryPath
|
|
40
|
+
content: bytes
|
|
41
|
+
normalization: SourceNormalization
|
|
42
|
+
source_identity: dict[str, Any]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class BaselineSourceProvider:
|
|
46
|
+
"""Read exact old-source blobs from the baseline Git commit only."""
|
|
47
|
+
|
|
48
|
+
def __init__(self, repo_root: Path, baseline: BaselineReference) -> None:
|
|
49
|
+
self.repo_root = repo_root.resolve()
|
|
50
|
+
self.baseline = baseline
|
|
51
|
+
commit_sha = baseline.working_tree_identity.head_sha
|
|
52
|
+
if not baseline.working_tree_identity.clean or not commit_sha:
|
|
53
|
+
raise BaselineSourceUnavailable(
|
|
54
|
+
"baseline source provider requires a clean baseline Git commit"
|
|
55
|
+
)
|
|
56
|
+
if not _COMMIT.fullmatch(commit_sha):
|
|
57
|
+
raise BaselineSourceIdentityMismatch(
|
|
58
|
+
"baseline commit SHA has invalid syntax"
|
|
59
|
+
)
|
|
60
|
+
if baseline.build_identity.commit_sha != commit_sha:
|
|
61
|
+
raise BaselineSourceIdentityMismatch(
|
|
62
|
+
"baseline build commit does not match baseline source commit"
|
|
63
|
+
)
|
|
64
|
+
self.commit_sha = commit_sha
|
|
65
|
+
self.git_tree_sha = _git_tree_or_raise(self.repo_root, commit_sha)
|
|
66
|
+
if self.git_tree_sha != baseline.build_identity.git_tree_sha:
|
|
67
|
+
raise BaselineSourceIdentityMismatch(
|
|
68
|
+
"baseline source tree does not match baseline build tree"
|
|
69
|
+
)
|
|
70
|
+
expected = {
|
|
71
|
+
"repo_id": baseline.repo_id,
|
|
72
|
+
"commit_sha": commit_sha,
|
|
73
|
+
"git_tree_sha": self.git_tree_sha,
|
|
74
|
+
"source_provider_version": SOURCE_PROVIDER_VERSION,
|
|
75
|
+
}
|
|
76
|
+
if canonical_digest(expected) != canonical_digest(
|
|
77
|
+
baseline.baseline_source_identity
|
|
78
|
+
):
|
|
79
|
+
raise BaselineSourceIdentityMismatch(
|
|
80
|
+
"baseline source provider identity differs"
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
def read(self, path: str | Path) -> SourceDocument:
|
|
84
|
+
normalized = normalize_repository_path(path, self.repo_root)
|
|
85
|
+
spec = f"{self.commit_sha}:{normalized.display_path}"
|
|
86
|
+
content = _git_bytes(self.repo_root, ["show", "--no-ext-diff", spec])
|
|
87
|
+
return SourceDocument(
|
|
88
|
+
path=normalized,
|
|
89
|
+
content=content,
|
|
90
|
+
normalization=normalize_implementation_source(
|
|
91
|
+
normalized.display_path, content
|
|
92
|
+
),
|
|
93
|
+
source_identity={
|
|
94
|
+
"repo_id": self.baseline.repo_id,
|
|
95
|
+
"mode": "baseline_git_commit",
|
|
96
|
+
"commit_sha": self.commit_sha,
|
|
97
|
+
"git_tree_sha": self.git_tree_sha,
|
|
98
|
+
"source_provider_version": SOURCE_PROVIDER_VERSION,
|
|
99
|
+
},
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class CurrentSourceProvider:
|
|
104
|
+
"""Read either a fixed current commit or the explicit current working tree."""
|
|
105
|
+
|
|
106
|
+
def __init__(
|
|
107
|
+
self,
|
|
108
|
+
repo_root: Path,
|
|
109
|
+
*,
|
|
110
|
+
repo_id: str,
|
|
111
|
+
commit_sha: str | None = None,
|
|
112
|
+
git_tree_sha: str | None = None,
|
|
113
|
+
) -> None:
|
|
114
|
+
self.repo_root = repo_root.resolve()
|
|
115
|
+
self.repo_id = repo_id
|
|
116
|
+
self.commit_sha = commit_sha
|
|
117
|
+
self.git_tree_sha = git_tree_sha
|
|
118
|
+
if commit_sha is not None and not _COMMIT.fullmatch(commit_sha):
|
|
119
|
+
raise BaselineSourceIdentityMismatch(
|
|
120
|
+
"current commit SHA has invalid syntax"
|
|
121
|
+
)
|
|
122
|
+
if commit_sha is not None and git_tree_sha is None:
|
|
123
|
+
self.git_tree_sha = _git_tree_or_raise(self.repo_root, commit_sha)
|
|
124
|
+
|
|
125
|
+
def read(self, path: str | Path) -> SourceDocument:
|
|
126
|
+
normalized = normalize_repository_path(path, self.repo_root)
|
|
127
|
+
if self.commit_sha:
|
|
128
|
+
content = _git_bytes(
|
|
129
|
+
self.repo_root,
|
|
130
|
+
[
|
|
131
|
+
"show",
|
|
132
|
+
"--no-ext-diff",
|
|
133
|
+
f"{self.commit_sha}:{normalized.display_path}",
|
|
134
|
+
],
|
|
135
|
+
)
|
|
136
|
+
mode = "current_git_commit"
|
|
137
|
+
else:
|
|
138
|
+
disk_path = self.repo_root / Path(*normalized.display_path.split("/"))
|
|
139
|
+
if disk_path.is_symlink() or not disk_path.is_file():
|
|
140
|
+
raise BaselineSourceUnavailable(
|
|
141
|
+
f"current source path cannot be read: {normalized.display_path}"
|
|
142
|
+
)
|
|
143
|
+
content = disk_path.read_bytes()
|
|
144
|
+
mode = "current_working_tree"
|
|
145
|
+
return SourceDocument(
|
|
146
|
+
path=normalized,
|
|
147
|
+
content=content,
|
|
148
|
+
normalization=normalize_implementation_source(
|
|
149
|
+
normalized.display_path, content
|
|
150
|
+
),
|
|
151
|
+
source_identity={
|
|
152
|
+
"repo_id": self.repo_id,
|
|
153
|
+
"mode": mode,
|
|
154
|
+
"commit_sha": self.commit_sha,
|
|
155
|
+
"git_tree_sha": self.git_tree_sha,
|
|
156
|
+
"source_provider_version": SOURCE_PROVIDER_VERSION,
|
|
157
|
+
},
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def normalize_implementation_source(path: str, content: bytes) -> SourceNormalization:
|
|
162
|
+
"""Produce a conservative, non-LLM implementation projection.
|
|
163
|
+
|
|
164
|
+
Python uses the standard AST with attributes omitted. This intentionally
|
|
165
|
+
retains literals, operators, control-flow nodes, calls, returns, and
|
|
166
|
+
expression structure while removing comments, whitespace, and line-ending
|
|
167
|
+
noise. Unsupported languages and invalid syntax are ``unknown`` rather
|
|
168
|
+
than falsely classified as a no-op.
|
|
169
|
+
"""
|
|
170
|
+
|
|
171
|
+
return _normalize_python_projection(path, content, content)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def normalize_implementation_region(
|
|
175
|
+
path: str,
|
|
176
|
+
content: bytes,
|
|
177
|
+
*,
|
|
178
|
+
start_line: int,
|
|
179
|
+
end_line: int,
|
|
180
|
+
) -> SourceNormalization:
|
|
181
|
+
"""Project one graph-mapped source region, rather than its whole file.
|
|
182
|
+
|
|
183
|
+
A ChangedRegion is authorized at Symbol granularity. Its source evidence
|
|
184
|
+
must therefore be derived from the mapped Symbol's exact raw line range and
|
|
185
|
+
from the smallest enclosing Python AST node. Parsing the complete document
|
|
186
|
+
preserves context while the selected AST node keeps unrelated symbols out
|
|
187
|
+
of the comparison projection.
|
|
188
|
+
"""
|
|
189
|
+
|
|
190
|
+
region = _source_region_bytes(content, start_line, end_line)
|
|
191
|
+
raw_content = region if region is not None else content
|
|
192
|
+
return _normalize_python_projection(
|
|
193
|
+
path,
|
|
194
|
+
content,
|
|
195
|
+
raw_content,
|
|
196
|
+
start_line=start_line,
|
|
197
|
+
end_line=end_line,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _normalize_python_projection(
|
|
202
|
+
path: str,
|
|
203
|
+
content: bytes,
|
|
204
|
+
raw_content: bytes,
|
|
205
|
+
*,
|
|
206
|
+
start_line: int | None = None,
|
|
207
|
+
end_line: int | None = None,
|
|
208
|
+
) -> SourceNormalization:
|
|
209
|
+
raw_digest = hashlib.sha256(raw_content).hexdigest()
|
|
210
|
+
if Path(path).suffix.lower() not in {".py", ".pyi"}:
|
|
211
|
+
return SourceNormalization(raw_digest, None, "unknown")
|
|
212
|
+
try:
|
|
213
|
+
source = content.decode("utf-8")
|
|
214
|
+
tree = ast.parse(source, filename=path)
|
|
215
|
+
except (SyntaxError, UnicodeDecodeError):
|
|
216
|
+
return SourceNormalization(raw_digest, None, "unknown")
|
|
217
|
+
if start_line is None and end_line is None:
|
|
218
|
+
projection_node: ast.AST | None = tree
|
|
219
|
+
elif start_line is not None and end_line is not None:
|
|
220
|
+
projection_node = _smallest_ast_region(
|
|
221
|
+
tree,
|
|
222
|
+
start_line,
|
|
223
|
+
end_line,
|
|
224
|
+
source_line_count=max(1, len(content.splitlines())),
|
|
225
|
+
)
|
|
226
|
+
else:
|
|
227
|
+
projection_node = None
|
|
228
|
+
if projection_node is None:
|
|
229
|
+
return SourceNormalization(raw_digest, None, "unknown")
|
|
230
|
+
projection = ast.dump(
|
|
231
|
+
projection_node,
|
|
232
|
+
annotate_fields=True,
|
|
233
|
+
include_attributes=False,
|
|
234
|
+
)
|
|
235
|
+
return SourceNormalization(
|
|
236
|
+
raw_digest,
|
|
237
|
+
hashlib.sha256(projection.encode("utf-8")).hexdigest(),
|
|
238
|
+
"normalized",
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _source_region_bytes(
|
|
243
|
+
content: bytes,
|
|
244
|
+
start_line: int,
|
|
245
|
+
end_line: int,
|
|
246
|
+
) -> bytes | None:
|
|
247
|
+
lines = content.splitlines(keepends=True)
|
|
248
|
+
if start_line < 1 or end_line < start_line or end_line > len(lines):
|
|
249
|
+
return None
|
|
250
|
+
return b"".join(lines[start_line - 1 : end_line])
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _smallest_ast_region(
|
|
254
|
+
tree: ast.Module,
|
|
255
|
+
start_line: int,
|
|
256
|
+
end_line: int,
|
|
257
|
+
*,
|
|
258
|
+
source_line_count: int,
|
|
259
|
+
) -> ast.AST | None:
|
|
260
|
+
if start_line < 1 or end_line < start_line:
|
|
261
|
+
return None
|
|
262
|
+
candidates = [
|
|
263
|
+
node
|
|
264
|
+
for node in ast.walk(tree)
|
|
265
|
+
if isinstance(getattr(node, "lineno", None), int)
|
|
266
|
+
and isinstance(getattr(node, "end_lineno", None), int)
|
|
267
|
+
and node.lineno <= start_line
|
|
268
|
+
and node.end_lineno >= end_line
|
|
269
|
+
]
|
|
270
|
+
if candidates:
|
|
271
|
+
return min(
|
|
272
|
+
candidates,
|
|
273
|
+
key=lambda node: (
|
|
274
|
+
int(getattr(node, "end_lineno")) - int(getattr(node, "lineno")),
|
|
275
|
+
type(node).__name__,
|
|
276
|
+
),
|
|
277
|
+
)
|
|
278
|
+
if start_line == 1 and end_line >= source_line_count:
|
|
279
|
+
return tree
|
|
280
|
+
return None
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _git_tree_or_raise(repo_root: Path, commit_sha: str) -> str:
|
|
284
|
+
value = _git_bytes(repo_root, ["rev-parse", f"{commit_sha}^{{tree}}"])
|
|
285
|
+
tree = value.decode("ascii", errors="strict").strip()
|
|
286
|
+
if not _COMMIT.fullmatch(tree):
|
|
287
|
+
raise BaselineGitObjectMissing("Git did not return a valid tree object id")
|
|
288
|
+
return tree
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _git_bytes(repo_root: Path, args: list[str]) -> bytes:
|
|
292
|
+
result = subprocess.run(
|
|
293
|
+
["git", *args],
|
|
294
|
+
cwd=repo_root,
|
|
295
|
+
check=False,
|
|
296
|
+
capture_output=True,
|
|
297
|
+
text=False,
|
|
298
|
+
)
|
|
299
|
+
if result.returncode != 0:
|
|
300
|
+
detail = result.stderr.decode("utf-8", errors="replace").strip()
|
|
301
|
+
lowered = detail.lower()
|
|
302
|
+
if (
|
|
303
|
+
"bad object" in lowered
|
|
304
|
+
or "path '" in lowered
|
|
305
|
+
or "unknown revision" in lowered
|
|
306
|
+
or "ambiguous argument" in lowered
|
|
307
|
+
or "invalid object name" in lowered
|
|
308
|
+
):
|
|
309
|
+
raise BaselineGitObjectMissing(detail or "Git object is unavailable")
|
|
310
|
+
raise BaselineSourceUnavailable(detail or "Git source provider is unavailable")
|
|
311
|
+
return result.stdout
|