@homericintelligence/athena-opencode 0.5.0 → 0.5.1
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.
- package/README.md +30 -17
- package/package.json +2 -2
- package/plugin.js +20 -5
- package/skills/TECHNICAL_ENGLISH.md +78 -0
- package/skills/_cli.py +26 -18
- package/skills/_support/docs/dependency-resolution.md +192 -0
- package/skills/_support/docs/policies/development.md +117 -0
- package/skills/_support/docs/policies/evidence-integrity.md +40 -0
- package/skills/_support/docs/principles/README.md +542 -0
- package/skills/_support/docs/principles/details/p001-kiss.md +114 -0
- package/skills/_support/docs/principles/details/p002-yagni.md +109 -0
- package/skills/_support/docs/principles/details/p003-dry.md +126 -0
- package/skills/_support/docs/principles/details/p004-solid.md +165 -0
- package/skills/_support/docs/principles/details/p005-modularity.md +123 -0
- package/skills/_support/docs/principles/details/p006-principle-of-least-astonishment.md +115 -0
- package/skills/_support/docs/principles/details/p007-subtraction-over-addition.md +113 -0
- package/skills/_support/docs/principles/details/p008-understand-before-subtracting.md +115 -0
- package/skills/_support/docs/principles/details/p009-general-mechanisms-over-special-cases.md +108 -0
- package/skills/_support/docs/principles/details/p010-scope-fidelity.md +117 -0
- package/skills/_support/docs/principles/details/p011-minimal-coherent-change.md +128 -0
- package/skills/_support/docs/principles/details/p012-evidence-before-modification.md +115 -0
- package/skills/_support/docs/principles/details/p013-avoid-hasty-abstractions.md +112 -0
- package/skills/_support/docs/principles/details/p014-preserve-unrequested-behavior.md +110 -0
- package/skills/_support/docs/principles/details/p015-architecture-conformance.md +124 -0
- package/skills/_support/docs/principles/details/p016-separation-of-concerns.md +125 -0
- package/skills/_support/docs/principles/details/p017-high-cohesion-low-coupling.md +130 -0
- package/skills/_support/docs/principles/details/p018-information-hiding.md +128 -0
- package/skills/_support/docs/principles/details/p019-explicit-contracts.md +134 -0
- package/skills/_support/docs/principles/details/p020-executable-architecture.md +130 -0
- package/skills/_support/docs/principles/details/p021-evolutionary-and-reversible-design.md +126 -0
- package/skills/_support/docs/principles/details/p022-test-behavior-not-implementation.md +129 -0
- package/skills/_support/docs/principles/details/p023-parameterized-table-driven-testing.md +129 -0
- package/skills/_support/docs/principles/details/p024-boundary-value-testing.md +128 -0
- package/skills/_support/docs/principles/details/p025-property-based-testing-for-invariants.md +133 -0
- package/skills/_support/docs/principles/details/p026-regression-before-repair.md +134 -0
- package/skills/_support/docs/principles/details/p027-deterministic-and-hermetic-tests.md +128 -0
- package/skills/_support/docs/principles/details/p028-test-failure-paths.md +133 -0
- package/skills/_support/docs/principles/details/p029-generalize-error-policy-preserve-specific-cause.md +131 -0
- package/skills/_support/docs/principles/details/p030-nearest-responsible-error-boundary.md +133 -0
- package/skills/_support/docs/principles/details/p031-propagate-rather-than-swallow.md +131 -0
- package/skills/_support/docs/principles/details/p032-handle-once-preserve-causality.md +137 -0
- package/skills/_support/docs/principles/details/p033-state-safe-failure-semantics.md +140 -0
- package/skills/_support/docs/principles/details/p034-fail-fast.md +132 -0
- package/skills/_support/docs/principles/details/p035-fail-secure-fail-closed.md +137 -0
- package/skills/_support/docs/principles/details/p036-graceful-degradation.md +137 -0
- package/skills/_support/docs/principles/details/p037-idempotency-before-retry.md +139 -0
- package/skills/_support/docs/principles/details/p038-bounded-retry.md +145 -0
- package/skills/_support/docs/principles/details/p039-bounded-waiting.md +151 -0
- package/skills/_support/docs/principles/details/p040-bounded-resources.md +138 -0
- package/skills/_support/docs/principles/details/p041-backpressure-and-load-shedding.md +141 -0
- package/skills/_support/docs/principles/details/p042-fault-isolation-bulkheads.md +140 -0
- package/skills/_support/docs/principles/details/p043-circuit-breakers.md +148 -0
- package/skills/_support/docs/principles/details/p044-atomicity-where-possible.md +141 -0
- package/skills/_support/docs/principles/details/p045-compensation-where-atomicity-is-impossible.md +163 -0
- package/skills/_support/docs/principles/details/p046-resumability.md +138 -0
- package/skills/_support/docs/principles/details/p047-observability-is-part-of-correctness.md +134 -0
- package/skills/_support/docs/principles/details/p048-secure-by-design.md +132 -0
- package/skills/_support/docs/principles/details/p049-secure-by-default.md +126 -0
- package/skills/_support/docs/principles/details/p050-least-privilege.md +123 -0
- package/skills/_support/docs/principles/details/p051-complete-mediation.md +124 -0
- package/skills/_support/docs/principles/details/p052-separation-of-duties.md +129 -0
- package/skills/_support/docs/principles/details/p053-validate-at-trust-boundaries.md +126 -0
- package/skills/_support/docs/principles/details/p054-defense-in-depth.md +135 -0
- package/skills/_support/docs/principles/details/p055-minimize-attack-surface.md +133 -0
- package/skills/_support/docs/principles/details/p056-secrets-stay-out-of-code-and-context.md +137 -0
- package/skills/_support/docs/principles/details/p057-supply-chain-integrity.md +135 -0
- package/skills/_support/docs/principles/details/p058-bounded-agent-authority.md +137 -0
- package/skills/_support/docs/principles/details/p059-data-is-not-instruction.md +130 -0
- package/skills/_support/docs/principles/details/p060-constrain-sub-agents.md +136 -0
- package/skills/_support/docs/principles/details/p061-separate-decision-from-high-impact-execution.md +131 -0
- package/skills/_support/docs/principles/details/p062-human-approval-for-irreversible-or-high-risk-actions.md +129 -0
- package/skills/_support/docs/principles/details/p063-requirement-to-code-traceability.md +116 -0
- package/skills/_support/docs/principles/details/p064-requirement-to-test-traceability.md +116 -0
- package/skills/_support/docs/principles/details/p065-verify-before-claiming-completion.md +129 -0
- package/skills/_support/docs/principles/details/p066-preserve-existing-work.md +114 -0
- package/skills/_support/docs/principles/details/p067-no-test-cheating.md +126 -0
- package/skills/_support/docs/principles/details/p068-no-validation-bypass.md +119 -0
- package/skills/_support/docs/principles/details/p069-independent-review-for-high-risk-changes.md +129 -0
- package/skills/_support/docs/principles/details/p070-code-health-must-not-regress.md +116 -0
- package/skills/_support/docs/principles/details/p071-consistency-over-personal-preference.md +117 -0
- package/skills/_support/docs/principles/details/p072-technical-evidence-over-preference.md +122 -0
- package/skills/_support/docs/principles/details/p073-optimize-only-with-evidence.md +123 -0
- package/skills/_support/docs/principles/details/p074-prefer-existing-mechanisms.md +121 -0
- package/skills/_support/docs/principles/details/p075-make-invalid-states-hard-to-represent.md +128 -0
- package/skills/_support/docs/principles/details/p076-parse-then-validate-then-operate.md +133 -0
- package/skills/_support/docs/principles/details/p077-separate-policy-from-mechanism.md +112 -0
- package/skills/_support/docs/principles/details/p078-single-source-of-truth.md +131 -0
- package/skills/_support/docs/principles/details/p079-explicit-ownership-and-lifetimes.md +124 -0
- package/skills/_support/docs/principles/details/p080-make-concurrency-deliberate.md +122 -0
- package/skills/_support/docs/principles/details/p081-forward-progress-with-safety.md +125 -0
- package/skills/_support/docs/principles/details/p082-design-for-cancellation.md +133 -0
- package/skills/_support/docs/principles/details/p083-irreversible-actions-last.md +124 -0
- package/skills/_support/docs/principles/details/p084-prefer-local-reasoning.md +131 -0
- package/skills/_support/docs/principles/details/p085-explicit-is-better-than-implicit.md +123 -0
- package/skills/_support/docs/principles/details/p086-readability-counts.md +113 -0
- package/skills/_support/docs/principles/details/p087-comments-explain-why-code-explains-what.md +120 -0
- package/skills/_support/docs/principles/details/p088-delete-dead-code.md +125 -0
- package/skills/_support/docs/principles/details/p089-delete-obsolete-configuration-and-dependencies.md +125 -0
- package/skills/_support/docs/principles/details/p090-prefer-negative-code.md +134 -0
- package/skills/_support/docs/principles/details/p091-test-driven-development.md +149 -0
- package/skills/_support/docs/review/README.md +59 -0
- package/skills/_support/docs/review/behavior-first-testing.md +73 -0
- package/skills/_support/docs/review/common.md +365 -0
- package/skills/_support/docs/review/design-docs.md +74 -0
- package/skills/_support/docs/review/issue-planning.md +220 -0
- package/skills/_support/docs/review/language-routing.md +141 -0
- package/skills/_support/docs/review/repository-scorecard.md +170 -0
- package/skills/advise/SKILL.md +93 -67
- package/skills/advise/scripts/list_retrievable_skills.py +3 -3
- package/skills/brainstorm/SKILL.md +160 -127
- package/skills/change-review/SKILL.md +91 -71
- package/skills/change-review/references/scope-resolution.md +69 -46
- package/skills/change-review/scripts/resolve_scope.py +119 -73
- package/skills/finalize-plan/SKILL.md +198 -139
- package/skills/git-worktrees/SKILL.md +100 -80
- package/skills/git-worktrees/scripts/prepare_worktree.py +16 -10
- package/skills/issue-review/SKILL.md +136 -79
- package/skills/learn/SKILL.md +276 -186
- package/skills/myrmidon-swarm/SKILL.md +126 -86
- package/skills/plan-issue/SKILL.md +122 -81
- package/skills/pr-review/SKILL.md +164 -114
- package/skills/pr-review/references/criteria.md +58 -37
- package/skills/pr-review/references/delivery.md +156 -98
- package/skills/pr-review/references/evidence.md +265 -148
- package/skills/pr-review/references/prevalidated.md +93 -61
- package/skills/pr-review/scripts/collect_evidence.py +211 -140
- package/skills/pr-review/scripts/diff_context.py +6 -4
- package/skills/pr-review/scripts/materialize_snapshot.py +112 -120
- package/skills/pr-review/scripts/pr_identity.py +27 -15
- package/skills/pr-review/scripts/resolve_pr.py +53 -31
- package/skills/repo-review/SKILL.md +202 -131
- package/skills/systematic-debugging/SKILL.md +210 -192
- package/skills/systematic-debugging/scripts/repository_evidence.py +9 -4
- package/skills/test-driven-development/SKILL.md +126 -96
- package/skills/tidy/SKILL.md +92 -59
- package/skills/tidy/scripts/run_tidy.py +3 -3
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""Resolve a change
|
|
2
|
+
"""Resolve the scope for a change review. Do not change the repository or Git state."""
|
|
3
3
|
|
|
4
4
|
from __future__ import annotations
|
|
5
5
|
|
|
@@ -34,7 +34,7 @@ MAX_WORKTREE_CANDIDATES = 50_000
|
|
|
34
34
|
|
|
35
35
|
|
|
36
36
|
def git_bytes(*arguments: str, repository_root: Path | None = None) -> bytes:
|
|
37
|
-
"""Run Git and return its raw
|
|
37
|
+
"""Run Git and return its raw standard output or raise a concise error."""
|
|
38
38
|
command = git_command(arguments, repository_root)
|
|
39
39
|
result = run_command(
|
|
40
40
|
command,
|
|
@@ -47,7 +47,9 @@ def git_bytes(*arguments: str, repository_root: Path | None = None) -> bytes:
|
|
|
47
47
|
stderr = cast(bytes, result.stderr)
|
|
48
48
|
if result.returncode != 0:
|
|
49
49
|
message = stderr.decode("utf-8", errors="replace").strip()
|
|
50
|
-
raise RuntimeError(
|
|
50
|
+
raise RuntimeError(
|
|
51
|
+
message or f"The Git command failed. Command: git {' '.join(arguments)}"
|
|
52
|
+
)
|
|
51
53
|
return stdout
|
|
52
54
|
|
|
53
55
|
|
|
@@ -60,13 +62,13 @@ def git_text(*arguments: str, repository_root: Path | None = None) -> str:
|
|
|
60
62
|
|
|
61
63
|
|
|
62
64
|
def path_list(document: bytes) -> list[str]:
|
|
63
|
-
"""Return sorted Git
|
|
65
|
+
"""Return sorted Git paths from null-byte-delimited data without lossy parsing."""
|
|
64
66
|
return sorted(os.fsdecode(path) for path in document.split(b"\0") if path)
|
|
65
67
|
|
|
66
68
|
|
|
67
69
|
@dataclass(frozen=True)
|
|
68
70
|
class PathEntry:
|
|
69
|
-
"""
|
|
71
|
+
"""This record describes one worktree or immutable Git-object manifest entry."""
|
|
70
72
|
|
|
71
73
|
path: str
|
|
72
74
|
kind: str
|
|
@@ -77,7 +79,7 @@ class PathEntry:
|
|
|
77
79
|
|
|
78
80
|
@dataclass(frozen=True)
|
|
79
81
|
class ContentFingerprint:
|
|
80
|
-
"""
|
|
82
|
+
"""This record contains a bounded identity for a byte stream of any size."""
|
|
81
83
|
|
|
82
84
|
length: int
|
|
83
85
|
digest: str
|
|
@@ -85,7 +87,7 @@ class ContentFingerprint:
|
|
|
85
87
|
|
|
86
88
|
@dataclass(frozen=True)
|
|
87
89
|
class ScopeCapture:
|
|
88
|
-
"""
|
|
90
|
+
"""This record contains one scope observation that detects worktree races."""
|
|
89
91
|
|
|
90
92
|
paths: tuple[str, ...]
|
|
91
93
|
tracked_paths: tuple[str, ...]
|
|
@@ -135,12 +137,15 @@ def git_stream_fingerprint(
|
|
|
135
137
|
)
|
|
136
138
|
except FileNotFoundError as error:
|
|
137
139
|
raise RuntimeError(
|
|
138
|
-
|
|
140
|
+
"The required command is not available: "
|
|
141
|
+
f"'{error.filename or command[0]}'."
|
|
139
142
|
) from error
|
|
140
143
|
try:
|
|
141
144
|
stdout = process.stdout
|
|
142
145
|
if stdout is None:
|
|
143
|
-
raise RuntimeError(
|
|
146
|
+
raise RuntimeError(
|
|
147
|
+
"Git did not provide standard output for scope capture."
|
|
148
|
+
)
|
|
144
149
|
try:
|
|
145
150
|
fingerprint = content_fingerprint(
|
|
146
151
|
iter(lambda: stdout.read(READ_CHUNK_SIZE), b"")
|
|
@@ -160,10 +165,16 @@ def git_stream_fingerprint(
|
|
|
160
165
|
.decode("utf-8", errors="replace")
|
|
161
166
|
.strip()
|
|
162
167
|
)
|
|
163
|
-
raise RuntimeError(
|
|
168
|
+
raise RuntimeError(
|
|
169
|
+
message
|
|
170
|
+
or f"The Git command failed. Command: git {' '.join(arguments)}"
|
|
171
|
+
)
|
|
164
172
|
return fingerprint
|
|
165
173
|
except OSError as error:
|
|
166
|
-
raise RuntimeError(
|
|
174
|
+
raise RuntimeError(
|
|
175
|
+
"The tool cannot stream Git output. "
|
|
176
|
+
f"The operation returned this diagnostic.\n{error}"
|
|
177
|
+
) from error
|
|
167
178
|
|
|
168
179
|
|
|
169
180
|
def consume_git_nul_records(
|
|
@@ -171,7 +182,7 @@ def consume_git_nul_records(
|
|
|
171
182
|
repository_root: Path,
|
|
172
183
|
consume: Callable[[bytes], None],
|
|
173
184
|
) -> None:
|
|
174
|
-
"""Pass Git
|
|
185
|
+
"""Pass null-byte-delimited Git records without buffering command output."""
|
|
175
186
|
command = git_command(arguments, repository_root)
|
|
176
187
|
try:
|
|
177
188
|
with tempfile.TemporaryFile() as error_output:
|
|
@@ -184,13 +195,16 @@ def consume_git_nul_records(
|
|
|
184
195
|
)
|
|
185
196
|
except FileNotFoundError as error:
|
|
186
197
|
raise RuntimeError(
|
|
187
|
-
|
|
198
|
+
"The required command is not available: "
|
|
199
|
+
f"'{error.filename or command[0]}'."
|
|
188
200
|
) from error
|
|
189
201
|
stdout = process.stdout
|
|
190
202
|
if stdout is None:
|
|
191
203
|
process.kill()
|
|
192
204
|
process.wait()
|
|
193
|
-
raise RuntimeError(
|
|
205
|
+
raise RuntimeError(
|
|
206
|
+
"Git did not provide standard output for scope capture."
|
|
207
|
+
)
|
|
194
208
|
pending = b""
|
|
195
209
|
try:
|
|
196
210
|
while chunk := stdout.read(READ_CHUNK_SIZE):
|
|
@@ -199,17 +213,18 @@ def consume_git_nul_records(
|
|
|
199
213
|
pending = records.pop()
|
|
200
214
|
if len(pending) > MAX_METADATA_RECORD_BYTES:
|
|
201
215
|
raise RuntimeError(
|
|
202
|
-
"Git metadata record
|
|
216
|
+
"The Git metadata record is larger than the safe scope limit."
|
|
203
217
|
)
|
|
204
218
|
for record in records:
|
|
205
219
|
if record:
|
|
206
220
|
if len(record) > MAX_METADATA_RECORD_BYTES:
|
|
207
221
|
raise RuntimeError(
|
|
208
|
-
"Git metadata record
|
|
222
|
+
"The Git metadata record is larger than the safe scope "
|
|
223
|
+
"limit."
|
|
209
224
|
)
|
|
210
225
|
consume(record)
|
|
211
226
|
if pending:
|
|
212
|
-
raise RuntimeError("
|
|
227
|
+
raise RuntimeError("The Git metadata record is not terminated.")
|
|
213
228
|
return_code = process.wait()
|
|
214
229
|
except BaseException:
|
|
215
230
|
stdout.close()
|
|
@@ -225,9 +240,15 @@ def consume_git_nul_records(
|
|
|
225
240
|
.decode("utf-8", errors="replace")
|
|
226
241
|
.strip()
|
|
227
242
|
)
|
|
228
|
-
raise RuntimeError(
|
|
243
|
+
raise RuntimeError(
|
|
244
|
+
message
|
|
245
|
+
or f"The Git command failed. Command: git {' '.join(arguments)}"
|
|
246
|
+
)
|
|
229
247
|
except OSError as error:
|
|
230
|
-
raise RuntimeError(
|
|
248
|
+
raise RuntimeError(
|
|
249
|
+
"The tool cannot stream Git metadata. "
|
|
250
|
+
f"The operation returned this diagnostic.\n{error}"
|
|
251
|
+
) from error
|
|
231
252
|
|
|
232
253
|
|
|
233
254
|
def pathspec_arguments(arguments: list[str], paths: Sequence[str]) -> list[str]:
|
|
@@ -237,7 +258,7 @@ def pathspec_arguments(arguments: list[str], paths: Sequence[str]) -> list[str]:
|
|
|
237
258
|
|
|
238
259
|
|
|
239
260
|
def normalized_paths(repository_root: Path, paths: Sequence[str]) -> list[str]:
|
|
240
|
-
"""Keep lexical filters inside the repository without following
|
|
261
|
+
"""Keep lexical filters inside the repository without following symbolic links."""
|
|
241
262
|
root = Path(os.path.abspath(os.fspath(repository_root)))
|
|
242
263
|
normalized: list[str] = []
|
|
243
264
|
for raw_path in paths:
|
|
@@ -249,15 +270,17 @@ def normalized_paths(repository_root: Path, paths: Sequence[str]) -> list[str]:
|
|
|
249
270
|
try:
|
|
250
271
|
relative = resolved.relative_to(root)
|
|
251
272
|
except ValueError as error:
|
|
252
|
-
raise RuntimeError(
|
|
273
|
+
raise RuntimeError(
|
|
274
|
+
f"The path is outside the repository: {raw_path!r}."
|
|
275
|
+
) from error
|
|
253
276
|
normalized.append(relative.as_posix())
|
|
254
277
|
return sorted(set(normalized))
|
|
255
278
|
|
|
256
279
|
|
|
257
280
|
def verified_commit(reference: str, repository_root: Path | None = None) -> str:
|
|
258
|
-
"""Resolve one non-option Git reference to an immutable commit
|
|
281
|
+
"""Resolve one non-option Git reference to an immutable commit object identifier."""
|
|
259
282
|
if not reference or reference.startswith("-"):
|
|
260
|
-
raise RuntimeError(f"
|
|
283
|
+
raise RuntimeError(f"The Git reference is not valid: {reference!r}.")
|
|
261
284
|
return git_text(
|
|
262
285
|
"rev-parse",
|
|
263
286
|
"--verify",
|
|
@@ -267,9 +290,9 @@ def verified_commit(reference: str, repository_root: Path | None = None) -> str:
|
|
|
267
290
|
|
|
268
291
|
|
|
269
292
|
def range_revisions(value: str, repository_root: Path) -> tuple[str, str]:
|
|
270
|
-
"""Resolve the required BASE..HEAD notation to immutable commit
|
|
293
|
+
"""Resolve the required BASE..HEAD notation to immutable commit object identifiers."""
|
|
271
294
|
if value.count("..") != 1:
|
|
272
|
-
raise RuntimeError("range must
|
|
295
|
+
raise RuntimeError("The range must contain exactly one BASE..HEAD separator.")
|
|
273
296
|
base_reference, head_reference = value.split("..", maxsplit=1)
|
|
274
297
|
return (
|
|
275
298
|
verified_commit(base_reference, repository_root),
|
|
@@ -368,8 +391,8 @@ def untracked_paths(paths: Sequence[str], repository_root: Path) -> list[str]:
|
|
|
368
391
|
def consume(record: bytes) -> None:
|
|
369
392
|
if len(selected) >= MAX_WORKTREE_CANDIDATES:
|
|
370
393
|
raise RuntimeError(
|
|
371
|
-
"untracked path limit "
|
|
372
|
-
|
|
394
|
+
f"The untracked path limit ({MAX_WORKTREE_CANDIDATES}) was reached. "
|
|
395
|
+
"Run the command again with narrower PATH arguments."
|
|
373
396
|
)
|
|
374
397
|
selected.append(os.fsdecode(record))
|
|
375
398
|
|
|
@@ -382,7 +405,7 @@ def untracked_paths(paths: Sequence[str], repository_root: Path) -> list[str]:
|
|
|
382
405
|
|
|
383
406
|
|
|
384
407
|
class Digest(Protocol):
|
|
385
|
-
"""
|
|
408
|
+
"""This protocol defines the minimum hashlib operations for the scope digest."""
|
|
386
409
|
|
|
387
410
|
def update(self, data: bytes) -> None:
|
|
388
411
|
"""Add bytes to the digest state."""
|
|
@@ -405,7 +428,7 @@ def path_components(relative_path: str) -> tuple[str, ...]:
|
|
|
405
428
|
"""Return a verified repository-relative path split into lexical components."""
|
|
406
429
|
components = Path(relative_path).parts
|
|
407
430
|
if not components or any(component in {".", ".."} for component in components):
|
|
408
|
-
raise RuntimeError(f"
|
|
431
|
+
raise RuntimeError(f"The repository path is not valid: {relative_path!r}.")
|
|
409
432
|
return components
|
|
410
433
|
|
|
411
434
|
|
|
@@ -421,10 +444,10 @@ def close_descriptor_quietly(descriptor: int) -> None:
|
|
|
421
444
|
def nofollow_parent_descriptor(
|
|
422
445
|
repository_root: Path, relative_path: str
|
|
423
446
|
) -> tuple[int, str]:
|
|
424
|
-
"""Open a path's parent without following any repository
|
|
447
|
+
"""Open a path's parent without following any repository symbolic link."""
|
|
425
448
|
if not hasattr(os, "O_NOFOLLOW") or not hasattr(os, "O_DIRECTORY"):
|
|
426
449
|
raise RuntimeError(
|
|
427
|
-
"host cannot inspect repository paths without following links"
|
|
450
|
+
"The host cannot inspect repository paths without following symbolic links."
|
|
428
451
|
)
|
|
429
452
|
components = path_components(relative_path)
|
|
430
453
|
descriptor: int | None = os.open(
|
|
@@ -442,13 +465,14 @@ def nofollow_parent_descriptor(
|
|
|
442
465
|
)
|
|
443
466
|
except (NotImplementedError, TypeError) as error:
|
|
444
467
|
raise RuntimeError(
|
|
445
|
-
"host cannot inspect repository paths without following
|
|
468
|
+
"The host cannot inspect repository paths without following symbolic "
|
|
469
|
+
"links."
|
|
446
470
|
) from error
|
|
447
471
|
try:
|
|
448
472
|
os.close(descriptor)
|
|
449
473
|
except OSError:
|
|
450
|
-
# `close()` leaves descriptor state unspecified
|
|
451
|
-
#
|
|
474
|
+
# After an error, `close()` leaves the descriptor state unspecified.
|
|
475
|
+
# Do not try the parent descriptor again. Always close the opened child.
|
|
452
476
|
descriptor = None
|
|
453
477
|
close_descriptor_quietly(child_descriptor)
|
|
454
478
|
raise
|
|
@@ -462,7 +486,7 @@ def nofollow_parent_descriptor(
|
|
|
462
486
|
|
|
463
487
|
|
|
464
488
|
def worktree_path_entry(repository_root: Path, relative_path: str) -> PathEntry:
|
|
465
|
-
"""Describe a path without following repository or target
|
|
489
|
+
"""Describe a path without following repository or target symbolic links."""
|
|
466
490
|
try:
|
|
467
491
|
parent_descriptor, filename = nofollow_parent_descriptor(
|
|
468
492
|
repository_root, relative_path
|
|
@@ -474,7 +498,7 @@ def worktree_path_entry(repository_root: Path, relative_path: str) -> PathEntry:
|
|
|
474
498
|
mode = os.lstat(filename, dir_fd=parent_descriptor).st_mode
|
|
475
499
|
except (NotImplementedError, TypeError) as error:
|
|
476
500
|
raise RuntimeError(
|
|
477
|
-
"host cannot inspect repository paths without following links"
|
|
501
|
+
"The host cannot inspect repository paths without following symbolic links."
|
|
478
502
|
) from error
|
|
479
503
|
except FileNotFoundError:
|
|
480
504
|
return PathEntry(relative_path, "absent")
|
|
@@ -483,7 +507,8 @@ def worktree_path_entry(repository_root: Path, relative_path: str) -> PathEntry:
|
|
|
483
507
|
target = os.readlink(filename, dir_fd=parent_descriptor)
|
|
484
508
|
except (NotImplementedError, TypeError) as error:
|
|
485
509
|
raise RuntimeError(
|
|
486
|
-
"host cannot inspect repository links without following
|
|
510
|
+
"The host cannot inspect repository symbolic links without following "
|
|
511
|
+
"them."
|
|
487
512
|
) from error
|
|
488
513
|
return PathEntry(
|
|
489
514
|
relative_path,
|
|
@@ -504,7 +529,7 @@ def worktree_path_entry(repository_root: Path, relative_path: str) -> PathEntry:
|
|
|
504
529
|
def worktree_path_entries(
|
|
505
530
|
repository_root: Path, paths: Sequence[str]
|
|
506
531
|
) -> tuple[PathEntry, ...]:
|
|
507
|
-
"""Describe
|
|
532
|
+
"""Describe each selected repository object without following symbolic links."""
|
|
508
533
|
return tuple(worktree_path_entry(repository_root, path) for path in paths)
|
|
509
534
|
|
|
510
535
|
|
|
@@ -520,7 +545,7 @@ def git_object_kind(mode: str, object_type: str) -> str:
|
|
|
520
545
|
|
|
521
546
|
|
|
522
547
|
def nul_records(document: bytes) -> list[bytes]:
|
|
523
|
-
"""Split
|
|
548
|
+
"""Split null-byte-delimited Git records and preserve the path bytes."""
|
|
524
549
|
return [record for record in document.split(b"\0") if record]
|
|
525
550
|
|
|
526
551
|
|
|
@@ -540,12 +565,14 @@ def index_entry_map(
|
|
|
540
565
|
raw_mode, raw_object_id, raw_stage = header.split()
|
|
541
566
|
except ValueError as error:
|
|
542
567
|
raise RuntimeError(
|
|
543
|
-
"
|
|
568
|
+
"The Git index entry is not valid for scope resolution."
|
|
544
569
|
) from error
|
|
545
570
|
path = os.fsdecode(raw_path)
|
|
546
571
|
stage = raw_stage.decode("ascii")
|
|
547
572
|
if stage != "0":
|
|
548
|
-
raise RuntimeError(
|
|
573
|
+
raise RuntimeError(
|
|
574
|
+
f"The selected scope contains an unmerged index entry: '{path}'."
|
|
575
|
+
)
|
|
549
576
|
mode = raw_mode.decode("ascii")
|
|
550
577
|
object_id = raw_object_id.decode("ascii")
|
|
551
578
|
entries[path] = PathEntry(
|
|
@@ -583,7 +610,7 @@ def head_tree_entry_map(
|
|
|
583
610
|
raw_mode, raw_type, raw_object_id = header.split()
|
|
584
611
|
except ValueError as error:
|
|
585
612
|
raise RuntimeError(
|
|
586
|
-
"
|
|
613
|
+
"The Git tree entry is not valid for scope resolution."
|
|
587
614
|
) from error
|
|
588
615
|
path = os.fsdecode(raw_path)
|
|
589
616
|
mode = raw_mode.decode("ascii")
|
|
@@ -609,7 +636,7 @@ def head_tree_path_entries(
|
|
|
609
636
|
|
|
610
637
|
@dataclass(frozen=True)
|
|
611
638
|
class WorktreeMetadata:
|
|
612
|
-
"""
|
|
639
|
+
"""This record contains bounded metadata for comparison of worktree candidates."""
|
|
613
640
|
|
|
614
641
|
head_entries: dict[str, PathEntry]
|
|
615
642
|
index_entries: dict[str, PathEntry]
|
|
@@ -623,8 +650,8 @@ def add_worktree_candidate(candidates: set[str], path: str) -> None:
|
|
|
623
650
|
return
|
|
624
651
|
if len(candidates) >= MAX_WORKTREE_CANDIDATES:
|
|
625
652
|
raise RuntimeError(
|
|
626
|
-
"worktree candidate limit "
|
|
627
|
-
|
|
653
|
+
f"The worktree candidate limit ({MAX_WORKTREE_CANDIDATES}) was reached. "
|
|
654
|
+
"Run the command again with narrower PATH arguments."
|
|
628
655
|
)
|
|
629
656
|
candidates.add(path)
|
|
630
657
|
|
|
@@ -635,7 +662,9 @@ def parse_head_tree_record(record: bytes) -> PathEntry:
|
|
|
635
662
|
header, raw_path = record.split(b"\t", maxsplit=1)
|
|
636
663
|
raw_mode, raw_type, raw_object_id = header.split()
|
|
637
664
|
except ValueError as error:
|
|
638
|
-
raise RuntimeError(
|
|
665
|
+
raise RuntimeError(
|
|
666
|
+
"The Git tree entry is not valid for scope resolution."
|
|
667
|
+
) from error
|
|
639
668
|
path = os.fsdecode(raw_path)
|
|
640
669
|
mode = raw_mode.decode("ascii")
|
|
641
670
|
object_type = raw_type.decode("ascii")
|
|
@@ -654,11 +683,15 @@ def parse_tagged_index_record(record: bytes) -> tuple[PathEntry, bool]:
|
|
|
654
683
|
header, raw_path = raw_entry.split(b"\t", maxsplit=1)
|
|
655
684
|
raw_mode, raw_object_id, raw_stage = header.split()
|
|
656
685
|
except ValueError as error:
|
|
657
|
-
raise RuntimeError(
|
|
686
|
+
raise RuntimeError(
|
|
687
|
+
"The Git index entry is not valid for scope resolution."
|
|
688
|
+
) from error
|
|
658
689
|
path = os.fsdecode(raw_path)
|
|
659
690
|
stage = raw_stage.decode("ascii")
|
|
660
691
|
if stage != "0":
|
|
661
|
-
raise RuntimeError(
|
|
692
|
+
raise RuntimeError(
|
|
693
|
+
f"The selected scope contains an unmerged index entry: '{path}'."
|
|
694
|
+
)
|
|
662
695
|
mode = raw_mode.decode("ascii")
|
|
663
696
|
return (
|
|
664
697
|
PathEntry(
|
|
@@ -674,7 +707,7 @@ def parse_tagged_index_record(record: bytes) -> tuple[PathEntry, bool]:
|
|
|
674
707
|
def worktree_metadata(
|
|
675
708
|
head: str, paths: Sequence[str], repository_root: Path
|
|
676
709
|
) -> WorktreeMetadata:
|
|
677
|
-
"""Stream bounded HEAD
|
|
710
|
+
"""Stream bounded HEAD and index metadata without reading worktree bytes."""
|
|
678
711
|
candidates: set[str] = set()
|
|
679
712
|
head_entries: dict[str, PathEntry] = {}
|
|
680
713
|
index_entries: dict[str, PathEntry] = {}
|
|
@@ -697,7 +730,8 @@ def worktree_metadata(
|
|
|
697
730
|
path = os.fsdecode(record)
|
|
698
731
|
if path not in candidates:
|
|
699
732
|
raise RuntimeError(
|
|
700
|
-
|
|
733
|
+
"The worktree scope metadata does not contain this staged change "
|
|
734
|
+
f"path: '{path}'."
|
|
701
735
|
)
|
|
702
736
|
staged_change_paths.add(path)
|
|
703
737
|
|
|
@@ -747,7 +781,7 @@ def worktree_metadata(
|
|
|
747
781
|
|
|
748
782
|
@dataclass(frozen=True)
|
|
749
783
|
class FileSnapshot:
|
|
750
|
-
"""
|
|
784
|
+
"""This record contains a regular-file identity and optional Git blob object identifier."""
|
|
751
785
|
|
|
752
786
|
fingerprint: ContentFingerprint
|
|
753
787
|
object_id: str | None
|
|
@@ -756,7 +790,7 @@ class FileSnapshot:
|
|
|
756
790
|
|
|
757
791
|
@dataclass(frozen=True)
|
|
758
792
|
class WorktreePathSnapshot:
|
|
759
|
-
"""
|
|
793
|
+
"""This record contains worktree metadata and the applicable raw content identity."""
|
|
760
794
|
|
|
761
795
|
entry: PathEntry
|
|
762
796
|
content: ContentFingerprint | None = None
|
|
@@ -765,7 +799,7 @@ class WorktreePathSnapshot:
|
|
|
765
799
|
|
|
766
800
|
@dataclass(frozen=True)
|
|
767
801
|
class WorktreeTrackedCapture:
|
|
768
|
-
"""
|
|
802
|
+
"""This record contains all worktree changes relative to HEAD within a size limit."""
|
|
769
803
|
|
|
770
804
|
paths: tuple[str, ...]
|
|
771
805
|
fingerprint: ContentFingerprint
|
|
@@ -780,7 +814,8 @@ def git_object_format(repository_root: Path) -> str:
|
|
|
780
814
|
hashlib.new(object_format)
|
|
781
815
|
except ValueError as error:
|
|
782
816
|
raise RuntimeError(
|
|
783
|
-
|
|
817
|
+
"The worktree review does not support this Git object format: "
|
|
818
|
+
f"'{object_format}'."
|
|
784
819
|
) from error
|
|
785
820
|
return object_format
|
|
786
821
|
|
|
@@ -817,7 +852,7 @@ def read_regular_file_snapshot_without_following(
|
|
|
817
852
|
relative_path: str,
|
|
818
853
|
object_format: str | None = None,
|
|
819
854
|
) -> FileSnapshot:
|
|
820
|
-
"""Fingerprint a regular file without following links or blocking on a
|
|
855
|
+
"""Fingerprint a regular file without following symbolic links or blocking on a named pipe."""
|
|
821
856
|
nonblocking_value = getattr(os, "O_NONBLOCK", None)
|
|
822
857
|
try:
|
|
823
858
|
if nonblocking_value is None:
|
|
@@ -825,7 +860,8 @@ def read_regular_file_snapshot_without_following(
|
|
|
825
860
|
nonblocking_flag = index(nonblocking_value)
|
|
826
861
|
except TypeError as error:
|
|
827
862
|
raise RuntimeError(
|
|
828
|
-
"host cannot inspect repository files without
|
|
863
|
+
"The host cannot inspect repository files without support for a "
|
|
864
|
+
"nonblocking open operation."
|
|
829
865
|
) from error
|
|
830
866
|
parent_descriptor, filename = nofollow_parent_descriptor(
|
|
831
867
|
repository_root, relative_path
|
|
@@ -840,7 +876,8 @@ def read_regular_file_snapshot_without_following(
|
|
|
840
876
|
)
|
|
841
877
|
except (NotImplementedError, TypeError) as error:
|
|
842
878
|
raise RuntimeError(
|
|
843
|
-
"host cannot inspect repository paths without following
|
|
879
|
+
"The host cannot inspect repository paths without following symbolic "
|
|
880
|
+
"links."
|
|
844
881
|
) from error
|
|
845
882
|
finally:
|
|
846
883
|
try:
|
|
@@ -853,7 +890,9 @@ def read_regular_file_snapshot_without_following(
|
|
|
853
890
|
try:
|
|
854
891
|
initial_stat = os.fstat(descriptor)
|
|
855
892
|
if not stat.S_ISREG(initial_stat.st_mode):
|
|
856
|
-
raise RuntimeError(
|
|
893
|
+
raise RuntimeError(
|
|
894
|
+
f"The untracked path is not a regular file: '{relative_path}'."
|
|
895
|
+
)
|
|
857
896
|
content_digest = sha256()
|
|
858
897
|
object_digest = (
|
|
859
898
|
hashlib.new(object_format) if object_format is not None else None
|
|
@@ -871,7 +910,7 @@ def read_regular_file_snapshot_without_following(
|
|
|
871
910
|
initial_stat, final_stat
|
|
872
911
|
):
|
|
873
912
|
raise RuntimeError(
|
|
874
|
-
f"repository file changed
|
|
913
|
+
f"The repository file changed during scope resolution: '{relative_path}'."
|
|
875
914
|
)
|
|
876
915
|
return FileSnapshot(
|
|
877
916
|
fingerprint=ContentFingerprint(
|
|
@@ -910,7 +949,8 @@ def worktree_path_snapshot(
|
|
|
910
949
|
if entry.kind == "symlink":
|
|
911
950
|
if entry.target is None:
|
|
912
951
|
raise RuntimeError(
|
|
913
|
-
|
|
952
|
+
"The repository symbolic link changed during scope resolution: "
|
|
953
|
+
f"'{relative_path}'."
|
|
914
954
|
)
|
|
915
955
|
contents = os.fsencode(entry.target)
|
|
916
956
|
return WorktreePathSnapshot(
|
|
@@ -924,7 +964,7 @@ def worktree_path_snapshot(
|
|
|
924
964
|
def git_mode_for_worktree_file(entry: PathEntry) -> str:
|
|
925
965
|
"""Map a regular filesystem mode to Git's executable-bit-only mode."""
|
|
926
966
|
if entry.kind != "file" or entry.mode is None:
|
|
927
|
-
raise RuntimeError(f"
|
|
967
|
+
raise RuntimeError(f"The worktree file entry is not valid: '{entry.path}'.")
|
|
928
968
|
return "100755" if int(entry.mode, 8) & 0o111 else "100644"
|
|
929
969
|
|
|
930
970
|
|
|
@@ -947,7 +987,8 @@ def worktree_matches_entry(
|
|
|
947
987
|
and tree_entry.mode == "120000"
|
|
948
988
|
)
|
|
949
989
|
raise RuntimeError(
|
|
950
|
-
|
|
990
|
+
"The worktree scope cannot safely compare the Git object type for "
|
|
991
|
+
f"'{tree_entry.path}'."
|
|
951
992
|
)
|
|
952
993
|
|
|
953
994
|
|
|
@@ -997,15 +1038,15 @@ def worktree_tracked_capture(
|
|
|
997
1038
|
and index_entry.kind == "git-submodule"
|
|
998
1039
|
):
|
|
999
1040
|
raise RuntimeError(
|
|
1000
|
-
"worktree scope cannot safely determine submodule state for "
|
|
1001
|
-
f"{path}
|
|
1041
|
+
"The worktree scope cannot safely determine the submodule state for "
|
|
1042
|
+
f"'{path}'. Use '--staged' or '--range'."
|
|
1002
1043
|
)
|
|
1003
1044
|
index_differs_from_head = index_entry != head_entry
|
|
1004
1045
|
if path in metadata.skip_worktree_paths and snapshot.entry.kind == "absent":
|
|
1005
1046
|
if index_differs_from_head:
|
|
1006
1047
|
raise RuntimeError(
|
|
1007
|
-
"worktree scope cannot safely inspect staged change in "
|
|
1008
|
-
f"skip-worktree path {path}
|
|
1048
|
+
"The worktree scope cannot safely inspect the staged change in "
|
|
1049
|
+
f"skip-worktree path '{path}'. Use '--staged'."
|
|
1009
1050
|
)
|
|
1010
1051
|
continue
|
|
1011
1052
|
if (
|
|
@@ -1014,8 +1055,8 @@ def worktree_tracked_capture(
|
|
|
1014
1055
|
and not worktree_matches_entry(snapshot, index_entry)
|
|
1015
1056
|
):
|
|
1016
1057
|
raise RuntimeError(
|
|
1017
|
-
"worktree scope cannot safely inspect staged change whose live "
|
|
1018
|
-
f"bytes differ from the index for {path}
|
|
1058
|
+
"The worktree scope cannot safely inspect a staged change whose live "
|
|
1059
|
+
f"bytes differ from the index for '{path}'. Use '--staged'."
|
|
1019
1060
|
)
|
|
1020
1061
|
if worktree_matches_entry(snapshot, head_entry):
|
|
1021
1062
|
continue
|
|
@@ -1039,14 +1080,17 @@ def untracked_content(
|
|
|
1039
1080
|
if entry.kind == "symlink":
|
|
1040
1081
|
if entry.target is None:
|
|
1041
1082
|
raise RuntimeError(
|
|
1042
|
-
|
|
1083
|
+
"The untracked symbolic link changed during scope resolution: "
|
|
1084
|
+
f"'{relative_path}'."
|
|
1043
1085
|
)
|
|
1044
1086
|
return b"symlink", content_fingerprint((os.fsencode(entry.target),))
|
|
1045
1087
|
if entry.kind == "file":
|
|
1046
1088
|
return b"file", read_regular_file_without_following(
|
|
1047
1089
|
repository_root, relative_path
|
|
1048
1090
|
)
|
|
1049
|
-
raise RuntimeError(
|
|
1091
|
+
raise RuntimeError(
|
|
1092
|
+
f"The untracked path changed during scope resolution: '{relative_path}'."
|
|
1093
|
+
)
|
|
1050
1094
|
|
|
1051
1095
|
|
|
1052
1096
|
def scope_digest(
|
|
@@ -1154,7 +1198,7 @@ def resolve_scope(
|
|
|
1154
1198
|
paths = normalized_paths(repository_root, selected_paths)
|
|
1155
1199
|
if scope == "range":
|
|
1156
1200
|
if range_value is None:
|
|
1157
|
-
raise RuntimeError("range scope requires BASE..HEAD")
|
|
1201
|
+
raise RuntimeError("The range scope requires 'BASE..HEAD'.")
|
|
1158
1202
|
base, head = range_revisions(range_value, repository_root)
|
|
1159
1203
|
else:
|
|
1160
1204
|
head = verified_commit("HEAD", repository_root)
|
|
@@ -1163,9 +1207,11 @@ def resolve_scope(
|
|
|
1163
1207
|
first_capture = capture_scope(scope, base, head, paths, repository_root)
|
|
1164
1208
|
second_capture = capture_scope(scope, base, head, paths, repository_root)
|
|
1165
1209
|
if first_capture != second_capture:
|
|
1166
|
-
raise RuntimeError(
|
|
1210
|
+
raise RuntimeError(
|
|
1211
|
+
"The change scope changed during resolution. Run the review again."
|
|
1212
|
+
)
|
|
1167
1213
|
if scope != "range" and verified_commit("HEAD", repository_root) != head:
|
|
1168
|
-
raise RuntimeError("HEAD changed
|
|
1214
|
+
raise RuntimeError("HEAD changed during resolution. Run the review again.")
|
|
1169
1215
|
return {
|
|
1170
1216
|
"base": base,
|
|
1171
1217
|
"content_source": (
|