@homericintelligence/athena-opencode 0.5.0 → 0.5.2
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 +33 -22
- package/skills/_plugin.json +1 -0
- package/skills/_support/docs/dependency-resolution.md +203 -0
- package/skills/_support/docs/policies/development.md +131 -0
- package/skills/_support/docs/policies/evidence-integrity.md +40 -0
- package/skills/_support/docs/principles/README.md +565 -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 +131 -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 +63 -0
- package/skills/_support/docs/review/behavior-first-testing.md +78 -0
- package/skills/_support/docs/review/common.md +400 -0
- package/skills/_support/docs/review/design-docs.md +74 -0
- package/skills/_support/docs/review/issue-planning.md +247 -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 +108 -74
- package/skills/advise/scripts/list_retrievable_skills.py +20 -8
- package/skills/advise/scripts/resolve_knowledge_checkout.py +533 -0
- package/skills/brainstorm/SKILL.md +163 -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 +144 -84
- package/skills/finalize-plan/SKILL.md +205 -139
- package/skills/git-worktrees/SKILL.md +100 -80
- package/skills/git-worktrees/scripts/prepare_worktree.py +34 -15
- package/skills/issue-review/SKILL.md +136 -79
- package/skills/learn/SKILL.md +359 -192
- package/skills/myrmidon-swarm/SKILL.md +126 -86
- package/skills/plan-issue/SKILL.md +122 -81
- package/skills/pr-review/SKILL.md +184 -116
- package/skills/pr-review/references/criteria.md +61 -37
- package/skills/pr-review/references/delivery.md +277 -101
- package/skills/pr-review/references/evidence.md +346 -149
- package/skills/pr-review/references/prevalidated.md +93 -61
- package/skills/pr-review/scripts/collect_evidence.py +312 -162
- package/skills/pr-review/scripts/deliver_go.py +701 -0
- package/skills/pr-review/scripts/diff_context.py +34 -15
- package/skills/pr-review/scripts/materialize_snapshot.py +141 -130
- package/skills/pr-review/scripts/pr_identity.py +27 -15
- package/skills/pr-review/scripts/resolve_pr.py +77 -41
- package/skills/realign/SKILL.md +516 -0
- package/skills/realign/references/aislop-integration.md +215 -0
- package/skills/realign/references/architecture-and-structure.md +271 -0
- package/skills/realign/references/control-flow-and-errors.md +344 -0
- package/skills/realign/references/tests-dependencies-and-security.md +261 -0
- package/skills/realign/scripts/resolve_assessment.py +1525 -0
- package/skills/repo-review/SKILL.md +202 -131
- package/skills/simplify/SKILL.md +174 -0
- package/skills/systematic-debugging/SKILL.md +212 -192
- package/skills/systematic-debugging/scripts/repository_evidence.py +26 -8
- package/skills/test-driven-development/SKILL.md +126 -96
- package/skills/tidy/SKILL.md +104 -59
- package/skills/tidy/scripts/run_tidy.py +54 -6
|
@@ -1,9 +1,10 @@
|
|
|
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
|
|
|
6
6
|
import hashlib
|
|
7
|
+
import importlib.util
|
|
7
8
|
import json
|
|
8
9
|
import os
|
|
9
10
|
import stat
|
|
@@ -15,17 +16,30 @@ from dataclasses import dataclass
|
|
|
15
16
|
from hashlib import sha256
|
|
16
17
|
from operator import index
|
|
17
18
|
from pathlib import Path
|
|
18
|
-
from typing import Protocol, cast
|
|
19
|
-
|
|
20
|
-
if __package__ in {None, ""}:
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
)
|
|
19
|
+
from typing import TYPE_CHECKING, Protocol, cast
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING or __package__ not in {None, ""}:
|
|
22
|
+
from skills._cli import (
|
|
23
|
+
argument_parser,
|
|
24
|
+
git_read_arguments,
|
|
25
|
+
git_read_environment,
|
|
26
|
+
run_command,
|
|
27
|
+
)
|
|
28
|
+
else:
|
|
29
|
+
_cli_path = Path(__file__).resolve().parents[2] / "_cli.py"
|
|
30
|
+
_cli_spec = importlib.util.spec_from_file_location(
|
|
31
|
+
"athena_installed_cli", _cli_path
|
|
32
|
+
)
|
|
33
|
+
if _cli_spec is None or _cli_spec.loader is None:
|
|
34
|
+
raise RuntimeError(
|
|
35
|
+
f"The installed Athena CLI helper is unavailable: '{_cli_path}'."
|
|
36
|
+
)
|
|
37
|
+
_cli = importlib.util.module_from_spec(_cli_spec)
|
|
38
|
+
_cli_spec.loader.exec_module(_cli)
|
|
39
|
+
argument_parser = _cli.argument_parser
|
|
40
|
+
git_read_arguments = _cli.git_read_arguments
|
|
41
|
+
git_read_environment = _cli.git_read_environment
|
|
42
|
+
run_command = _cli.run_command
|
|
29
43
|
|
|
30
44
|
READ_CHUNK_SIZE = 1024 * 1024
|
|
31
45
|
ERROR_OUTPUT_LIMIT = 16 * 1024
|
|
@@ -34,7 +48,7 @@ MAX_WORKTREE_CANDIDATES = 50_000
|
|
|
34
48
|
|
|
35
49
|
|
|
36
50
|
def git_bytes(*arguments: str, repository_root: Path | None = None) -> bytes:
|
|
37
|
-
"""Run Git and return its raw
|
|
51
|
+
"""Run Git and return its raw standard output or raise a concise error."""
|
|
38
52
|
command = git_command(arguments, repository_root)
|
|
39
53
|
result = run_command(
|
|
40
54
|
command,
|
|
@@ -47,7 +61,9 @@ def git_bytes(*arguments: str, repository_root: Path | None = None) -> bytes:
|
|
|
47
61
|
stderr = cast(bytes, result.stderr)
|
|
48
62
|
if result.returncode != 0:
|
|
49
63
|
message = stderr.decode("utf-8", errors="replace").strip()
|
|
50
|
-
raise RuntimeError(
|
|
64
|
+
raise RuntimeError(
|
|
65
|
+
message or f"The Git command failed. Command: git {' '.join(arguments)}"
|
|
66
|
+
)
|
|
51
67
|
return stdout
|
|
52
68
|
|
|
53
69
|
|
|
@@ -60,13 +76,13 @@ def git_text(*arguments: str, repository_root: Path | None = None) -> str:
|
|
|
60
76
|
|
|
61
77
|
|
|
62
78
|
def path_list(document: bytes) -> list[str]:
|
|
63
|
-
"""Return sorted Git
|
|
79
|
+
"""Return sorted Git paths from null-byte-delimited data without lossy parsing."""
|
|
64
80
|
return sorted(os.fsdecode(path) for path in document.split(b"\0") if path)
|
|
65
81
|
|
|
66
82
|
|
|
67
83
|
@dataclass(frozen=True)
|
|
68
84
|
class PathEntry:
|
|
69
|
-
"""
|
|
85
|
+
"""This record describes one worktree or immutable Git-object manifest entry."""
|
|
70
86
|
|
|
71
87
|
path: str
|
|
72
88
|
kind: str
|
|
@@ -77,7 +93,7 @@ class PathEntry:
|
|
|
77
93
|
|
|
78
94
|
@dataclass(frozen=True)
|
|
79
95
|
class ContentFingerprint:
|
|
80
|
-
"""
|
|
96
|
+
"""This record contains a bounded identity for a byte stream of any size."""
|
|
81
97
|
|
|
82
98
|
length: int
|
|
83
99
|
digest: str
|
|
@@ -85,7 +101,7 @@ class ContentFingerprint:
|
|
|
85
101
|
|
|
86
102
|
@dataclass(frozen=True)
|
|
87
103
|
class ScopeCapture:
|
|
88
|
-
"""
|
|
104
|
+
"""This record contains one scope observation that detects worktree races."""
|
|
89
105
|
|
|
90
106
|
paths: tuple[str, ...]
|
|
91
107
|
tracked_paths: tuple[str, ...]
|
|
@@ -135,12 +151,15 @@ def git_stream_fingerprint(
|
|
|
135
151
|
)
|
|
136
152
|
except FileNotFoundError as error:
|
|
137
153
|
raise RuntimeError(
|
|
138
|
-
|
|
154
|
+
"The required command is not available: "
|
|
155
|
+
f"'{error.filename or command[0]}'."
|
|
139
156
|
) from error
|
|
140
157
|
try:
|
|
141
158
|
stdout = process.stdout
|
|
142
159
|
if stdout is None:
|
|
143
|
-
raise RuntimeError(
|
|
160
|
+
raise RuntimeError(
|
|
161
|
+
"Git did not provide standard output for scope capture."
|
|
162
|
+
)
|
|
144
163
|
try:
|
|
145
164
|
fingerprint = content_fingerprint(
|
|
146
165
|
iter(lambda: stdout.read(READ_CHUNK_SIZE), b"")
|
|
@@ -160,10 +179,16 @@ def git_stream_fingerprint(
|
|
|
160
179
|
.decode("utf-8", errors="replace")
|
|
161
180
|
.strip()
|
|
162
181
|
)
|
|
163
|
-
raise RuntimeError(
|
|
182
|
+
raise RuntimeError(
|
|
183
|
+
message
|
|
184
|
+
or f"The Git command failed. Command: git {' '.join(arguments)}"
|
|
185
|
+
)
|
|
164
186
|
return fingerprint
|
|
165
187
|
except OSError as error:
|
|
166
|
-
raise RuntimeError(
|
|
188
|
+
raise RuntimeError(
|
|
189
|
+
"The tool cannot stream Git output. "
|
|
190
|
+
f"The operation returned this diagnostic.\n{error}"
|
|
191
|
+
) from error
|
|
167
192
|
|
|
168
193
|
|
|
169
194
|
def consume_git_nul_records(
|
|
@@ -171,7 +196,7 @@ def consume_git_nul_records(
|
|
|
171
196
|
repository_root: Path,
|
|
172
197
|
consume: Callable[[bytes], None],
|
|
173
198
|
) -> None:
|
|
174
|
-
"""Pass Git
|
|
199
|
+
"""Pass null-byte-delimited Git records without buffering command output."""
|
|
175
200
|
command = git_command(arguments, repository_root)
|
|
176
201
|
try:
|
|
177
202
|
with tempfile.TemporaryFile() as error_output:
|
|
@@ -184,13 +209,16 @@ def consume_git_nul_records(
|
|
|
184
209
|
)
|
|
185
210
|
except FileNotFoundError as error:
|
|
186
211
|
raise RuntimeError(
|
|
187
|
-
|
|
212
|
+
"The required command is not available: "
|
|
213
|
+
f"'{error.filename or command[0]}'."
|
|
188
214
|
) from error
|
|
189
215
|
stdout = process.stdout
|
|
190
216
|
if stdout is None:
|
|
191
217
|
process.kill()
|
|
192
218
|
process.wait()
|
|
193
|
-
raise RuntimeError(
|
|
219
|
+
raise RuntimeError(
|
|
220
|
+
"Git did not provide standard output for scope capture."
|
|
221
|
+
)
|
|
194
222
|
pending = b""
|
|
195
223
|
try:
|
|
196
224
|
while chunk := stdout.read(READ_CHUNK_SIZE):
|
|
@@ -199,17 +227,18 @@ def consume_git_nul_records(
|
|
|
199
227
|
pending = records.pop()
|
|
200
228
|
if len(pending) > MAX_METADATA_RECORD_BYTES:
|
|
201
229
|
raise RuntimeError(
|
|
202
|
-
"Git metadata record
|
|
230
|
+
"The Git metadata record is larger than the safe scope limit."
|
|
203
231
|
)
|
|
204
232
|
for record in records:
|
|
205
233
|
if record:
|
|
206
234
|
if len(record) > MAX_METADATA_RECORD_BYTES:
|
|
207
235
|
raise RuntimeError(
|
|
208
|
-
"Git metadata record
|
|
236
|
+
"The Git metadata record is larger than the safe scope "
|
|
237
|
+
"limit."
|
|
209
238
|
)
|
|
210
239
|
consume(record)
|
|
211
240
|
if pending:
|
|
212
|
-
raise RuntimeError("
|
|
241
|
+
raise RuntimeError("The Git metadata record is not terminated.")
|
|
213
242
|
return_code = process.wait()
|
|
214
243
|
except BaseException:
|
|
215
244
|
stdout.close()
|
|
@@ -225,9 +254,15 @@ def consume_git_nul_records(
|
|
|
225
254
|
.decode("utf-8", errors="replace")
|
|
226
255
|
.strip()
|
|
227
256
|
)
|
|
228
|
-
raise RuntimeError(
|
|
257
|
+
raise RuntimeError(
|
|
258
|
+
message
|
|
259
|
+
or f"The Git command failed. Command: git {' '.join(arguments)}"
|
|
260
|
+
)
|
|
229
261
|
except OSError as error:
|
|
230
|
-
raise RuntimeError(
|
|
262
|
+
raise RuntimeError(
|
|
263
|
+
"The tool cannot stream Git metadata. "
|
|
264
|
+
f"The operation returned this diagnostic.\n{error}"
|
|
265
|
+
) from error
|
|
231
266
|
|
|
232
267
|
|
|
233
268
|
def pathspec_arguments(arguments: list[str], paths: Sequence[str]) -> list[str]:
|
|
@@ -237,7 +272,7 @@ def pathspec_arguments(arguments: list[str], paths: Sequence[str]) -> list[str]:
|
|
|
237
272
|
|
|
238
273
|
|
|
239
274
|
def normalized_paths(repository_root: Path, paths: Sequence[str]) -> list[str]:
|
|
240
|
-
"""Keep lexical filters inside the repository without following
|
|
275
|
+
"""Keep lexical filters inside the repository without following symbolic links."""
|
|
241
276
|
root = Path(os.path.abspath(os.fspath(repository_root)))
|
|
242
277
|
normalized: list[str] = []
|
|
243
278
|
for raw_path in paths:
|
|
@@ -249,15 +284,17 @@ def normalized_paths(repository_root: Path, paths: Sequence[str]) -> list[str]:
|
|
|
249
284
|
try:
|
|
250
285
|
relative = resolved.relative_to(root)
|
|
251
286
|
except ValueError as error:
|
|
252
|
-
raise RuntimeError(
|
|
287
|
+
raise RuntimeError(
|
|
288
|
+
f"The path is outside the repository: {raw_path!r}."
|
|
289
|
+
) from error
|
|
253
290
|
normalized.append(relative.as_posix())
|
|
254
291
|
return sorted(set(normalized))
|
|
255
292
|
|
|
256
293
|
|
|
257
294
|
def verified_commit(reference: str, repository_root: Path | None = None) -> str:
|
|
258
|
-
"""Resolve one non-option Git reference to an immutable commit
|
|
295
|
+
"""Resolve one non-option Git reference to an immutable commit object identifier."""
|
|
259
296
|
if not reference or reference.startswith("-"):
|
|
260
|
-
raise RuntimeError(f"
|
|
297
|
+
raise RuntimeError(f"The Git reference is not valid: {reference!r}.")
|
|
261
298
|
return git_text(
|
|
262
299
|
"rev-parse",
|
|
263
300
|
"--verify",
|
|
@@ -267,9 +304,9 @@ def verified_commit(reference: str, repository_root: Path | None = None) -> str:
|
|
|
267
304
|
|
|
268
305
|
|
|
269
306
|
def range_revisions(value: str, repository_root: Path) -> tuple[str, str]:
|
|
270
|
-
"""Resolve the required BASE..HEAD notation to immutable commit
|
|
307
|
+
"""Resolve the required BASE..HEAD notation to immutable commit object identifiers."""
|
|
271
308
|
if value.count("..") != 1:
|
|
272
|
-
raise RuntimeError("range must
|
|
309
|
+
raise RuntimeError("The range must contain exactly one BASE..HEAD separator.")
|
|
273
310
|
base_reference, head_reference = value.split("..", maxsplit=1)
|
|
274
311
|
return (
|
|
275
312
|
verified_commit(base_reference, repository_root),
|
|
@@ -368,8 +405,8 @@ def untracked_paths(paths: Sequence[str], repository_root: Path) -> list[str]:
|
|
|
368
405
|
def consume(record: bytes) -> None:
|
|
369
406
|
if len(selected) >= MAX_WORKTREE_CANDIDATES:
|
|
370
407
|
raise RuntimeError(
|
|
371
|
-
"untracked path limit "
|
|
372
|
-
|
|
408
|
+
f"The untracked path limit ({MAX_WORKTREE_CANDIDATES}) was reached. "
|
|
409
|
+
"Run the command again with narrower PATH arguments."
|
|
373
410
|
)
|
|
374
411
|
selected.append(os.fsdecode(record))
|
|
375
412
|
|
|
@@ -382,7 +419,7 @@ def untracked_paths(paths: Sequence[str], repository_root: Path) -> list[str]:
|
|
|
382
419
|
|
|
383
420
|
|
|
384
421
|
class Digest(Protocol):
|
|
385
|
-
"""
|
|
422
|
+
"""This protocol defines the minimum hashlib operations for the scope digest."""
|
|
386
423
|
|
|
387
424
|
def update(self, data: bytes) -> None:
|
|
388
425
|
"""Add bytes to the digest state."""
|
|
@@ -405,7 +442,7 @@ def path_components(relative_path: str) -> tuple[str, ...]:
|
|
|
405
442
|
"""Return a verified repository-relative path split into lexical components."""
|
|
406
443
|
components = Path(relative_path).parts
|
|
407
444
|
if not components or any(component in {".", ".."} for component in components):
|
|
408
|
-
raise RuntimeError(f"
|
|
445
|
+
raise RuntimeError(f"The repository path is not valid: {relative_path!r}.")
|
|
409
446
|
return components
|
|
410
447
|
|
|
411
448
|
|
|
@@ -421,10 +458,10 @@ def close_descriptor_quietly(descriptor: int) -> None:
|
|
|
421
458
|
def nofollow_parent_descriptor(
|
|
422
459
|
repository_root: Path, relative_path: str
|
|
423
460
|
) -> tuple[int, str]:
|
|
424
|
-
"""Open a path's parent without following any repository
|
|
461
|
+
"""Open a path's parent without following any repository symbolic link."""
|
|
425
462
|
if not hasattr(os, "O_NOFOLLOW") or not hasattr(os, "O_DIRECTORY"):
|
|
426
463
|
raise RuntimeError(
|
|
427
|
-
"host cannot inspect repository paths without following links"
|
|
464
|
+
"The host cannot inspect repository paths without following symbolic links."
|
|
428
465
|
)
|
|
429
466
|
components = path_components(relative_path)
|
|
430
467
|
descriptor: int | None = os.open(
|
|
@@ -442,13 +479,14 @@ def nofollow_parent_descriptor(
|
|
|
442
479
|
)
|
|
443
480
|
except (NotImplementedError, TypeError) as error:
|
|
444
481
|
raise RuntimeError(
|
|
445
|
-
"host cannot inspect repository paths without following
|
|
482
|
+
"The host cannot inspect repository paths without following symbolic "
|
|
483
|
+
"links."
|
|
446
484
|
) from error
|
|
447
485
|
try:
|
|
448
486
|
os.close(descriptor)
|
|
449
487
|
except OSError:
|
|
450
|
-
# `close()` leaves descriptor state unspecified
|
|
451
|
-
#
|
|
488
|
+
# After an error, `close()` leaves the descriptor state unspecified.
|
|
489
|
+
# Do not try the parent descriptor again. Always close the opened child.
|
|
452
490
|
descriptor = None
|
|
453
491
|
close_descriptor_quietly(child_descriptor)
|
|
454
492
|
raise
|
|
@@ -462,7 +500,7 @@ def nofollow_parent_descriptor(
|
|
|
462
500
|
|
|
463
501
|
|
|
464
502
|
def worktree_path_entry(repository_root: Path, relative_path: str) -> PathEntry:
|
|
465
|
-
"""Describe a path without following repository or target
|
|
503
|
+
"""Describe a path without following repository or target symbolic links."""
|
|
466
504
|
try:
|
|
467
505
|
parent_descriptor, filename = nofollow_parent_descriptor(
|
|
468
506
|
repository_root, relative_path
|
|
@@ -474,7 +512,7 @@ def worktree_path_entry(repository_root: Path, relative_path: str) -> PathEntry:
|
|
|
474
512
|
mode = os.lstat(filename, dir_fd=parent_descriptor).st_mode
|
|
475
513
|
except (NotImplementedError, TypeError) as error:
|
|
476
514
|
raise RuntimeError(
|
|
477
|
-
"host cannot inspect repository paths without following links"
|
|
515
|
+
"The host cannot inspect repository paths without following symbolic links."
|
|
478
516
|
) from error
|
|
479
517
|
except FileNotFoundError:
|
|
480
518
|
return PathEntry(relative_path, "absent")
|
|
@@ -483,7 +521,8 @@ def worktree_path_entry(repository_root: Path, relative_path: str) -> PathEntry:
|
|
|
483
521
|
target = os.readlink(filename, dir_fd=parent_descriptor)
|
|
484
522
|
except (NotImplementedError, TypeError) as error:
|
|
485
523
|
raise RuntimeError(
|
|
486
|
-
"host cannot inspect repository links without following
|
|
524
|
+
"The host cannot inspect repository symbolic links without following "
|
|
525
|
+
"them."
|
|
487
526
|
) from error
|
|
488
527
|
return PathEntry(
|
|
489
528
|
relative_path,
|
|
@@ -504,7 +543,7 @@ def worktree_path_entry(repository_root: Path, relative_path: str) -> PathEntry:
|
|
|
504
543
|
def worktree_path_entries(
|
|
505
544
|
repository_root: Path, paths: Sequence[str]
|
|
506
545
|
) -> tuple[PathEntry, ...]:
|
|
507
|
-
"""Describe
|
|
546
|
+
"""Describe each selected repository object without following symbolic links."""
|
|
508
547
|
return tuple(worktree_path_entry(repository_root, path) for path in paths)
|
|
509
548
|
|
|
510
549
|
|
|
@@ -520,7 +559,7 @@ def git_object_kind(mode: str, object_type: str) -> str:
|
|
|
520
559
|
|
|
521
560
|
|
|
522
561
|
def nul_records(document: bytes) -> list[bytes]:
|
|
523
|
-
"""Split
|
|
562
|
+
"""Split null-byte-delimited Git records and preserve the path bytes."""
|
|
524
563
|
return [record for record in document.split(b"\0") if record]
|
|
525
564
|
|
|
526
565
|
|
|
@@ -540,12 +579,14 @@ def index_entry_map(
|
|
|
540
579
|
raw_mode, raw_object_id, raw_stage = header.split()
|
|
541
580
|
except ValueError as error:
|
|
542
581
|
raise RuntimeError(
|
|
543
|
-
"
|
|
582
|
+
"The Git index entry is not valid for scope resolution."
|
|
544
583
|
) from error
|
|
545
584
|
path = os.fsdecode(raw_path)
|
|
546
585
|
stage = raw_stage.decode("ascii")
|
|
547
586
|
if stage != "0":
|
|
548
|
-
raise RuntimeError(
|
|
587
|
+
raise RuntimeError(
|
|
588
|
+
f"The selected scope contains an unmerged index entry: '{path}'."
|
|
589
|
+
)
|
|
549
590
|
mode = raw_mode.decode("ascii")
|
|
550
591
|
object_id = raw_object_id.decode("ascii")
|
|
551
592
|
entries[path] = PathEntry(
|
|
@@ -583,7 +624,7 @@ def head_tree_entry_map(
|
|
|
583
624
|
raw_mode, raw_type, raw_object_id = header.split()
|
|
584
625
|
except ValueError as error:
|
|
585
626
|
raise RuntimeError(
|
|
586
|
-
"
|
|
627
|
+
"The Git tree entry is not valid for scope resolution."
|
|
587
628
|
) from error
|
|
588
629
|
path = os.fsdecode(raw_path)
|
|
589
630
|
mode = raw_mode.decode("ascii")
|
|
@@ -609,7 +650,7 @@ def head_tree_path_entries(
|
|
|
609
650
|
|
|
610
651
|
@dataclass(frozen=True)
|
|
611
652
|
class WorktreeMetadata:
|
|
612
|
-
"""
|
|
653
|
+
"""This record contains bounded metadata for comparison of worktree candidates."""
|
|
613
654
|
|
|
614
655
|
head_entries: dict[str, PathEntry]
|
|
615
656
|
index_entries: dict[str, PathEntry]
|
|
@@ -623,8 +664,8 @@ def add_worktree_candidate(candidates: set[str], path: str) -> None:
|
|
|
623
664
|
return
|
|
624
665
|
if len(candidates) >= MAX_WORKTREE_CANDIDATES:
|
|
625
666
|
raise RuntimeError(
|
|
626
|
-
"worktree candidate limit "
|
|
627
|
-
|
|
667
|
+
f"The worktree candidate limit ({MAX_WORKTREE_CANDIDATES}) was reached. "
|
|
668
|
+
"Run the command again with narrower PATH arguments."
|
|
628
669
|
)
|
|
629
670
|
candidates.add(path)
|
|
630
671
|
|
|
@@ -635,7 +676,9 @@ def parse_head_tree_record(record: bytes) -> PathEntry:
|
|
|
635
676
|
header, raw_path = record.split(b"\t", maxsplit=1)
|
|
636
677
|
raw_mode, raw_type, raw_object_id = header.split()
|
|
637
678
|
except ValueError as error:
|
|
638
|
-
raise RuntimeError(
|
|
679
|
+
raise RuntimeError(
|
|
680
|
+
"The Git tree entry is not valid for scope resolution."
|
|
681
|
+
) from error
|
|
639
682
|
path = os.fsdecode(raw_path)
|
|
640
683
|
mode = raw_mode.decode("ascii")
|
|
641
684
|
object_type = raw_type.decode("ascii")
|
|
@@ -654,11 +697,15 @@ def parse_tagged_index_record(record: bytes) -> tuple[PathEntry, bool]:
|
|
|
654
697
|
header, raw_path = raw_entry.split(b"\t", maxsplit=1)
|
|
655
698
|
raw_mode, raw_object_id, raw_stage = header.split()
|
|
656
699
|
except ValueError as error:
|
|
657
|
-
raise RuntimeError(
|
|
700
|
+
raise RuntimeError(
|
|
701
|
+
"The Git index entry is not valid for scope resolution."
|
|
702
|
+
) from error
|
|
658
703
|
path = os.fsdecode(raw_path)
|
|
659
704
|
stage = raw_stage.decode("ascii")
|
|
660
705
|
if stage != "0":
|
|
661
|
-
raise RuntimeError(
|
|
706
|
+
raise RuntimeError(
|
|
707
|
+
f"The selected scope contains an unmerged index entry: '{path}'."
|
|
708
|
+
)
|
|
662
709
|
mode = raw_mode.decode("ascii")
|
|
663
710
|
return (
|
|
664
711
|
PathEntry(
|
|
@@ -674,7 +721,7 @@ def parse_tagged_index_record(record: bytes) -> tuple[PathEntry, bool]:
|
|
|
674
721
|
def worktree_metadata(
|
|
675
722
|
head: str, paths: Sequence[str], repository_root: Path
|
|
676
723
|
) -> WorktreeMetadata:
|
|
677
|
-
"""Stream bounded HEAD
|
|
724
|
+
"""Stream bounded HEAD and index metadata without reading worktree bytes."""
|
|
678
725
|
candidates: set[str] = set()
|
|
679
726
|
head_entries: dict[str, PathEntry] = {}
|
|
680
727
|
index_entries: dict[str, PathEntry] = {}
|
|
@@ -697,7 +744,8 @@ def worktree_metadata(
|
|
|
697
744
|
path = os.fsdecode(record)
|
|
698
745
|
if path not in candidates:
|
|
699
746
|
raise RuntimeError(
|
|
700
|
-
|
|
747
|
+
"The worktree scope metadata does not contain this staged change "
|
|
748
|
+
f"path: '{path}'."
|
|
701
749
|
)
|
|
702
750
|
staged_change_paths.add(path)
|
|
703
751
|
|
|
@@ -747,7 +795,7 @@ def worktree_metadata(
|
|
|
747
795
|
|
|
748
796
|
@dataclass(frozen=True)
|
|
749
797
|
class FileSnapshot:
|
|
750
|
-
"""
|
|
798
|
+
"""This record contains a regular-file identity and optional Git blob object identifier."""
|
|
751
799
|
|
|
752
800
|
fingerprint: ContentFingerprint
|
|
753
801
|
object_id: str | None
|
|
@@ -756,7 +804,7 @@ class FileSnapshot:
|
|
|
756
804
|
|
|
757
805
|
@dataclass(frozen=True)
|
|
758
806
|
class WorktreePathSnapshot:
|
|
759
|
-
"""
|
|
807
|
+
"""This record contains worktree metadata and the applicable raw content identity."""
|
|
760
808
|
|
|
761
809
|
entry: PathEntry
|
|
762
810
|
content: ContentFingerprint | None = None
|
|
@@ -765,7 +813,7 @@ class WorktreePathSnapshot:
|
|
|
765
813
|
|
|
766
814
|
@dataclass(frozen=True)
|
|
767
815
|
class WorktreeTrackedCapture:
|
|
768
|
-
"""
|
|
816
|
+
"""This record contains all worktree changes relative to HEAD within a size limit."""
|
|
769
817
|
|
|
770
818
|
paths: tuple[str, ...]
|
|
771
819
|
fingerprint: ContentFingerprint
|
|
@@ -780,7 +828,8 @@ def git_object_format(repository_root: Path) -> str:
|
|
|
780
828
|
hashlib.new(object_format)
|
|
781
829
|
except ValueError as error:
|
|
782
830
|
raise RuntimeError(
|
|
783
|
-
|
|
831
|
+
"The worktree review does not support this Git object format: "
|
|
832
|
+
f"'{object_format}'."
|
|
784
833
|
) from error
|
|
785
834
|
return object_format
|
|
786
835
|
|
|
@@ -817,7 +866,7 @@ def read_regular_file_snapshot_without_following(
|
|
|
817
866
|
relative_path: str,
|
|
818
867
|
object_format: str | None = None,
|
|
819
868
|
) -> FileSnapshot:
|
|
820
|
-
"""Fingerprint a regular file without following links or blocking on a
|
|
869
|
+
"""Fingerprint a regular file without following symbolic links or blocking on a named pipe."""
|
|
821
870
|
nonblocking_value = getattr(os, "O_NONBLOCK", None)
|
|
822
871
|
try:
|
|
823
872
|
if nonblocking_value is None:
|
|
@@ -825,7 +874,8 @@ def read_regular_file_snapshot_without_following(
|
|
|
825
874
|
nonblocking_flag = index(nonblocking_value)
|
|
826
875
|
except TypeError as error:
|
|
827
876
|
raise RuntimeError(
|
|
828
|
-
"host cannot inspect repository files without
|
|
877
|
+
"The host cannot inspect repository files without support for a "
|
|
878
|
+
"nonblocking open operation."
|
|
829
879
|
) from error
|
|
830
880
|
parent_descriptor, filename = nofollow_parent_descriptor(
|
|
831
881
|
repository_root, relative_path
|
|
@@ -840,7 +890,8 @@ def read_regular_file_snapshot_without_following(
|
|
|
840
890
|
)
|
|
841
891
|
except (NotImplementedError, TypeError) as error:
|
|
842
892
|
raise RuntimeError(
|
|
843
|
-
"host cannot inspect repository paths without following
|
|
893
|
+
"The host cannot inspect repository paths without following symbolic "
|
|
894
|
+
"links."
|
|
844
895
|
) from error
|
|
845
896
|
finally:
|
|
846
897
|
try:
|
|
@@ -853,7 +904,9 @@ def read_regular_file_snapshot_without_following(
|
|
|
853
904
|
try:
|
|
854
905
|
initial_stat = os.fstat(descriptor)
|
|
855
906
|
if not stat.S_ISREG(initial_stat.st_mode):
|
|
856
|
-
raise RuntimeError(
|
|
907
|
+
raise RuntimeError(
|
|
908
|
+
f"The untracked path is not a regular file: '{relative_path}'."
|
|
909
|
+
)
|
|
857
910
|
content_digest = sha256()
|
|
858
911
|
object_digest = (
|
|
859
912
|
hashlib.new(object_format) if object_format is not None else None
|
|
@@ -871,7 +924,7 @@ def read_regular_file_snapshot_without_following(
|
|
|
871
924
|
initial_stat, final_stat
|
|
872
925
|
):
|
|
873
926
|
raise RuntimeError(
|
|
874
|
-
f"repository file changed
|
|
927
|
+
f"The repository file changed during scope resolution: '{relative_path}'."
|
|
875
928
|
)
|
|
876
929
|
return FileSnapshot(
|
|
877
930
|
fingerprint=ContentFingerprint(
|
|
@@ -910,7 +963,8 @@ def worktree_path_snapshot(
|
|
|
910
963
|
if entry.kind == "symlink":
|
|
911
964
|
if entry.target is None:
|
|
912
965
|
raise RuntimeError(
|
|
913
|
-
|
|
966
|
+
"The repository symbolic link changed during scope resolution: "
|
|
967
|
+
f"'{relative_path}'."
|
|
914
968
|
)
|
|
915
969
|
contents = os.fsencode(entry.target)
|
|
916
970
|
return WorktreePathSnapshot(
|
|
@@ -924,7 +978,7 @@ def worktree_path_snapshot(
|
|
|
924
978
|
def git_mode_for_worktree_file(entry: PathEntry) -> str:
|
|
925
979
|
"""Map a regular filesystem mode to Git's executable-bit-only mode."""
|
|
926
980
|
if entry.kind != "file" or entry.mode is None:
|
|
927
|
-
raise RuntimeError(f"
|
|
981
|
+
raise RuntimeError(f"The worktree file entry is not valid: '{entry.path}'.")
|
|
928
982
|
return "100755" if int(entry.mode, 8) & 0o111 else "100644"
|
|
929
983
|
|
|
930
984
|
|
|
@@ -947,7 +1001,8 @@ def worktree_matches_entry(
|
|
|
947
1001
|
and tree_entry.mode == "120000"
|
|
948
1002
|
)
|
|
949
1003
|
raise RuntimeError(
|
|
950
|
-
|
|
1004
|
+
"The worktree scope cannot safely compare the Git object type for "
|
|
1005
|
+
f"'{tree_entry.path}'."
|
|
951
1006
|
)
|
|
952
1007
|
|
|
953
1008
|
|
|
@@ -997,15 +1052,15 @@ def worktree_tracked_capture(
|
|
|
997
1052
|
and index_entry.kind == "git-submodule"
|
|
998
1053
|
):
|
|
999
1054
|
raise RuntimeError(
|
|
1000
|
-
"worktree scope cannot safely determine submodule state for "
|
|
1001
|
-
f"{path}
|
|
1055
|
+
"The worktree scope cannot safely determine the submodule state for "
|
|
1056
|
+
f"'{path}'. Use '--staged' or '--range'."
|
|
1002
1057
|
)
|
|
1003
1058
|
index_differs_from_head = index_entry != head_entry
|
|
1004
1059
|
if path in metadata.skip_worktree_paths and snapshot.entry.kind == "absent":
|
|
1005
1060
|
if index_differs_from_head:
|
|
1006
1061
|
raise RuntimeError(
|
|
1007
|
-
"worktree scope cannot safely inspect staged change in "
|
|
1008
|
-
f"skip-worktree path {path}
|
|
1062
|
+
"The worktree scope cannot safely inspect the staged change in "
|
|
1063
|
+
f"skip-worktree path '{path}'. Use '--staged'."
|
|
1009
1064
|
)
|
|
1010
1065
|
continue
|
|
1011
1066
|
if (
|
|
@@ -1014,8 +1069,8 @@ def worktree_tracked_capture(
|
|
|
1014
1069
|
and not worktree_matches_entry(snapshot, index_entry)
|
|
1015
1070
|
):
|
|
1016
1071
|
raise RuntimeError(
|
|
1017
|
-
"worktree scope cannot safely inspect staged change whose live "
|
|
1018
|
-
f"bytes differ from the index for {path}
|
|
1072
|
+
"The worktree scope cannot safely inspect a staged change whose live "
|
|
1073
|
+
f"bytes differ from the index for '{path}'. Use '--staged'."
|
|
1019
1074
|
)
|
|
1020
1075
|
if worktree_matches_entry(snapshot, head_entry):
|
|
1021
1076
|
continue
|
|
@@ -1039,14 +1094,17 @@ def untracked_content(
|
|
|
1039
1094
|
if entry.kind == "symlink":
|
|
1040
1095
|
if entry.target is None:
|
|
1041
1096
|
raise RuntimeError(
|
|
1042
|
-
|
|
1097
|
+
"The untracked symbolic link changed during scope resolution: "
|
|
1098
|
+
f"'{relative_path}'."
|
|
1043
1099
|
)
|
|
1044
1100
|
return b"symlink", content_fingerprint((os.fsencode(entry.target),))
|
|
1045
1101
|
if entry.kind == "file":
|
|
1046
1102
|
return b"file", read_regular_file_without_following(
|
|
1047
1103
|
repository_root, relative_path
|
|
1048
1104
|
)
|
|
1049
|
-
raise RuntimeError(
|
|
1105
|
+
raise RuntimeError(
|
|
1106
|
+
f"The untracked path changed during scope resolution: '{relative_path}'."
|
|
1107
|
+
)
|
|
1050
1108
|
|
|
1051
1109
|
|
|
1052
1110
|
def scope_digest(
|
|
@@ -1154,7 +1212,7 @@ def resolve_scope(
|
|
|
1154
1212
|
paths = normalized_paths(repository_root, selected_paths)
|
|
1155
1213
|
if scope == "range":
|
|
1156
1214
|
if range_value is None:
|
|
1157
|
-
raise RuntimeError("range scope requires BASE..HEAD")
|
|
1215
|
+
raise RuntimeError("The range scope requires 'BASE..HEAD'.")
|
|
1158
1216
|
base, head = range_revisions(range_value, repository_root)
|
|
1159
1217
|
else:
|
|
1160
1218
|
head = verified_commit("HEAD", repository_root)
|
|
@@ -1163,9 +1221,11 @@ def resolve_scope(
|
|
|
1163
1221
|
first_capture = capture_scope(scope, base, head, paths, repository_root)
|
|
1164
1222
|
second_capture = capture_scope(scope, base, head, paths, repository_root)
|
|
1165
1223
|
if first_capture != second_capture:
|
|
1166
|
-
raise RuntimeError(
|
|
1224
|
+
raise RuntimeError(
|
|
1225
|
+
"The change scope changed during resolution. Run the review again."
|
|
1226
|
+
)
|
|
1167
1227
|
if scope != "range" and verified_commit("HEAD", repository_root) != head:
|
|
1168
|
-
raise RuntimeError("HEAD changed
|
|
1228
|
+
raise RuntimeError("HEAD changed during resolution. Run the review again.")
|
|
1169
1229
|
return {
|
|
1170
1230
|
"base": base,
|
|
1171
1231
|
"content_source": (
|