claude-dev-env 2.5.0 → 2.7.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/CLAUDE.md +20 -57
- package/_shared/pr-loop/scripts/code_rules_gate.py +2 -1
- package/_shared/pr-loop/scripts/code_rules_gate_parts/CLAUDE.md +12 -2
- package/_shared/pr-loop/scripts/code_rules_gate_parts/baseline_import_isolation.py +309 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_regression.py +540 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_running.py +206 -70
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/__init__.py +1 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/_repo_test_helpers.py +76 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_baseline_import_isolation.py +248 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_staged_test_regression.py +309 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_staged_test_running.py +91 -58
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +202 -0
- package/_shared/pr-loop/worker-spawn.md +3 -1
- package/agents/CLAUDE.md +1 -1
- package/agents/code-verifier.md +36 -7
- package/bin/codex-compat.mjs +104 -0
- package/bin/codex-compat.test.mjs +51 -0
- package/codex-capability-map.json +13 -0
- package/docs/CODE_RULES.md +2 -0
- package/docs/codex-compatibility.md +25 -0
- package/docs/nas-ssh-invocation.md +96 -12
- package/docs/references/code-review-enforcement.md +31 -6
- package/hooks/blocking/CLAUDE.md +3 -0
- package/hooks/blocking/config/code_review_enforcement_constants.py +40 -10
- package/hooks/blocking/config/test_code_review_enforcement_constants.py +56 -3
- package/hooks/blocking/eli11_reply_enforcer.py +479 -0
- package/hooks/blocking/gh_body_arg_blocker.py +1 -1
- package/hooks/blocking/nas_ssh_binary_enforcer.py +8 -46
- package/hooks/blocking/shell_substitution_blocker.py +129 -0
- package/hooks/blocking/state_description_blocker.py +1 -1
- package/hooks/blocking/stop_dispatcher.py +1 -1
- package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +2 -3
- package/hooks/blocking/test_eli11_reply_enforcer.py +457 -0
- package/hooks/blocking/test_shell_substitution_blocker.py +124 -0
- package/hooks/blocking/test_stop_dispatcher.py +23 -0
- package/hooks/blocking/test_unscoped_search_blocker.py +102 -0
- package/hooks/blocking/test_verdict_directory_write_blocker.py +4 -8
- package/hooks/blocking/unscoped_search_blocker.py +391 -0
- package/hooks/git-hooks/CLAUDE.md +3 -0
- package/hooks/git-hooks/conftest.py +30 -0
- package/hooks/git-hooks/gate_utils.py +2 -2
- package/hooks/git-hooks/git_hooks_constants/__init__.py +41 -2
- package/hooks/git-hooks/pre_push.py +75 -4
- package/hooks/git-hooks/pre_push_base_reference.py +166 -0
- package/hooks/git-hooks/test_config.py +0 -15
- package/hooks/git-hooks/test_gate_utils.py +3 -15
- package/hooks/git-hooks/test_pre_commit.py +1 -15
- package/hooks/git-hooks/test_pre_push.py +236 -27
- package/hooks/git-hooks/test_pre_push_base_reference.py +339 -0
- package/hooks/hooks.json +0 -12
- package/hooks/hooks_constants/CLAUDE.md +5 -1
- package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +4 -4
- package/hooks/hooks_constants/eli11_reply_enforcer_constants.py +101 -0
- package/hooks/hooks_constants/nas_ssh_binary_enforcer_constants.py +2 -8
- package/hooks/hooks_constants/shell_command_segments.py +82 -0
- package/hooks/hooks_constants/shell_substitution_blocker_constants.py +67 -0
- package/hooks/hooks_constants/stop_dispatcher_constants.py +1 -0
- package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +5 -6
- package/hooks/hooks_constants/test_stop_dispatcher_constants.py +1 -0
- package/hooks/hooks_constants/unscoped_search_blocker_constants.py +153 -0
- package/package.json +4 -2
- package/rules/CLAUDE.md +17 -23
- package/rules/agent-spawn-protocol.md +6 -6
- package/rules/anti-corollary-tests.md +1 -1
- package/rules/bdd.md +1 -1
- package/rules/cleanup-temp-files.md +10 -4
- package/rules/code-standards.md +7 -0
- package/rules/conservative-action.md +1 -5
- package/rules/context7.md +0 -4
- package/rules/destructive-commands.md +47 -0
- package/rules/doc-inventory-integrity.md +48 -0
- package/rules/doc-prose-cuts.md +58 -0
- package/rules/docstring-prose-matches-implementation.md +10 -2
- package/rules/durable-post-artifacts.md +0 -4
- package/rules/eli11-replies.md +31 -0
- package/rules/explore-thoroughly.md +4 -4
- package/rules/falsify-before-green.md +68 -0
- package/rules/file-global-constants.md +1 -1
- package/rules/filesystem-search.md +51 -0
- package/rules/gh-cli-conventions.md +27 -0
- package/rules/git-workflow.md +26 -0
- package/rules/hedging-claims.md +9 -0
- package/rules/long-horizon-autonomy.md +0 -4
- package/rules/measurement-denominators.md +48 -0
- package/rules/nas-ssh-invocation.md +23 -5
- package/rules/parallel-tools.md +2 -2
- package/rules/plain-illustrative-docstrings.md +3 -7
- package/rules/plain-language.md +2 -0
- package/rules/proof-of-work-pr-comments.md +0 -4
- package/rules/re-stage-before-commit.md +2 -0
- package/rules/research-mode.md +10 -0
- package/rules/shell-invocation.md +21 -0
- package/rules/testing.md +4 -0
- package/rules/verified-commit-gate-skip.md +3 -27
- package/rules/verify-before-asking.md +5 -0
- package/rules/windows-filesystem-safe.md +1 -1
- package/rules/workers-done-before-complete.md +4 -0
- package/scripts/CLAUDE.md +3 -3
- package/scripts/Migrate-ShellPolicy.ps1 +1 -1
- package/scripts/codex_capability_bridge.py +171 -0
- package/scripts/codex_compat_materializer.py +1087 -0
- package/scripts/codex_compat_watcher.py +502 -0
- package/scripts/dev_env_scripts_constants/CLAUDE.md +1 -1
- package/scripts/dev_env_scripts_constants/code_review_constants.py +37 -0
- package/scripts/dev_env_scripts_constants/grok_worker_constants.py +79 -13
- package/scripts/grok_headless_runner.py +213 -16
- package/scripts/invoke_code_review.py +11 -4
- package/scripts/resolve_worker_spawn.py +56 -10
- package/scripts/spawn_grok_batch.py +43 -22
- package/scripts/sync_to_cursor/rules.py +0 -10
- package/scripts/test_grok_headless_runner.py +592 -10
- package/scripts/test_invoke_code_review.py +143 -0
- package/scripts/test_invoke_code_review_chain.py +1 -1
- package/scripts/test_invoke_code_review_contract.py +1 -1
- package/scripts/test_resolve_worker_spawn.py +179 -15
- package/scripts/test_spawn_grok_batch.py +225 -22
- package/scripts/tests/test_code_review_constants.py +80 -0
- package/scripts/tests/test_codex_capability_bridge.py +91 -0
- package/scripts/tests/test_codex_compat_materializer.py +632 -0
- package/scripts/tests/test_codex_compat_watcher.py +599 -0
- package/scripts/tests/test_sync_to_cursor.py +0 -1
- package/skills/autoconverge/workflow/converge.contract.test.mjs +28 -6
- package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +73 -0
- package/skills/autoconverge/workflow/converge.mjs +64 -15
- package/skills/bugteam/reference/copilot-gap-analysis.md +1 -1
- package/skills/condensing-instructions/SKILL.md +42 -51
- package/skills/fresh-branch/CLAUDE.md +1 -1
- package/skills/fresh-branch/SKILL.md +5 -6
- package/skills/fresh-branch/scripts/create_fresh_branch.py +42 -24
- package/skills/fresh-branch/scripts/fresh_branch_scripts_constants/fresh_branch_cli_constants.py +1 -3
- package/skills/fresh-branch/scripts/test_create_fresh_branch.py +30 -126
- package/skills/grok-spawn/SKILL.md +5 -3
- package/skills/grok-spawn/reference/flag-profiles.md +3 -1
- package/skills/orchestrator/SKILL.md +23 -9
- package/skills/orchestrator-refresh/SKILL.md +20 -1
- package/skills/privacy-hygiene/reference/sweep-procedure.md +1 -1
- package/skills/session-log/SKILL.md +1 -1
- package/rules/claude-md-orphan-file.md +0 -28
- package/rules/cleanup-command-forms.md +0 -23
- package/rules/code-reviews.md +0 -11
- package/rules/env-var-table-code-drift.md +0 -10
- package/rules/gh-body-file.md +0 -5
- package/rules/gh-paginate.md +0 -3
- package/rules/hook-prose-matches-detector.md +0 -15
- package/rules/no-historical-clutter.md +0 -26
- package/rules/no-inline-destructive-literals.md +0 -9
- package/rules/no-justification-noise.md +0 -61
- package/rules/package-inventory-stale-entry.md +0 -25
- package/rules/right-sized-engineering.md +0 -28
- package/rules/self-contained-docs.md +0 -17
- package/rules/shell-invocation-policy.md +0 -5
- package/rules/state-what-is.md +0 -25
- package/rules/tdd.md +0 -7
|
@@ -1,40 +1,219 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import importlib
|
|
4
3
|
import io
|
|
4
|
+
import subprocess
|
|
5
5
|
import sys
|
|
6
|
+
from collections.abc import Callable
|
|
6
7
|
from pathlib import Path
|
|
7
8
|
|
|
8
9
|
import pytest
|
|
9
10
|
|
|
11
|
+
import git_hooks_constants
|
|
12
|
+
import pre_push
|
|
13
|
+
|
|
10
14
|
|
|
11
15
|
SCRIPT_DIRECTORY = Path(__file__).resolve().parent
|
|
12
|
-
_git_hooks_directory_string = str(SCRIPT_DIRECTORY)
|
|
13
|
-
while _git_hooks_directory_string in sys.path:
|
|
14
|
-
sys.path.remove(_git_hooks_directory_string)
|
|
15
|
-
sys.path.insert(0, _git_hooks_directory_string)
|
|
16
|
-
for each_module_name in list(sys.modules):
|
|
17
|
-
if each_module_name == "config" or each_module_name.startswith("config."):
|
|
18
|
-
del sys.modules[each_module_name]
|
|
19
|
-
importlib.invalidate_caches()
|
|
20
16
|
|
|
21
|
-
|
|
22
|
-
import git_hooks_constants
|
|
17
|
+
REAL_RUN_GIT_TEXT_COMMAND = pre_push.run_git_text_command
|
|
23
18
|
|
|
24
19
|
|
|
20
|
+
CODE_REVIEW_ENFORCEMENT_ENV_VAR: str = "CLAUDE_CODE_REVIEW_ENFORCEMENT"
|
|
21
|
+
ENFORCEMENT_CONSTANTS_MODULE_NAME: str = "config.code_review_enforcement_constants"
|
|
25
22
|
ALL_ZEROS_OBJECT_NAME: str = "0" * 40
|
|
26
23
|
NON_ZERO_LOCAL_SHA: str = "a" * 40
|
|
27
24
|
NON_ZERO_REMOTE_SHA_ONE: str = "1" * 40
|
|
28
25
|
NON_ZERO_REMOTE_SHA_TWO: str = "2" * 40
|
|
29
26
|
|
|
30
27
|
|
|
28
|
+
GIT_RESOLVED_EXIT_CODE: int = 0
|
|
29
|
+
RESOLVED_COMMIT_OBJECT_NAME: str = "c" * 40
|
|
30
|
+
HOOK_INVOCATION_NAME: str = "pre-push"
|
|
31
|
+
PUSHED_REMOTE_NAME: str = "upstream"
|
|
32
|
+
PUSHED_REMOTE_URL: str = "https://example.invalid/owner/repository.git"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
LOCALE_INVALID_REFERENCE_LISTING_BYTES: bytes = b"origin/caf\xe9\norigin/main\n"
|
|
36
|
+
EMPTY_COMMAND_STREAM_BYTES: bytes = b""
|
|
37
|
+
STRICT_DECODER_NAME: str = "utf-8"
|
|
38
|
+
UNDECODABLE_BYTE_START_INDEX: int = 10
|
|
39
|
+
UNDECODABLE_BYTE_END_INDEX: int = 11
|
|
40
|
+
UNDECODABLE_BYTE_REASON: str = "invalid continuation byte"
|
|
41
|
+
TEXT_DECODING_KEYWORD: str = "text"
|
|
42
|
+
GIT_MISSING_REFERENCE_EXIT_CODE: int = 1
|
|
43
|
+
GIT_LAUNCH_FAILURE_DETAIL: str = "git executable not found"
|
|
44
|
+
RESOLVED_REMOTE_MAIN_REFERENCE: str = "origin/main"
|
|
45
|
+
UNRESOLVABLE_BASE_REPORT_MARKER: str = "no usable gate base"
|
|
46
|
+
GIT_UNAVAILABLE_REPORT_MARKER: str = "could not run git"
|
|
47
|
+
NEW_BRANCH_PUSH_STDIN: str = (
|
|
48
|
+
f"refs/heads/feature {NON_ZERO_LOCAL_SHA} refs/heads/feature {ALL_ZEROS_OBJECT_NAME}\n"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _refuse_locale_decoding() -> UnicodeDecodeError:
|
|
53
|
+
"""Build the error a text-mode subprocess raises on undecodable output."""
|
|
54
|
+
return UnicodeDecodeError(
|
|
55
|
+
STRICT_DECODER_NAME,
|
|
56
|
+
LOCALE_INVALID_REFERENCE_LISTING_BYTES,
|
|
57
|
+
UNDECODABLE_BYTE_START_INDEX,
|
|
58
|
+
UNDECODABLE_BYTE_END_INDEX,
|
|
59
|
+
UNDECODABLE_BYTE_REASON,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _completed_git_process(
|
|
64
|
+
all_command_arguments: list[str], exit_code: int, standard_output_bytes: bytes
|
|
65
|
+
) -> subprocess.CompletedProcess[bytes]:
|
|
66
|
+
"""Build a completed git process carrying raw bytes on standard output."""
|
|
67
|
+
return subprocess.CompletedProcess(
|
|
68
|
+
all_command_arguments,
|
|
69
|
+
exit_code,
|
|
70
|
+
standard_output_bytes,
|
|
71
|
+
EMPTY_COMMAND_STREAM_BYTES,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _answer_git_with_locale_invalid_bytes(
|
|
76
|
+
all_command_arguments: list[str],
|
|
77
|
+
**all_keyword_arguments: object,
|
|
78
|
+
) -> subprocess.CompletedProcess[bytes]:
|
|
79
|
+
"""Stand in for a git whose output the process locale cannot decode.
|
|
80
|
+
|
|
81
|
+
::
|
|
82
|
+
|
|
83
|
+
text=True -> UnicodeDecodeError bytes mode -> the raw listing
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
all_command_arguments: The full git command the seam assembled.
|
|
87
|
+
all_keyword_arguments: The keyword arguments the seam passed.
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
A completed process carrying the raw listing bytes.
|
|
91
|
+
"""
|
|
92
|
+
if all_keyword_arguments.get(TEXT_DECODING_KEYWORD):
|
|
93
|
+
raise _refuse_locale_decoding()
|
|
94
|
+
if git_hooks_constants.GIT_FOR_EACH_REF_SUBCOMMAND in all_command_arguments:
|
|
95
|
+
return _completed_git_process(
|
|
96
|
+
all_command_arguments,
|
|
97
|
+
GIT_RESOLVED_EXIT_CODE,
|
|
98
|
+
LOCALE_INVALID_REFERENCE_LISTING_BYTES,
|
|
99
|
+
)
|
|
100
|
+
return _completed_git_process(
|
|
101
|
+
all_command_arguments,
|
|
102
|
+
GIT_MISSING_REFERENCE_EXIT_CODE,
|
|
103
|
+
EMPTY_COMMAND_STREAM_BYTES,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _write_gate_that_allows_every_push(
|
|
108
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
109
|
+
) -> None:
|
|
110
|
+
"""Point both gates at stubs that allow, with a new-branch push on stdin."""
|
|
111
|
+
passing_gate_path = tmp_path / "code_rules_gate.py"
|
|
112
|
+
passing_gate_path.write_text("import sys\nsys.exit(0)\n", encoding="utf-8")
|
|
113
|
+
monkeypatch.setenv("CODE_RULES_GATE_PATH", str(passing_gate_path))
|
|
114
|
+
_isolate_code_review_gate(tmp_path, monkeypatch)
|
|
115
|
+
monkeypatch.setattr(sys, "stdin", io.StringIO(NEW_BRANCH_PUSH_STDIN))
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def test_reference_listing_with_locale_invalid_bytes_resolves_a_base(
|
|
119
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
120
|
+
) -> None:
|
|
121
|
+
monkeypatch.setattr(subprocess, "run", _answer_git_with_locale_invalid_bytes)
|
|
122
|
+
|
|
123
|
+
resolved_reference = pre_push.resolve_usable_base_reference(
|
|
124
|
+
git_hooks_constants.DEFAULT_REMOTE_BASE_REFERENCE,
|
|
125
|
+
git_hooks_constants.DEFAULT_REMOTE_NAME,
|
|
126
|
+
REAL_RUN_GIT_TEXT_COMMAND,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
assert resolved_reference == RESOLVED_REMOTE_MAIN_REFERENCE
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def test_run_git_text_command_raises_when_git_cannot_launch(
|
|
133
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
134
|
+
) -> None:
|
|
135
|
+
def raising_subprocess_run(
|
|
136
|
+
all_command_arguments: list[str], **all_keyword_arguments: object
|
|
137
|
+
) -> subprocess.CompletedProcess[bytes]:
|
|
138
|
+
raise OSError(GIT_LAUNCH_FAILURE_DETAIL)
|
|
139
|
+
|
|
140
|
+
monkeypatch.setattr(subprocess, "run", raising_subprocess_run)
|
|
141
|
+
|
|
142
|
+
with pytest.raises(pre_push.GitCommandUnavailable):
|
|
143
|
+
REAL_RUN_GIT_TEXT_COMMAND([git_hooks_constants.GIT_FOR_EACH_REF_SUBCOMMAND])
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def test_main_reports_infrastructure_failure_when_git_cannot_launch(
|
|
147
|
+
tmp_path: Path,
|
|
148
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
149
|
+
capsys: pytest.CaptureFixture[str],
|
|
150
|
+
) -> None:
|
|
151
|
+
_write_gate_that_allows_every_push(tmp_path, monkeypatch)
|
|
152
|
+
|
|
153
|
+
def unavailable_run_git_text_command(
|
|
154
|
+
all_command_arguments: list[str],
|
|
155
|
+
) -> tuple[int, str]:
|
|
156
|
+
raise pre_push.GitCommandUnavailable(GIT_LAUNCH_FAILURE_DETAIL)
|
|
157
|
+
|
|
158
|
+
monkeypatch.setattr(
|
|
159
|
+
pre_push, "run_git_text_command", unavailable_run_git_text_command
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
exit_code = pre_push.main()
|
|
163
|
+
|
|
164
|
+
captured_streams = capsys.readouterr()
|
|
165
|
+
assert exit_code == git_hooks_constants.GATE_INFRASTRUCTURE_FAILURE_EXIT_CODE
|
|
166
|
+
assert GIT_UNAVAILABLE_REPORT_MARKER in captured_streams.err
|
|
167
|
+
assert GIT_LAUNCH_FAILURE_DETAIL in captured_streams.err
|
|
168
|
+
assert UNRESOLVABLE_BASE_REPORT_MARKER not in captured_streams.err
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def test_main_reports_a_missing_default_branch_as_absence(
|
|
172
|
+
tmp_path: Path,
|
|
173
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
174
|
+
capsys: pytest.CaptureFixture[str],
|
|
175
|
+
) -> None:
|
|
176
|
+
_write_gate_that_allows_every_push(tmp_path, monkeypatch)
|
|
177
|
+
|
|
178
|
+
def empty_run_git_text_command(
|
|
179
|
+
all_command_arguments: list[str],
|
|
180
|
+
) -> tuple[int, str]:
|
|
181
|
+
return GIT_MISSING_REFERENCE_EXIT_CODE, ""
|
|
182
|
+
|
|
183
|
+
monkeypatch.setattr(pre_push, "run_git_text_command", empty_run_git_text_command)
|
|
184
|
+
|
|
185
|
+
exit_code = pre_push.main()
|
|
186
|
+
|
|
187
|
+
captured_streams = capsys.readouterr()
|
|
188
|
+
assert exit_code == git_hooks_constants.GATE_INFRASTRUCTURE_FAILURE_EXIT_CODE
|
|
189
|
+
assert UNRESOLVABLE_BASE_REPORT_MARKER in captured_streams.err
|
|
190
|
+
assert GIT_UNAVAILABLE_REPORT_MARKER not in captured_streams.err
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
@pytest.fixture(autouse=True)
|
|
194
|
+
def resolve_remote_head_reference(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
195
|
+
"""Make every reference resolve, so these tests ignore the clone's remote state.
|
|
196
|
+
|
|
197
|
+
``main`` resolves the gate base through git before it invokes the gate. A
|
|
198
|
+
clone with no ``origin/HEAD`` would send these tests down the unresolvable
|
|
199
|
+
path and change what ``main`` returns. Stubbing the seam keeps each test
|
|
200
|
+
about the flow it names. Base resolution itself is covered by
|
|
201
|
+
``test_pre_push_base_reference.py``.
|
|
202
|
+
"""
|
|
203
|
+
|
|
204
|
+
def fake_run_git_text_command(all_command_arguments: list[str]) -> tuple[int, str]:
|
|
205
|
+
return GIT_RESOLVED_EXIT_CODE, RESOLVED_COMMIT_OBJECT_NAME
|
|
206
|
+
|
|
207
|
+
monkeypatch.setattr(pre_push, "run_git_text_command", fake_run_git_text_command)
|
|
208
|
+
|
|
209
|
+
|
|
31
210
|
def _isolate_code_review_gate(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
32
211
|
monkeypatch.setenv(
|
|
33
212
|
"CODE_REVIEW_PUSH_GATE_PATH", str(tmp_path / "no_code_review_gate.py")
|
|
34
213
|
)
|
|
35
214
|
|
|
36
215
|
|
|
37
|
-
def
|
|
216
|
+
def test_resolve_base_reference_from_stdin_uses_remote_object_when_non_zero() -> None:
|
|
38
217
|
stdin_text = (
|
|
39
218
|
f"refs/heads/feature {NON_ZERO_LOCAL_SHA} refs/heads/feature {NON_ZERO_REMOTE_SHA_ONE}\n"
|
|
40
219
|
)
|
|
@@ -44,7 +223,7 @@ def test_resolve_base_reference_uses_remote_object_when_non_zero() -> None:
|
|
|
44
223
|
assert base_reference == NON_ZERO_REMOTE_SHA_ONE
|
|
45
224
|
|
|
46
225
|
|
|
47
|
-
def
|
|
226
|
+
def test_resolve_base_reference_from_stdin_falls_back_when_remote_is_all_zeros() -> None:
|
|
48
227
|
stdin_text = f"refs/heads/feature {NON_ZERO_LOCAL_SHA} refs/heads/feature {ALL_ZEROS_OBJECT_NAME}\n"
|
|
49
228
|
|
|
50
229
|
base_reference = pre_push.resolve_base_reference_from_stdin(stdin_text)
|
|
@@ -52,13 +231,13 @@ def test_resolve_base_reference_falls_back_when_remote_is_all_zeros() -> None:
|
|
|
52
231
|
assert base_reference == pre_push.DEFAULT_REMOTE_BASE_REFERENCE
|
|
53
232
|
|
|
54
233
|
|
|
55
|
-
def
|
|
234
|
+
def test_resolve_base_reference_from_stdin_falls_back_when_stdin_is_empty() -> None:
|
|
56
235
|
base_reference = pre_push.resolve_base_reference_from_stdin("")
|
|
57
236
|
|
|
58
237
|
assert base_reference == pre_push.DEFAULT_REMOTE_BASE_REFERENCE
|
|
59
238
|
|
|
60
239
|
|
|
61
|
-
def
|
|
240
|
+
def test_resolve_base_reference_from_stdin_prefers_first_non_zero_remote_object() -> (
|
|
62
241
|
None
|
|
63
242
|
):
|
|
64
243
|
stdin_text = (
|
|
@@ -121,6 +300,37 @@ def test_main_invokes_gate_with_resolved_base_reference(
|
|
|
121
300
|
assert recorded_arguments == ["--base", remote_sha]
|
|
122
301
|
|
|
123
302
|
|
|
303
|
+
def test_main_passes_the_pushed_remote_name_to_base_resolution(
|
|
304
|
+
tmp_path: Path,
|
|
305
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
306
|
+
) -> None:
|
|
307
|
+
_write_passing_code_rules_gate(tmp_path, monkeypatch)
|
|
308
|
+
_isolate_code_review_gate(tmp_path, monkeypatch)
|
|
309
|
+
all_recorded_remote_names: list[str] = []
|
|
310
|
+
|
|
311
|
+
def recording_resolve_usable_base_reference(
|
|
312
|
+
base_reference: str,
|
|
313
|
+
remote_name: str,
|
|
314
|
+
ask_git: Callable[[list[str]], tuple[int, str]],
|
|
315
|
+
) -> str:
|
|
316
|
+
all_recorded_remote_names.append(remote_name)
|
|
317
|
+
return base_reference
|
|
318
|
+
|
|
319
|
+
monkeypatch.setattr(
|
|
320
|
+
pre_push,
|
|
321
|
+
"resolve_usable_base_reference",
|
|
322
|
+
recording_resolve_usable_base_reference,
|
|
323
|
+
)
|
|
324
|
+
monkeypatch.setattr(
|
|
325
|
+
sys, "argv", [HOOK_INVOCATION_NAME, PUSHED_REMOTE_NAME, PUSHED_REMOTE_URL]
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
exit_code = pre_push.main()
|
|
329
|
+
|
|
330
|
+
assert exit_code == 0
|
|
331
|
+
assert all_recorded_remote_names == [PUSHED_REMOTE_NAME]
|
|
332
|
+
|
|
333
|
+
|
|
124
334
|
def test_main_propagates_blocking_exit_code_from_gate(
|
|
125
335
|
tmp_path: Path,
|
|
126
336
|
monkeypatch: pytest.MonkeyPatch,
|
|
@@ -193,7 +403,7 @@ def test_main_exits_two_when_invoke_gate_raises_oserror(
|
|
|
193
403
|
assert exit_code == git_hooks_constants.GATE_INFRASTRUCTURE_FAILURE_EXIT_CODE
|
|
194
404
|
|
|
195
405
|
|
|
196
|
-
def
|
|
406
|
+
def test_resolve_base_reference_from_stdin_emits_warning_for_malformed_line(
|
|
197
407
|
capsys: pytest.CaptureFixture[str],
|
|
198
408
|
) -> None:
|
|
199
409
|
malformed_stdin_text = "only_one_field\n"
|
|
@@ -204,7 +414,7 @@ def test_resolve_base_reference_emits_warning_for_malformed_line(
|
|
|
204
414
|
assert "malformed" in captured.err
|
|
205
415
|
|
|
206
416
|
|
|
207
|
-
def
|
|
417
|
+
def test_resolve_base_reference_from_stdin_returns_none_when_local_sha_is_all_zeros() -> None:
|
|
208
418
|
stdin_text = f"refs/heads/feature {ALL_ZEROS_OBJECT_NAME} refs/heads/feature {ALL_ZEROS_OBJECT_NAME}\n"
|
|
209
419
|
|
|
210
420
|
base_reference = pre_push.resolve_base_reference_from_stdin(stdin_text)
|
|
@@ -227,9 +437,7 @@ def test_main_exits_zero_immediately_when_push_is_deletion(
|
|
|
227
437
|
assert exit_code == 0
|
|
228
438
|
|
|
229
439
|
|
|
230
|
-
def
|
|
231
|
-
capsys: pytest.CaptureFixture[str],
|
|
232
|
-
) -> None:
|
|
440
|
+
def test_resolve_base_reference_from_stdin_returns_sentinel_for_malformed_lines() -> None:
|
|
233
441
|
malformed_only_stdin = "one_field_only\nalso_malformed\n"
|
|
234
442
|
|
|
235
443
|
base_reference = pre_push.resolve_base_reference_from_stdin(malformed_only_stdin)
|
|
@@ -254,9 +462,7 @@ def test_main_prints_stderr_when_gate_script_missing(
|
|
|
254
462
|
assert captured.err != ""
|
|
255
463
|
|
|
256
464
|
|
|
257
|
-
def
|
|
258
|
-
capsys: pytest.CaptureFixture[str],
|
|
259
|
-
) -> None:
|
|
465
|
+
def test_resolve_base_reference_from_stdin_reports_sentinel_with_no_valid_lines() -> None:
|
|
260
466
|
malformed_only_stdin = "one_field_only\nalso_malformed\n"
|
|
261
467
|
|
|
262
468
|
result = pre_push.resolve_base_reference_from_stdin(malformed_only_stdin)
|
|
@@ -550,11 +756,14 @@ def test_main_allows_when_code_review_enforcement_flag_is_off(
|
|
|
550
756
|
) -> None:
|
|
551
757
|
"""Native pre-push honors the real gate with enforcement off.
|
|
552
758
|
|
|
553
|
-
Points at the production push gate (not a reason stub).
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
759
|
+
Points at the production push gate (not a reason stub). The gate resolves
|
|
760
|
+
its flag when its config module first executes, so this test clears the
|
|
761
|
+
environment variable and evicts that one module if a sibling suite cached
|
|
762
|
+
it earlier in the same session. The gate then returns no deny reason and
|
|
763
|
+
the backstop allows the push.
|
|
557
764
|
"""
|
|
765
|
+
monkeypatch.delenv(CODE_REVIEW_ENFORCEMENT_ENV_VAR, raising=False)
|
|
766
|
+
monkeypatch.delitem(sys.modules, ENFORCEMENT_CONSTANTS_MODULE_NAME, raising=False)
|
|
558
767
|
_write_passing_code_rules_gate(tmp_path, monkeypatch)
|
|
559
768
|
real_gate_path = (
|
|
560
769
|
Path(__file__).resolve().parent.parent
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
"""Specifications for the pre-push hook's remote base reference resolution.
|
|
2
|
+
|
|
3
|
+
A clone leaves ``origin/HEAD`` unset, so the gate base the hook hands to
|
|
4
|
+
``code_rules_gate.py`` names an object git cannot resolve and the push aborts.
|
|
5
|
+
These specifications pin the fallback to the default branch of the remote git
|
|
6
|
+
names in the hook's arguments, across every candidate default branch name.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from collections.abc import Callable
|
|
12
|
+
|
|
13
|
+
import pytest
|
|
14
|
+
|
|
15
|
+
import git_hooks_constants
|
|
16
|
+
import pre_push_base_reference
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
RESOLVED_REMOTE_MAIN_REFERENCE: str = "origin/main"
|
|
20
|
+
RESOLVED_REMOTE_MASTER_REFERENCE: str = "origin/master"
|
|
21
|
+
RESOLVED_REMOTE_TRUNK_REFERENCE: str = "origin/trunk"
|
|
22
|
+
REMOTE_HEAD_SYMBOLIC_TARGET: str = "refs/remotes/origin/main"
|
|
23
|
+
UPSTREAM_REMOTE_NAME: str = "upstream"
|
|
24
|
+
RESOLVED_UPSTREAM_MAIN_REFERENCE: str = "upstream/main"
|
|
25
|
+
UPSTREAM_HEAD_SYMBOLIC_TARGET: str = "refs/remotes/upstream/main"
|
|
26
|
+
HOOK_INVOCATION_NAME: str = "pre-push"
|
|
27
|
+
PUSHED_REMOTE_URL: str = "https://example.invalid/owner/repository.git"
|
|
28
|
+
SSH_PUSHED_REMOTE_URL: str = "git@example.invalid:owner/repository.git"
|
|
29
|
+
CONCRETE_REMOTE_OBJECT_NAME: str = "1" * 40
|
|
30
|
+
ALL_GLOB_BEARING_REMOTE_NAMES: tuple[str, ...] = (
|
|
31
|
+
"orig*n",
|
|
32
|
+
"orig?n",
|
|
33
|
+
"orig[i]n",
|
|
34
|
+
"orig\\n",
|
|
35
|
+
)
|
|
36
|
+
GIT_COMMAND_SUCCESS_CODE: int = 0
|
|
37
|
+
GIT_COMMAND_FAILURE_CODE: int = 1
|
|
38
|
+
EMPTY_COMMAND_OUTPUT: str = ""
|
|
39
|
+
LISTING_LINE_SEPARATOR: str = "\n"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _list_existing_short_names(
|
|
43
|
+
command_arguments: list[str],
|
|
44
|
+
all_resolvable_references: set[str],
|
|
45
|
+
) -> str:
|
|
46
|
+
"""Answer a ``for-each-ref`` listing in reverse-sorted order.
|
|
47
|
+
|
|
48
|
+
Git sorts a listing by reference name rather than by the pattern order the
|
|
49
|
+
caller passed, so this seam reverses the sort to keep that clash visible.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
command_arguments: The git arguments the hook passed.
|
|
53
|
+
all_resolvable_references: The short names this repository holds.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
The newline-joined short names, one per existing reference.
|
|
57
|
+
"""
|
|
58
|
+
prefix = git_hooks_constants.REMOTE_REFERENCE_NAME_PREFIX
|
|
59
|
+
all_listed_short_names = [
|
|
60
|
+
each_argument.removeprefix(prefix)
|
|
61
|
+
for each_argument in command_arguments
|
|
62
|
+
if each_argument.removeprefix(prefix) in all_resolvable_references
|
|
63
|
+
]
|
|
64
|
+
return LISTING_LINE_SEPARATOR.join(sorted(all_listed_short_names, reverse=True))
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _build_fake_git(
|
|
68
|
+
all_resolvable_references: set[str],
|
|
69
|
+
symbolic_reference_target: str | None,
|
|
70
|
+
all_recorded_command_arguments: list[list[str]],
|
|
71
|
+
) -> Callable[[list[str]], tuple[int, str]]:
|
|
72
|
+
"""Return a git seam that answers from a table rather than a repository."""
|
|
73
|
+
|
|
74
|
+
def fake_run_git_text_command(command_arguments: list[str]) -> tuple[int, str]:
|
|
75
|
+
all_recorded_command_arguments.append(list(command_arguments))
|
|
76
|
+
if git_hooks_constants.GIT_SYMBOLIC_REFERENCE_SUBCOMMAND in command_arguments:
|
|
77
|
+
if symbolic_reference_target is None:
|
|
78
|
+
return GIT_COMMAND_FAILURE_CODE, EMPTY_COMMAND_OUTPUT
|
|
79
|
+
return GIT_COMMAND_SUCCESS_CODE, symbolic_reference_target
|
|
80
|
+
if git_hooks_constants.GIT_FOR_EACH_REF_SUBCOMMAND in command_arguments:
|
|
81
|
+
return GIT_COMMAND_SUCCESS_CODE, _list_existing_short_names(
|
|
82
|
+
command_arguments, all_resolvable_references
|
|
83
|
+
)
|
|
84
|
+
requested_reference = command_arguments[-1].removesuffix(
|
|
85
|
+
git_hooks_constants.COMMIT_OBJECT_NAME_SUFFIX
|
|
86
|
+
)
|
|
87
|
+
if requested_reference in all_resolvable_references:
|
|
88
|
+
return GIT_COMMAND_SUCCESS_CODE, CONCRETE_REMOTE_OBJECT_NAME
|
|
89
|
+
return GIT_COMMAND_FAILURE_CODE, EMPTY_COMMAND_OUTPUT
|
|
90
|
+
|
|
91
|
+
return fake_run_git_text_command
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _build_recording_git(
|
|
95
|
+
all_resolvable_references: set[str],
|
|
96
|
+
symbolic_reference_target: str | None,
|
|
97
|
+
) -> tuple[Callable[[list[str]], tuple[int, str]], list[list[str]]]:
|
|
98
|
+
"""Build the table-driven git seam together with its call record.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
all_resolvable_references: Reference names ``rev-parse`` resolves and
|
|
102
|
+
``for-each-ref`` lists.
|
|
103
|
+
symbolic_reference_target: The target ``symbolic-ref`` reports, or None
|
|
104
|
+
when the symbolic reference is unset.
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
The git seam paired with the list recording each argument list it saw.
|
|
108
|
+
"""
|
|
109
|
+
all_recorded_command_arguments: list[list[str]] = []
|
|
110
|
+
ask_git = _build_fake_git(
|
|
111
|
+
all_resolvable_references,
|
|
112
|
+
symbolic_reference_target,
|
|
113
|
+
all_recorded_command_arguments,
|
|
114
|
+
)
|
|
115
|
+
return ask_git, all_recorded_command_arguments
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def should_keep_the_base_reference_when_git_resolves_it() -> None:
|
|
119
|
+
ask_git, _ = _build_recording_git(
|
|
120
|
+
{git_hooks_constants.DEFAULT_REMOTE_BASE_REFERENCE},
|
|
121
|
+
symbolic_reference_target=None,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
resolved_reference = pre_push_base_reference.resolve_usable_base_reference(
|
|
125
|
+
git_hooks_constants.DEFAULT_REMOTE_BASE_REFERENCE,
|
|
126
|
+
git_hooks_constants.DEFAULT_REMOTE_NAME,
|
|
127
|
+
ask_git,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
assert resolved_reference == git_hooks_constants.DEFAULT_REMOTE_BASE_REFERENCE
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def should_use_the_symbolic_target_when_remote_head_is_unresolvable() -> None:
|
|
134
|
+
ask_git, _ = _build_recording_git(
|
|
135
|
+
{RESOLVED_REMOTE_MAIN_REFERENCE},
|
|
136
|
+
symbolic_reference_target=REMOTE_HEAD_SYMBOLIC_TARGET,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
resolved_reference = pre_push_base_reference.resolve_usable_base_reference(
|
|
140
|
+
git_hooks_constants.DEFAULT_REMOTE_BASE_REFERENCE,
|
|
141
|
+
git_hooks_constants.DEFAULT_REMOTE_NAME,
|
|
142
|
+
ask_git,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
assert resolved_reference == RESOLVED_REMOTE_MAIN_REFERENCE
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def should_fall_back_to_a_candidate_when_the_symbolic_reference_is_unset() -> None:
|
|
149
|
+
ask_git, _ = _build_recording_git(
|
|
150
|
+
{RESOLVED_REMOTE_MASTER_REFERENCE},
|
|
151
|
+
symbolic_reference_target=None,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
resolved_reference = pre_push_base_reference.resolve_usable_base_reference(
|
|
155
|
+
git_hooks_constants.DEFAULT_REMOTE_BASE_REFERENCE,
|
|
156
|
+
git_hooks_constants.DEFAULT_REMOTE_NAME,
|
|
157
|
+
ask_git,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
assert resolved_reference == RESOLVED_REMOTE_MASTER_REFERENCE
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def should_prefer_main_when_the_listing_reports_master_first() -> None:
|
|
164
|
+
ask_git, _ = _build_recording_git(
|
|
165
|
+
{RESOLVED_REMOTE_MAIN_REFERENCE, RESOLVED_REMOTE_MASTER_REFERENCE},
|
|
166
|
+
symbolic_reference_target=None,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
resolved_reference = pre_push_base_reference.resolve_usable_base_reference(
|
|
170
|
+
git_hooks_constants.DEFAULT_REMOTE_BASE_REFERENCE,
|
|
171
|
+
git_hooks_constants.DEFAULT_REMOTE_NAME,
|
|
172
|
+
ask_git,
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
assert resolved_reference == RESOLVED_REMOTE_MAIN_REFERENCE
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def should_resolve_a_trunk_default_branch() -> None:
|
|
179
|
+
ask_git, _ = _build_recording_git(
|
|
180
|
+
{RESOLVED_REMOTE_TRUNK_REFERENCE},
|
|
181
|
+
symbolic_reference_target=None,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
resolved_reference = pre_push_base_reference.resolve_usable_base_reference(
|
|
185
|
+
git_hooks_constants.DEFAULT_REMOTE_BASE_REFERENCE,
|
|
186
|
+
git_hooks_constants.DEFAULT_REMOTE_NAME,
|
|
187
|
+
ask_git,
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
assert resolved_reference == RESOLVED_REMOTE_TRUNK_REFERENCE
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def should_resolve_through_a_remote_named_other_than_origin() -> None:
|
|
194
|
+
ask_git, _ = _build_recording_git(
|
|
195
|
+
{RESOLVED_UPSTREAM_MAIN_REFERENCE},
|
|
196
|
+
symbolic_reference_target=UPSTREAM_HEAD_SYMBOLIC_TARGET,
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
resolved_reference = pre_push_base_reference.resolve_usable_base_reference(
|
|
200
|
+
git_hooks_constants.DEFAULT_REMOTE_BASE_REFERENCE,
|
|
201
|
+
UPSTREAM_REMOTE_NAME,
|
|
202
|
+
ask_git,
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
assert resolved_reference == RESOLVED_UPSTREAM_MAIN_REFERENCE
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def should_list_candidate_branches_under_the_named_remote() -> None:
|
|
209
|
+
ask_git, all_recorded_command_arguments = _build_recording_git(
|
|
210
|
+
{RESOLVED_UPSTREAM_MAIN_REFERENCE},
|
|
211
|
+
symbolic_reference_target=None,
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
resolved_reference = pre_push_base_reference.resolve_usable_base_reference(
|
|
215
|
+
git_hooks_constants.DEFAULT_REMOTE_BASE_REFERENCE,
|
|
216
|
+
UPSTREAM_REMOTE_NAME,
|
|
217
|
+
ask_git,
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
listing_command_arguments = all_recorded_command_arguments[-1]
|
|
221
|
+
assert resolved_reference == RESOLVED_UPSTREAM_MAIN_REFERENCE
|
|
222
|
+
assert UPSTREAM_HEAD_SYMBOLIC_TARGET in listing_command_arguments
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def should_report_no_usable_reference_when_nothing_resolves() -> None:
|
|
226
|
+
ask_git, _ = _build_recording_git(set(), symbolic_reference_target=None)
|
|
227
|
+
|
|
228
|
+
resolved_reference = pre_push_base_reference.resolve_usable_base_reference(
|
|
229
|
+
git_hooks_constants.DEFAULT_REMOTE_BASE_REFERENCE,
|
|
230
|
+
git_hooks_constants.DEFAULT_REMOTE_NAME,
|
|
231
|
+
ask_git,
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
assert resolved_reference is None
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def should_name_the_pushed_remote_in_the_unresolvable_report(
|
|
238
|
+
capsys: pytest.CaptureFixture[str],
|
|
239
|
+
) -> None:
|
|
240
|
+
ask_git, _ = _build_recording_git(set(), symbolic_reference_target=None)
|
|
241
|
+
|
|
242
|
+
pre_push_base_reference.resolve_usable_base_reference(
|
|
243
|
+
git_hooks_constants.DEFAULT_REMOTE_BASE_REFERENCE,
|
|
244
|
+
UPSTREAM_REMOTE_NAME,
|
|
245
|
+
ask_git,
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
captured_streams = capsys.readouterr()
|
|
249
|
+
assert UPSTREAM_REMOTE_NAME in captured_streams.err
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def should_pass_a_concrete_object_name_through_untouched() -> None:
|
|
253
|
+
ask_git, _ = _build_recording_git(
|
|
254
|
+
set(), symbolic_reference_target=REMOTE_HEAD_SYMBOLIC_TARGET
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
resolved_reference = pre_push_base_reference.resolve_usable_base_reference(
|
|
258
|
+
CONCRETE_REMOTE_OBJECT_NAME,
|
|
259
|
+
git_hooks_constants.DEFAULT_REMOTE_NAME,
|
|
260
|
+
ask_git,
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
assert resolved_reference == CONCRETE_REMOTE_OBJECT_NAME
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def should_ask_git_nothing_about_a_concrete_object_name() -> None:
|
|
267
|
+
ask_git, all_recorded_command_arguments = _build_recording_git(
|
|
268
|
+
set(), symbolic_reference_target=REMOTE_HEAD_SYMBOLIC_TARGET
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
pre_push_base_reference.resolve_usable_base_reference(
|
|
272
|
+
CONCRETE_REMOTE_OBJECT_NAME,
|
|
273
|
+
git_hooks_constants.DEFAULT_REMOTE_NAME,
|
|
274
|
+
ask_git,
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
assert all_recorded_command_arguments == []
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def should_ask_git_to_verify_the_reference_as_a_commit() -> None:
|
|
281
|
+
ask_git, all_recorded_command_arguments = _build_recording_git(
|
|
282
|
+
{git_hooks_constants.DEFAULT_REMOTE_BASE_REFERENCE},
|
|
283
|
+
symbolic_reference_target=None,
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
pre_push_base_reference.resolve_usable_base_reference(
|
|
287
|
+
git_hooks_constants.DEFAULT_REMOTE_BASE_REFERENCE,
|
|
288
|
+
git_hooks_constants.DEFAULT_REMOTE_NAME,
|
|
289
|
+
ask_git,
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
first_command_arguments = all_recorded_command_arguments[0]
|
|
293
|
+
assert first_command_arguments[-1] == (
|
|
294
|
+
git_hooks_constants.DEFAULT_REMOTE_BASE_REFERENCE
|
|
295
|
+
+ git_hooks_constants.COMMIT_OBJECT_NAME_SUFFIX
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def test_reads_the_remote_name_from_the_push_arguments() -> None:
|
|
300
|
+
resolved_remote_name = pre_push_base_reference.resolve_remote_name_from_arguments(
|
|
301
|
+
[HOOK_INVOCATION_NAME, UPSTREAM_REMOTE_NAME, PUSHED_REMOTE_URL]
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
assert resolved_remote_name == UPSTREAM_REMOTE_NAME
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def test_uses_the_default_remote_when_a_url_takes_the_remote_position() -> None:
|
|
308
|
+
resolved_remote_name = pre_push_base_reference.resolve_remote_name_from_arguments(
|
|
309
|
+
[HOOK_INVOCATION_NAME, PUSHED_REMOTE_URL, PUSHED_REMOTE_URL]
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
assert resolved_remote_name == git_hooks_constants.DEFAULT_REMOTE_NAME
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def test_uses_the_default_remote_when_an_ssh_url_takes_the_remote_position() -> None:
|
|
316
|
+
resolved_remote_name = pre_push_base_reference.resolve_remote_name_from_arguments(
|
|
317
|
+
[HOOK_INVOCATION_NAME, SSH_PUSHED_REMOTE_URL, SSH_PUSHED_REMOTE_URL]
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
assert resolved_remote_name == git_hooks_constants.DEFAULT_REMOTE_NAME
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
@pytest.mark.parametrize("glob_bearing_remote_name", ALL_GLOB_BEARING_REMOTE_NAMES)
|
|
324
|
+
def test_uses_the_default_remote_when_the_remote_name_carries_a_glob_character(
|
|
325
|
+
glob_bearing_remote_name: str,
|
|
326
|
+
) -> None:
|
|
327
|
+
resolved_remote_name = pre_push_base_reference.resolve_remote_name_from_arguments(
|
|
328
|
+
[HOOK_INVOCATION_NAME, glob_bearing_remote_name, PUSHED_REMOTE_URL]
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
assert resolved_remote_name == git_hooks_constants.DEFAULT_REMOTE_NAME
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def test_uses_the_default_remote_when_the_push_arguments_are_empty() -> None:
|
|
335
|
+
resolved_remote_name = pre_push_base_reference.resolve_remote_name_from_arguments(
|
|
336
|
+
[HOOK_INVOCATION_NAME]
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
assert resolved_remote_name == git_hooks_constants.DEFAULT_REMOTE_NAME
|