claude-dev-env 2.7.1 → 2.9.0
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/CLAUDE.md +7 -1
- package/_shared/CLAUDE.md +1 -0
- package/_shared/advisor/advisor-protocol.md +19 -9
- package/_shared/pr-loop/audit-contract.md +4 -4
- package/_shared/pr-loop/precatch-rubric.md +2 -2
- package/_shared/process-tree/CLAUDE.md +41 -0
- package/_shared/process-tree/scripts/config/process_tree_scripts_constants/__init__.py +1 -0
- package/_shared/process-tree/scripts/config/process_tree_scripts_constants/process_tree_kill_constants.py +27 -0
- package/_shared/process-tree/scripts/process_tree_kill.py +141 -0
- package/_shared/process-tree/scripts/pyproject.toml +16 -0
- package/_shared/process-tree/scripts/test_process_tree_kill.py +278 -0
- package/agents/clean-coder.md +9 -19
- package/agents/code-quality-agent.md +6 -5
- package/agents/deep-research.md +7 -24
- package/agents/docs-agent.md +1 -27
- package/agents/issue-tracker.md +1 -7
- package/agents/skill-writer-agent.md +1 -2
- package/agents/test_agent_frontmatter.py +335 -12
- package/docs/CODE_RULES.md +4 -2
- package/docs/references/CLAUDE.md +2 -2
- package/docs/references/advisor-tool.md +44 -6
- package/docs/references/team-advisor-skill.md +14 -8
- package/hooks/blocking/CLAUDE.md +2 -0
- package/hooks/blocking/fable_spawn_gate.py +187 -0
- package/hooks/blocking/piped_pytest_blocker.py +1223 -0
- package/hooks/blocking/plain_language_blocker.py +287 -15
- package/hooks/blocking/test_fable_spawn_gate.py +374 -0
- package/hooks/blocking/test_piped_pytest_blocker.py +587 -0
- package/hooks/blocking/test_plain_language_blocker.py +277 -2
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +34 -2
- package/hooks/git-hooks/CLAUDE.md +2 -2
- package/hooks/git-hooks/git_hooks_constants/__init__.py +28 -0
- package/hooks/git-hooks/pre_push.py +343 -54
- package/hooks/git-hooks/test_pre_push.py +852 -6
- package/hooks/hooks.json +9 -19
- package/hooks/hooks_constants/CLAUDE.md +2 -0
- package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +1 -0
- package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -0
- package/hooks/hooks_constants/fable_spawn_gate_constants.py +62 -0
- package/hooks/hooks_constants/piped_pytest_blocker_constants.py +360 -0
- package/hooks/hooks_constants/plain_language_blocker_constants.py +64 -1
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +10 -0
- package/hooks/hooks_constants/shell_command_segments.py +1 -1
- package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +1 -0
- package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +27 -0
- package/hooks/hooks_constants/test_prose_metrics_parity.py +124 -0
- package/output-styles/CLAUDE.md +17 -0
- package/output-styles/caveman-agent.md +37 -0
- package/package.json +2 -1
- package/rules/CLAUDE.md +1 -0
- package/rules/ask-user-question-required.md +26 -0
- package/rules/claims-as-quotes.md +65 -0
- package/rules/code-standards.md +33 -7
- package/rules/eli11-replies.md +1 -1
- package/scripts/CLAUDE.md +3 -3
- package/scripts/_code_review_test_support.py +6 -0
- package/scripts/check.ps1 +18 -5
- package/scripts/claude_chain_runner.py +203 -31
- package/scripts/codec_forwarding_test_support.py +2 -0
- package/scripts/dev_env_scripts_constants/CLAUDE.md +4 -4
- package/scripts/dev_env_scripts_constants/claude_chain_constants.py +38 -0
- package/scripts/dev_env_scripts_constants/code_review_constants.py +403 -2
- package/scripts/dev_env_scripts_constants/grok_run_ledger_constants.py +50 -0
- package/scripts/dev_env_scripts_constants/grok_worker_constants.py +132 -24
- package/scripts/grok_headless_runner.py +16 -83
- package/scripts/grok_patch_artifacts.py +123 -0
- package/scripts/grok_run_ledger.py +318 -0
- package/scripts/spawn_grok_batch.py +591 -10
- package/scripts/test_claude_chain_runner.py +358 -0
- package/scripts/test_grok_headless_runner.py +18 -63
- package/scripts/test_grok_patch_artifacts.py +82 -0
- package/scripts/test_grok_run_ledger.py +116 -0
- package/scripts/test_invoke_code_review.py +298 -0
- package/scripts/test_resolve_worker_spawn.py +6 -0
- package/scripts/test_spawn_grok_batch.py +396 -0
- package/scripts/tests/CLAUDE.md +1 -0
- package/scripts/tests/test_grok_worker_constants.py +59 -0
- package/skills/CLAUDE.md +4 -2
- package/skills/_shared/CLAUDE.md +37 -4
- package/skills/_shared/advisor/CLAUDE.md +9 -0
- package/skills/_shared/advisor/advisor-protocol.md +5 -0
- package/skills/_shared/advisor/scripts/README.md +9 -0
- package/skills/_shared/end-of-run-gotcha-recommendations.md +156 -0
- package/skills/_shared/pr-loop/CLAUDE.md +18 -1
- package/skills/_shared/pr-loop/audit-contract.md +5 -0
- package/skills/_shared/pr-loop/audit-reply-template.md +5 -0
- package/skills/_shared/pr-loop/code-rules-gate.md +5 -0
- package/skills/_shared/pr-loop/fix-protocol.md +5 -0
- package/skills/_shared/pr-loop/gh-payloads.md +5 -0
- package/skills/_shared/pr-loop/post-audit-thread-contract.md +5 -0
- package/skills/_shared/pr-loop/precatch-rubric.md +5 -0
- package/skills/_shared/pr-loop/scripts/CLAUDE.md +8 -1
- package/skills/_shared/pr-loop/scripts/RUNTIME_SCRIPTS.md +29 -0
- package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +46 -0
- package/skills/_shared/pr-loop/state-schema.md +5 -0
- package/skills/_shared/pr-loop/worker-spawn.md +5 -0
- package/skills/autoconverge/workflow/converge.contract.test.mjs +105 -2
- package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +34 -1
- package/skills/autoconverge/workflow/converge.mjs +66 -33
- package/skills/codex-review/scripts/codex_review_scripts_constants/codex_usage_probe_constants.py +0 -4
- package/skills/codex-review/scripts/codex_usage_probe.py +20 -33
- package/skills/codex-review/scripts/run_codex_review.py +16 -64
- package/skills/codex-review/scripts/test_codex_usage_probe.py +46 -41
- package/skills/codex-review/scripts/test_run_codex_review.py +1 -33
- package/skills/e-code-review/SKILL.md +15 -9
- package/skills/e-code-review/reference/fix.md +29 -7
- package/skills/e-code-review/reference/loop.md +230 -14
- package/skills/e-code-review/reference/low.md +33 -15
- package/skills/e-code-review/reference/medium.md +55 -21
- package/skills/e-code-review/reference/runner-selection.md +40 -0
- package/skills/e-code-review/reference/xhigh.md +30 -12
- package/skills/e-code-review/scripts/e_code_review_scripts_constants/__init__.py +1 -0
- package/skills/e-code-review/scripts/e_code_review_scripts_constants/grok_code_review_constants.py +55 -0
- package/skills/e-code-review/scripts/grok_code_review.py +221 -0
- package/skills/e-code-review/scripts/test_grok_code_review.py +212 -0
- package/skills/fresh-branch/CLAUDE.md +5 -5
- package/skills/fresh-branch/SKILL.md +14 -6
- package/skills/fresh-branch/scripts/create_fresh_branch.py +122 -39
- package/skills/fresh-branch/scripts/fresh_branch_scripts_constants/fresh_branch_cli_constants.py +10 -3
- package/skills/fresh-branch/scripts/test_create_fresh_branch.py +251 -0
- package/skills/grok-spawn/SKILL.md +10 -0
- package/skills/orchestrator/SKILL.md +9 -1
- package/skills/orchestrator-refresh/SKILL.md +5 -1
- package/skills/task-build/reference/tool-routing.md +3 -0
- package/skills/team-advisor/SKILL.md +23 -41
- package/system-prompts/software-engineer.xml +6 -3
- package/skills/test_markdown_link_integrity.py +0 -107
|
@@ -15,20 +15,31 @@ branch is blocked before the gate runs. This catches a branch that tracks
|
|
|
15
15
|
resolves to `main`. The guard runs whether or not the CODE_RULES gate is
|
|
16
16
|
installed; deletions and same-name pushes pass.
|
|
17
17
|
|
|
18
|
-
Gate base:
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
18
|
+
Gate base: a branch push takes its `--base` from the merge base between the
|
|
19
|
+
pushed object and the remote default branch, so a rebase onto the default
|
|
20
|
+
branch leaves the gate reading only the branch's own work. The stdin remote
|
|
21
|
+
object name is the base when no default-branch ref resolves and when the
|
|
22
|
+
pushed branch is the default branch. When every remote object name is zero
|
|
23
|
+
(new branch) or stdin is empty, the gate falls back to the remote's default
|
|
24
|
+
branch symbolic ref. When a default-branch ref resolves and git still reports
|
|
25
|
+
no merge base, the base is unknown, so CODE_RULES enforcement is skipped with
|
|
26
|
+
a printed reason rather than scoped to the stdin remote object name, which on
|
|
27
|
+
a rebased tip would re-flag replayed commits.
|
|
28
|
+
|
|
29
|
+
Gate scope: the gate process this hook launches diffs its `--base` against the
|
|
30
|
+
current checkout's HEAD, so the surface is the commits between that base and
|
|
31
|
+
HEAD. A push whose pushed object is HEAD reads as the branch's own work; a
|
|
32
|
+
push of any other object is still scoped to HEAD.
|
|
24
33
|
|
|
25
34
|
Exit codes:
|
|
26
35
|
0 - the push destination is allowed and its commits pass the gate (or
|
|
27
|
-
the gate is not installed
|
|
36
|
+
the gate is not installed, or CODE_RULES is skipped because a
|
|
37
|
+
default-branch ref resolved and git still reported no merge base).
|
|
28
38
|
1 - the push would land a non-protected local branch onto a protected
|
|
29
39
|
remote branch, or a commit introduces a blocking violation.
|
|
30
|
-
2 - unexpected invocation failure (e.g., subprocess could not launch),
|
|
31
|
-
no
|
|
40
|
+
2 - unexpected invocation failure (e.g., subprocess could not launch),
|
|
41
|
+
stdin carried no parseable line, or a new-branch/empty-stdin base
|
|
42
|
+
could not be turned into a usable commit name.
|
|
32
43
|
"""
|
|
33
44
|
|
|
34
45
|
from __future__ import annotations
|
|
@@ -38,9 +49,13 @@ import os
|
|
|
38
49
|
import subprocess
|
|
39
50
|
import sys
|
|
40
51
|
from pathlib import Path
|
|
52
|
+
from typing import NamedTuple
|
|
41
53
|
|
|
42
|
-
from gate_utils import is_safe_regular_file, resolve_gate_script_path
|
|
43
54
|
from git_hooks_constants import (
|
|
55
|
+
ALL_DEFAULT_BRANCH_FALLBACK_REFERENCES,
|
|
56
|
+
ALL_GIT_MERGE_BASE_COMMAND_PREFIX,
|
|
57
|
+
ALL_GIT_SYMBOLIC_REFERENCE_COMMAND_PREFIX,
|
|
58
|
+
ALL_GIT_VERIFY_REFERENCE_COMMAND_PREFIX,
|
|
44
59
|
ALL_PROTECTED_BRANCH_PUSH_NAMES,
|
|
45
60
|
ALL_ZEROS_OBJECT_NAME_CHARACTER,
|
|
46
61
|
BASE_REFERENCE_ARGUMENT,
|
|
@@ -57,6 +72,7 @@ from git_hooks_constants import (
|
|
|
57
72
|
GIT_EXECUTABLE_NAME,
|
|
58
73
|
GIT_OUTPUT_DECODE_ERRORS_POLICY,
|
|
59
74
|
GIT_OUTPUT_ENCODING_NAME,
|
|
75
|
+
GIT_REFERENCE_QUERY_TIMEOUT_SECONDS,
|
|
60
76
|
INVOKE_GATE_FAILURE_MESSAGE,
|
|
61
77
|
LOCAL_BRANCH_REFERENCE_PREFIX,
|
|
62
78
|
LOCAL_REFERENCE_FIELD_INDEX,
|
|
@@ -64,6 +80,8 @@ from git_hooks_constants import (
|
|
|
64
80
|
MALFORMED_STDIN_LINE_MESSAGE,
|
|
65
81
|
NO_PARSEABLE_STDIN_LINES_MESSAGE,
|
|
66
82
|
NO_PARSEABLE_STDIN_LINES_SENTINEL,
|
|
83
|
+
ORIGIN_HEAD_SYMBOLIC_REFERENCE,
|
|
84
|
+
ORIGIN_REMOTE_TRACKING_REFERENCE_PREFIX,
|
|
67
85
|
PRE_PUSH_GATE_SCRIPT_NOT_FOUND_MESSAGE,
|
|
68
86
|
PROTECTED_BRANCH_PUSH_BLOCK_EXIT_CODE,
|
|
69
87
|
PROTECTED_BRANCH_PUSH_BLOCK_MESSAGE,
|
|
@@ -71,7 +89,10 @@ from git_hooks_constants import (
|
|
|
71
89
|
STDIN_LINE_FIELD_COUNT,
|
|
72
90
|
STDIN_READ_FAILURE_MESSAGE,
|
|
73
91
|
STDIN_REMOTE_OBJECT_FIELD_INDEX,
|
|
92
|
+
UNRESOLVABLE_MERGE_BASE_MESSAGE,
|
|
93
|
+
UNRESOLVABLE_MERGE_BASE_SENTINEL,
|
|
74
94
|
)
|
|
95
|
+
from gate_utils import is_safe_regular_file, resolve_gate_script_path
|
|
75
96
|
from pre_push_base_reference import (
|
|
76
97
|
resolve_remote_name_from_arguments,
|
|
77
98
|
resolve_usable_base_reference,
|
|
@@ -139,71 +160,331 @@ def is_all_zeros_object_name(object_name: str) -> bool:
|
|
|
139
160
|
)
|
|
140
161
|
|
|
141
162
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
163
|
+
class PushLine(NamedTuple):
|
|
164
|
+
"""One parsed ``<local-ref> <local-sha> <remote-ref> <remote-sha>`` stdin line."""
|
|
165
|
+
|
|
166
|
+
local_branch_name: str
|
|
167
|
+
local_object_name: str
|
|
168
|
+
remote_branch_name: str
|
|
169
|
+
remote_object_name: str
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def parse_push_line(stripped_line: str) -> PushLine | None:
|
|
173
|
+
"""Return the four fields of one push line, or None when the line is malformed.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
stripped_line: One whitespace-stripped stdin line.
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
The parsed line with both ref names reduced to branch names, or None
|
|
180
|
+
when the line carries fewer than the four fields git writes.
|
|
181
|
+
"""
|
|
182
|
+
fields = stripped_line.split()
|
|
183
|
+
if len(fields) < STDIN_LINE_FIELD_COUNT:
|
|
184
|
+
return None
|
|
185
|
+
return PushLine(
|
|
186
|
+
local_branch_name=fields[LOCAL_REFERENCE_FIELD_INDEX].removeprefix(
|
|
187
|
+
LOCAL_BRANCH_REFERENCE_PREFIX
|
|
188
|
+
),
|
|
189
|
+
local_object_name=fields[LOCAL_SHA_FIELD_INDEX],
|
|
190
|
+
remote_branch_name=fields[REMOTE_REFERENCE_FIELD_INDEX].removeprefix(
|
|
191
|
+
LOCAL_BRANCH_REFERENCE_PREFIX
|
|
192
|
+
),
|
|
193
|
+
remote_object_name=fields[STDIN_REMOTE_OBJECT_FIELD_INDEX],
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def is_branch_update(push_line: PushLine) -> bool:
|
|
198
|
+
"""Report whether a push line moves a branch the remote already holds.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
push_line: One parsed push line.
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
True when the local and the remote object names are both non-zero.
|
|
205
|
+
"""
|
|
206
|
+
if is_all_zeros_object_name(push_line.local_object_name):
|
|
207
|
+
return False
|
|
208
|
+
return not is_all_zeros_object_name(push_line.remote_object_name)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
class ParsedPushStdin(NamedTuple):
|
|
212
|
+
"""Every well-formed push line from one stdin payload, and whether text arrived."""
|
|
213
|
+
|
|
214
|
+
all_push_lines: tuple[PushLine, ...]
|
|
215
|
+
has_stdin_content: bool
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _parse_push_stdin(stdin_text: str) -> ParsedPushStdin:
|
|
219
|
+
"""Parse the whole stdin payload once, warning on each malformed line.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
stdin_text: The pre-push stdin payload.
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
Every well-formed line in stdin order, paired with whether stdin
|
|
226
|
+
carried any non-blank text at all.
|
|
227
|
+
"""
|
|
228
|
+
all_push_lines: list[PushLine] = []
|
|
150
229
|
has_stdin_content = False
|
|
151
230
|
for each_line in stdin_text.splitlines():
|
|
152
231
|
stripped_line = each_line.strip()
|
|
153
232
|
if not stripped_line:
|
|
154
233
|
continue
|
|
155
234
|
has_stdin_content = True
|
|
156
|
-
|
|
157
|
-
if
|
|
235
|
+
push_line = parse_push_line(stripped_line)
|
|
236
|
+
if push_line is None:
|
|
158
237
|
print(
|
|
159
|
-
|
|
238
|
+
MALFORMED_STDIN_LINE_MESSAGE.format(line=stripped_line),
|
|
160
239
|
file=sys.stderr,
|
|
161
240
|
)
|
|
162
241
|
continue
|
|
163
|
-
|
|
164
|
-
|
|
242
|
+
all_push_lines.append(push_line)
|
|
243
|
+
return ParsedPushStdin(tuple(all_push_lines), has_stdin_content)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _resolve_base_reference_from_lines(parsed_stdin: ParsedPushStdin) -> str | None:
|
|
247
|
+
"""Return the stdin-derived base reference for already-parsed lines.
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
parsed_stdin: The parsed stdin payload.
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
The first branch update's remote object name, the no-parseable-lines
|
|
254
|
+
sentinel, None for a deletion-only push, or the remote default branch
|
|
255
|
+
reference.
|
|
256
|
+
"""
|
|
257
|
+
is_all_valid_lines_deletions = True
|
|
258
|
+
for each_push_line in parsed_stdin.all_push_lines:
|
|
259
|
+
if is_all_zeros_object_name(each_push_line.local_object_name):
|
|
165
260
|
continue
|
|
166
261
|
is_all_valid_lines_deletions = False
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
if has_stdin_content and not has_seen_any_valid_line:
|
|
262
|
+
if is_branch_update(each_push_line):
|
|
263
|
+
return each_push_line.remote_object_name
|
|
264
|
+
if parsed_stdin.has_stdin_content and not parsed_stdin.all_push_lines:
|
|
171
265
|
return NO_PARSEABLE_STDIN_LINES_SENTINEL
|
|
172
|
-
if
|
|
266
|
+
if parsed_stdin.all_push_lines and is_all_valid_lines_deletions:
|
|
173
267
|
return None
|
|
174
|
-
return
|
|
268
|
+
return DEFAULT_REMOTE_BASE_REFERENCE
|
|
175
269
|
|
|
176
270
|
|
|
177
|
-
def
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
271
|
+
def resolve_base_reference_from_stdin(stdin_text: str) -> str | None:
|
|
272
|
+
return _resolve_base_reference_from_lines(_parse_push_stdin(stdin_text))
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def run_git_reference_query(all_git_arguments: tuple[str, ...]) -> str | None:
|
|
276
|
+
"""Return the stripped stdout of a read-only git query.
|
|
277
|
+
|
|
278
|
+
Output decodes with the same replacing policy as ``run_git_text_command``,
|
|
279
|
+
so locale-invalid bytes become a marked string instead of crashing the hook.
|
|
280
|
+
|
|
281
|
+
Args:
|
|
282
|
+
all_git_arguments: The full git argv, including the ``git`` word.
|
|
283
|
+
|
|
284
|
+
Returns:
|
|
285
|
+
The stripped stdout, or None when git exits non-zero, prints nothing,
|
|
286
|
+
or cannot run.
|
|
287
|
+
"""
|
|
288
|
+
try:
|
|
289
|
+
completion = subprocess.run(
|
|
290
|
+
list(all_git_arguments),
|
|
291
|
+
capture_output=True,
|
|
292
|
+
check=False,
|
|
293
|
+
timeout=GIT_REFERENCE_QUERY_TIMEOUT_SECONDS,
|
|
195
294
|
)
|
|
196
|
-
|
|
197
|
-
|
|
295
|
+
except (OSError, subprocess.SubprocessError):
|
|
296
|
+
return None
|
|
297
|
+
if completion.returncode != 0:
|
|
298
|
+
return None
|
|
299
|
+
decoded_output = completion.stdout.decode(
|
|
300
|
+
GIT_OUTPUT_ENCODING_NAME, errors=GIT_OUTPUT_DECODE_ERRORS_POLICY
|
|
301
|
+
)
|
|
302
|
+
return decoded_output.strip() or None
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def resolve_default_branch_reference() -> str | None:
|
|
306
|
+
"""Return the remote-tracking ref name of the remote's default branch.
|
|
307
|
+
|
|
308
|
+
The remote's HEAD symbolic ref answers first. When it is unset, each
|
|
309
|
+
candidate default-branch remote-tracking ref answers in turn.
|
|
310
|
+
|
|
311
|
+
Resolution reads ``origin`` alone and never the remote name git passes as
|
|
312
|
+
argv[1], and its unset-``origin/HEAD`` fallback prefers ``origin/main``
|
|
313
|
+
over ``origin/master``, so a repo whose default is another branch beside a
|
|
314
|
+
legacy ``main`` resolves the wrong base ΓÇö the assumption set tracked at
|
|
315
|
+
~/.claude/orchestrator-runs/falsify-first/parked-items.md row 3.
|
|
316
|
+
|
|
317
|
+
Returns:
|
|
318
|
+
A remote-tracking ref name, or None when no default-branch ref
|
|
319
|
+
resolves.
|
|
320
|
+
"""
|
|
321
|
+
symbolic_reference = run_git_reference_query(
|
|
322
|
+
(*ALL_GIT_SYMBOLIC_REFERENCE_COMMAND_PREFIX, ORIGIN_HEAD_SYMBOLIC_REFERENCE)
|
|
323
|
+
)
|
|
324
|
+
if symbolic_reference is not None:
|
|
325
|
+
verified_symbolic_target = run_git_reference_query(
|
|
326
|
+
(*ALL_GIT_VERIFY_REFERENCE_COMMAND_PREFIX, symbolic_reference)
|
|
327
|
+
)
|
|
328
|
+
if verified_symbolic_target is not None:
|
|
329
|
+
return symbolic_reference
|
|
330
|
+
for each_candidate_reference in ALL_DEFAULT_BRANCH_FALLBACK_REFERENCES:
|
|
331
|
+
verified_reference = run_git_reference_query(
|
|
332
|
+
(*ALL_GIT_VERIFY_REFERENCE_COMMAND_PREFIX, each_candidate_reference)
|
|
333
|
+
)
|
|
334
|
+
if verified_reference is not None:
|
|
335
|
+
return each_candidate_reference
|
|
336
|
+
return None
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def _find_branch_update_in_lines(
|
|
340
|
+
all_push_lines: tuple[PushLine, ...],
|
|
341
|
+
) -> PushLine | None:
|
|
342
|
+
"""Return the first already-parsed line that moves an existing remote branch.
|
|
343
|
+
|
|
344
|
+
Args:
|
|
345
|
+
all_push_lines: Well-formed push lines in stdin order.
|
|
346
|
+
|
|
347
|
+
Returns:
|
|
348
|
+
The first branch-update line, or None when no line updates a branch
|
|
349
|
+
the remote already holds.
|
|
350
|
+
"""
|
|
351
|
+
for each_push_line in all_push_lines:
|
|
352
|
+
if is_branch_update(each_push_line):
|
|
353
|
+
return each_push_line
|
|
354
|
+
return None
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def find_branch_update_push(stdin_text: str) -> PushLine | None:
|
|
358
|
+
"""Return the first stdin line that moves a branch the remote already holds.
|
|
359
|
+
|
|
360
|
+
Args:
|
|
361
|
+
stdin_text: The pre-push stdin payload.
|
|
362
|
+
|
|
363
|
+
Returns:
|
|
364
|
+
The parsed branch-update line, or None when no line updates a branch
|
|
365
|
+
the remote already holds.
|
|
366
|
+
"""
|
|
367
|
+
return _find_branch_update_in_lines(_parse_push_stdin(stdin_text).all_push_lines)
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def resolve_default_branch_merge_base(
|
|
371
|
+
remote_branch_name: str, pushed_object_name: str
|
|
372
|
+
) -> str | None:
|
|
373
|
+
"""Return the merge base of the pushed object and the default branch.
|
|
374
|
+
|
|
375
|
+
The remote branch the push updates decides whether a merge base applies,
|
|
376
|
+
so ``git push origin release-candidate:develop`` reads as an update of
|
|
377
|
+
``develop`` whatever the local branch is called.
|
|
378
|
+
|
|
379
|
+
Args:
|
|
380
|
+
remote_branch_name: The remote branch the push updates.
|
|
381
|
+
pushed_object_name: The object name the push carries.
|
|
382
|
+
|
|
383
|
+
Returns:
|
|
384
|
+
The merge-base object name; None when no default-branch ref resolves or
|
|
385
|
+
when the push updates the default branch, which are the two cases where
|
|
386
|
+
no merge base applies; or the unresolvable-merge-base sentinel when a
|
|
387
|
+
default-branch ref resolves and git still reports no merge base.
|
|
388
|
+
"""
|
|
389
|
+
default_branch_reference = resolve_default_branch_reference()
|
|
390
|
+
if default_branch_reference is None:
|
|
391
|
+
return None
|
|
392
|
+
default_branch_name = default_branch_reference.removeprefix(
|
|
393
|
+
ORIGIN_REMOTE_TRACKING_REFERENCE_PREFIX
|
|
394
|
+
)
|
|
395
|
+
if remote_branch_name == default_branch_name:
|
|
396
|
+
return None
|
|
397
|
+
# Anchors to the pushed object; deferring to the gate's HEAD-based merge-base would change behavior on non-HEAD pushes.
|
|
398
|
+
merge_base_object_name = run_git_reference_query(
|
|
399
|
+
(
|
|
400
|
+
*ALL_GIT_MERGE_BASE_COMMAND_PREFIX,
|
|
401
|
+
pushed_object_name,
|
|
402
|
+
default_branch_reference,
|
|
198
403
|
)
|
|
404
|
+
)
|
|
405
|
+
if merge_base_object_name is None:
|
|
406
|
+
return UNRESOLVABLE_MERGE_BASE_SENTINEL
|
|
407
|
+
return merge_base_object_name
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def resolve_gate_base_reference(stdin_text: str) -> str | None:
|
|
411
|
+
"""Return the reference the gate scopes its changed lines to.
|
|
412
|
+
|
|
413
|
+
A branch update reads from the merge base with the remote default branch,
|
|
414
|
+
and every other push reads from the stdin remote object name::
|
|
415
|
+
|
|
416
|
+
branch update, default branch resolved -> merge-base(pushed, default)
|
|
417
|
+
branch update, no merge base found -> unresolvable-merge-base sentinel
|
|
418
|
+
branch update, no default branch ref -> stdin remote object name
|
|
419
|
+
default-branch push -> stdin remote object name
|
|
420
|
+
new branch or empty stdin -> remote default branch ref
|
|
421
|
+
deletion -> None
|
|
422
|
+
|
|
423
|
+
Args:
|
|
424
|
+
stdin_text: The pre-push stdin payload.
|
|
425
|
+
|
|
426
|
+
Returns:
|
|
427
|
+
The gate's base reference, the no-parseable-lines sentinel, the
|
|
428
|
+
unresolvable-merge-base sentinel, or None when the push only deletes
|
|
429
|
+
remote branches.
|
|
430
|
+
"""
|
|
431
|
+
return _resolve_gate_base_from_parsed(_parse_push_stdin(stdin_text))
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def _resolve_gate_base_from_parsed(parsed_stdin: ParsedPushStdin) -> str | None:
|
|
435
|
+
"""Return the gate's base reference for an already-parsed stdin payload.
|
|
436
|
+
|
|
437
|
+
Args:
|
|
438
|
+
parsed_stdin: The parsed stdin payload.
|
|
439
|
+
|
|
440
|
+
Returns:
|
|
441
|
+
The merge base with the remote default branch for a branch update, the
|
|
442
|
+
branch update's remote object name when no merge base applies, the
|
|
443
|
+
unresolvable-merge-base sentinel when a default-branch ref resolves and
|
|
444
|
+
git reports no merge base anyway, and otherwise whatever the
|
|
445
|
+
stdin-derived base reference reports.
|
|
446
|
+
"""
|
|
447
|
+
branch_update = _find_branch_update_in_lines(parsed_stdin.all_push_lines)
|
|
448
|
+
if branch_update is None:
|
|
449
|
+
return _resolve_base_reference_from_lines(parsed_stdin)
|
|
450
|
+
merge_base_object_name = resolve_default_branch_merge_base(
|
|
451
|
+
branch_update.remote_branch_name, branch_update.local_object_name
|
|
452
|
+
)
|
|
453
|
+
if merge_base_object_name is None:
|
|
454
|
+
return branch_update.remote_object_name
|
|
455
|
+
return merge_base_object_name
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def _find_protected_violation_in_lines(
|
|
459
|
+
all_push_lines: tuple[PushLine, ...],
|
|
460
|
+
) -> tuple[str, str] | None:
|
|
461
|
+
"""Return the first already-parsed line that pushes onto a protected branch.
|
|
462
|
+
|
|
463
|
+
Args:
|
|
464
|
+
all_push_lines: Well-formed push lines in stdin order.
|
|
465
|
+
|
|
466
|
+
Returns:
|
|
467
|
+
The local and protected remote branch names, or None when no line
|
|
468
|
+
lands a differently named branch on a protected one.
|
|
469
|
+
"""
|
|
470
|
+
for each_push_line in all_push_lines:
|
|
471
|
+
if is_all_zeros_object_name(each_push_line.local_object_name):
|
|
472
|
+
continue
|
|
199
473
|
if (
|
|
200
|
-
remote_branch_name in
|
|
201
|
-
and local_branch_name != remote_branch_name
|
|
474
|
+
each_push_line.remote_branch_name in ALL_PROTECTED_BRANCH_PUSH_NAMES
|
|
475
|
+
and each_push_line.local_branch_name != each_push_line.remote_branch_name
|
|
202
476
|
):
|
|
203
|
-
return (
|
|
477
|
+
return (
|
|
478
|
+
each_push_line.local_branch_name,
|
|
479
|
+
each_push_line.remote_branch_name,
|
|
480
|
+
)
|
|
204
481
|
return None
|
|
205
482
|
|
|
206
483
|
|
|
484
|
+
def find_protected_branch_push_violation(stdin_text: str) -> tuple[str, str] | None:
|
|
485
|
+
return _find_protected_violation_in_lines(_parse_push_stdin(stdin_text).all_push_lines)
|
|
486
|
+
|
|
487
|
+
|
|
207
488
|
def invoke_gate(gate_script_path: Path, base_reference: str) -> int:
|
|
208
489
|
base_reference_argument = BASE_REFERENCE_ARGUMENT
|
|
209
490
|
invoke_gate_failure_message = INVOKE_GATE_FAILURE_MESSAGE
|
|
@@ -310,6 +591,8 @@ def main() -> int:
|
|
|
310
591
|
pre_push_gate_script_not_found_message = PRE_PUSH_GATE_SCRIPT_NOT_FOUND_MESSAGE
|
|
311
592
|
no_parseable_stdin_lines_message = NO_PARSEABLE_STDIN_LINES_MESSAGE
|
|
312
593
|
no_parseable_stdin_lines_sentinel = NO_PARSEABLE_STDIN_LINES_SENTINEL
|
|
594
|
+
unresolvable_merge_base_message = UNRESOLVABLE_MERGE_BASE_MESSAGE
|
|
595
|
+
unresolvable_merge_base_sentinel = UNRESOLVABLE_MERGE_BASE_SENTINEL
|
|
313
596
|
protected_branch_push_block_message = PROTECTED_BRANCH_PUSH_BLOCK_MESSAGE
|
|
314
597
|
protected_branch_push_block_exit_code = PROTECTED_BRANCH_PUSH_BLOCK_EXIT_CODE
|
|
315
598
|
try:
|
|
@@ -320,7 +603,10 @@ def main() -> int:
|
|
|
320
603
|
file=sys.stderr,
|
|
321
604
|
)
|
|
322
605
|
return gate_infrastructure_failure_exit_code
|
|
323
|
-
|
|
606
|
+
parsed_stdin = _parse_push_stdin(stdin_text)
|
|
607
|
+
protected_branch_push_violation = _find_protected_violation_in_lines(
|
|
608
|
+
parsed_stdin.all_push_lines
|
|
609
|
+
)
|
|
324
610
|
if protected_branch_push_violation is not None:
|
|
325
611
|
local_branch_name, remote_branch_name = protected_branch_push_violation
|
|
326
612
|
print(
|
|
@@ -338,12 +624,15 @@ def main() -> int:
|
|
|
338
624
|
file=sys.stderr,
|
|
339
625
|
)
|
|
340
626
|
return code_review_stamp_block_exit_code()
|
|
341
|
-
base_reference =
|
|
627
|
+
base_reference = _resolve_gate_base_from_parsed(parsed_stdin)
|
|
342
628
|
if base_reference is None:
|
|
343
629
|
return 0
|
|
344
630
|
if base_reference == no_parseable_stdin_lines_sentinel:
|
|
345
631
|
print(no_parseable_stdin_lines_message, file=sys.stderr)
|
|
346
632
|
return gate_infrastructure_failure_exit_code
|
|
633
|
+
if base_reference == unresolvable_merge_base_sentinel:
|
|
634
|
+
print(unresolvable_merge_base_message, file=sys.stderr)
|
|
635
|
+
return code_review_stamp_block_exit_code()
|
|
347
636
|
remote_name = resolve_remote_name_from_arguments(sys.argv)
|
|
348
637
|
try:
|
|
349
638
|
usable_base_reference = resolve_usable_base_reference(
|