claude-dev-env 2.7.0 → 2.8.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/_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/pr-loop/worker-spawn.md +3 -1
- 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/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 +309 -12
- 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/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/package.json +1 -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/scripts/CLAUDE.md +4 -4
- 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_worker_constants.py +83 -13
- package/scripts/grok_headless_runner.py +148 -18
- package/scripts/resolve_worker_spawn.py +56 -10
- package/scripts/spawn_grok_batch.py +81 -23
- package/scripts/test_claude_chain_runner.py +358 -0
- package/scripts/test_grok_headless_runner.py +547 -10
- package/scripts/test_invoke_code_review.py +298 -0
- package/scripts/test_resolve_worker_spawn.py +185 -15
- package/scripts/test_spawn_grok_batch.py +326 -22
- package/scripts/tests/CLAUDE.md +1 -0
- package/scripts/tests/test_grok_worker_constants.py +59 -0
- package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +46 -0
- package/skills/autoconverge/workflow/converge.contract.test.mjs +133 -8
- package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +107 -1
- package/skills/autoconverge/workflow/converge.mjs +113 -31
- 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 +9 -8
- 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/xhigh.md +30 -12
- 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 -3
- package/skills/grok-spawn/reference/flag-profiles.md +3 -1
- package/skills/orchestrator/SKILL.md +4 -1
- package/skills/orchestrator-refresh/SKILL.md +5 -1
- package/skills/team-advisor/SKILL.md +4 -1
|
@@ -129,6 +129,43 @@ def test_reference_listing_with_locale_invalid_bytes_resolves_a_base(
|
|
|
129
129
|
assert resolved_reference == RESOLVED_REMOTE_MAIN_REFERENCE
|
|
130
130
|
|
|
131
131
|
|
|
132
|
+
def test_run_git_reference_query_returns_replaced_text_on_locale_invalid_bytes(
|
|
133
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
134
|
+
) -> None:
|
|
135
|
+
"""text=True would raise; bytes mode returns the replacing-policy decode."""
|
|
136
|
+
locale_invalid_reference_bytes = b"refs/remotes/origin/caf\xe9"
|
|
137
|
+
expected_replaced_reference = locale_invalid_reference_bytes.decode(
|
|
138
|
+
git_hooks_constants.GIT_OUTPUT_ENCODING_NAME,
|
|
139
|
+
errors=git_hooks_constants.GIT_OUTPUT_DECODE_ERRORS_POLICY,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
def answer_with_locale_invalid_reference(
|
|
143
|
+
all_command_arguments: list[str], **all_keyword_arguments: object
|
|
144
|
+
) -> subprocess.CompletedProcess[bytes]:
|
|
145
|
+
if all_keyword_arguments.get(TEXT_DECODING_KEYWORD):
|
|
146
|
+
raise _refuse_locale_decoding()
|
|
147
|
+
return _completed_git_process(
|
|
148
|
+
all_command_arguments,
|
|
149
|
+
GIT_RESOLVED_EXIT_CODE,
|
|
150
|
+
locale_invalid_reference_bytes,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
monkeypatch.setattr(subprocess, "run", answer_with_locale_invalid_reference)
|
|
154
|
+
|
|
155
|
+
resolved_output = pre_push.run_git_reference_query(
|
|
156
|
+
("git", "rev-parse", "--verify", "--quiet", "refs/remotes/origin/HEAD")
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
assert resolved_output == expected_replaced_reference
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def test_unresolvable_merge_base_message_does_not_claim_dangling_origin_head() -> None:
|
|
163
|
+
skip_message = git_hooks_constants.UNRESOLVABLE_MERGE_BASE_MESSAGE
|
|
164
|
+
assert "origin/HEAD names a ref" not in skip_message
|
|
165
|
+
assert "unrelated histories" in skip_message
|
|
166
|
+
assert "merge-base still could not name a shared commit" in skip_message
|
|
167
|
+
|
|
168
|
+
|
|
132
169
|
def test_run_git_text_command_raises_when_git_cannot_launch(
|
|
133
170
|
monkeypatch: pytest.MonkeyPatch,
|
|
134
171
|
) -> None:
|
|
@@ -281,6 +318,7 @@ def test_main_invokes_gate_with_resolved_base_reference(
|
|
|
281
318
|
)
|
|
282
319
|
monkeypatch.setenv("CODE_RULES_GATE_PATH", str(recording_gate_script_path))
|
|
283
320
|
_isolate_code_review_gate(tmp_path, monkeypatch)
|
|
321
|
+
_isolate_from_default_branch_refs(tmp_path, monkeypatch)
|
|
284
322
|
remote_sha = "9" * 40
|
|
285
323
|
monkeypatch.setattr(
|
|
286
324
|
sys,
|
|
@@ -304,7 +342,9 @@ def test_main_passes_the_pushed_remote_name_to_base_resolution(
|
|
|
304
342
|
tmp_path: Path,
|
|
305
343
|
monkeypatch: pytest.MonkeyPatch,
|
|
306
344
|
) -> None:
|
|
307
|
-
|
|
345
|
+
# No default-branch ref so the gate base is the stdin remote object, not a
|
|
346
|
+
# merge-base skip; usable-base resolution then sees the pushed remote name.
|
|
347
|
+
_isolate_repository_and_write_passing_code_rules_gate(tmp_path, monkeypatch)
|
|
308
348
|
_isolate_code_review_gate(tmp_path, monkeypatch)
|
|
309
349
|
all_recorded_remote_names: list[str] = []
|
|
310
350
|
|
|
@@ -527,6 +567,7 @@ def test_invoke_gate_uses_resolved_path(
|
|
|
527
567
|
resolved_path = symlink_gate_path.resolve()
|
|
528
568
|
monkeypatch.setenv("CODE_RULES_GATE_PATH", str(symlink_gate_path))
|
|
529
569
|
_isolate_code_review_gate(tmp_path, monkeypatch)
|
|
570
|
+
_isolate_from_default_branch_refs(tmp_path, monkeypatch)
|
|
530
571
|
monkeypatch.setattr(sys, "stdin", io.StringIO(
|
|
531
572
|
f"refs/heads/feature {NON_ZERO_LOCAL_SHA} refs/heads/feature {NON_ZERO_REMOTE_SHA_ONE}\n"
|
|
532
573
|
))
|
|
@@ -660,7 +701,10 @@ def test_main_allows_main_onto_main_push(
|
|
|
660
701
|
CODE_REVIEW_STUB_BLOCK_REASON: str = "CODE_REVIEW_STUB_BLOCK_REASON"
|
|
661
702
|
|
|
662
703
|
|
|
663
|
-
def _write_passing_code_rules_gate(
|
|
704
|
+
def _write_passing_code_rules_gate(
|
|
705
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
706
|
+
) -> None:
|
|
707
|
+
"""Install a passing CODE_RULES gate and a one-line feature push on stdin."""
|
|
664
708
|
passing_gate_path = tmp_path / "code_rules_gate.py"
|
|
665
709
|
passing_gate_path.write_text("import sys\nsys.exit(0)\n", encoding="utf-8")
|
|
666
710
|
monkeypatch.setenv("CODE_RULES_GATE_PATH", str(passing_gate_path))
|
|
@@ -673,6 +717,14 @@ def _write_passing_code_rules_gate(tmp_path: Path, monkeypatch: pytest.MonkeyPat
|
|
|
673
717
|
)
|
|
674
718
|
|
|
675
719
|
|
|
720
|
+
def _isolate_repository_and_write_passing_code_rules_gate(
|
|
721
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
722
|
+
) -> None:
|
|
723
|
+
"""Move into a repository without origin refs, then install a passing gate."""
|
|
724
|
+
_isolate_from_default_branch_refs(tmp_path, monkeypatch)
|
|
725
|
+
_write_passing_code_rules_gate(tmp_path, monkeypatch)
|
|
726
|
+
|
|
727
|
+
|
|
676
728
|
def _write_code_review_gate_stub(tmp_path: Path, returned_reason_literal: str) -> Path:
|
|
677
729
|
stub_gate_path = tmp_path / "code_review_push_gate.py"
|
|
678
730
|
stub_gate_path.write_text(
|
|
@@ -688,7 +740,7 @@ def test_main_blocks_when_code_review_gate_returns_reason(
|
|
|
688
740
|
monkeypatch: pytest.MonkeyPatch,
|
|
689
741
|
capsys: pytest.CaptureFixture[str],
|
|
690
742
|
) -> None:
|
|
691
|
-
|
|
743
|
+
_isolate_repository_and_write_passing_code_rules_gate(tmp_path, monkeypatch)
|
|
692
744
|
stub_gate_path = _write_code_review_gate_stub(
|
|
693
745
|
tmp_path, repr(CODE_REVIEW_STUB_BLOCK_REASON)
|
|
694
746
|
)
|
|
@@ -705,7 +757,7 @@ def test_main_allows_when_code_review_gate_returns_none(
|
|
|
705
757
|
tmp_path: Path,
|
|
706
758
|
monkeypatch: pytest.MonkeyPatch,
|
|
707
759
|
) -> None:
|
|
708
|
-
|
|
760
|
+
_isolate_repository_and_write_passing_code_rules_gate(tmp_path, monkeypatch)
|
|
709
761
|
stub_gate_path = _write_code_review_gate_stub(tmp_path, "None")
|
|
710
762
|
monkeypatch.setenv("CODE_REVIEW_PUSH_GATE_PATH", str(stub_gate_path))
|
|
711
763
|
|
|
@@ -718,7 +770,7 @@ def test_main_code_review_check_fails_open_when_gate_module_absent(
|
|
|
718
770
|
tmp_path: Path,
|
|
719
771
|
monkeypatch: pytest.MonkeyPatch,
|
|
720
772
|
) -> None:
|
|
721
|
-
|
|
773
|
+
_isolate_repository_and_write_passing_code_rules_gate(tmp_path, monkeypatch)
|
|
722
774
|
monkeypatch.setenv(
|
|
723
775
|
"CODE_REVIEW_PUSH_GATE_PATH", str(tmp_path / "does_not_exist.py")
|
|
724
776
|
)
|
|
@@ -750,6 +802,800 @@ def test_main_allows_deletion_push_even_when_code_review_gate_would_block(
|
|
|
750
802
|
assert exit_code == 0
|
|
751
803
|
|
|
752
804
|
|
|
805
|
+
DEFAULT_BRANCH_NAME: str = "main"
|
|
806
|
+
FEATURE_BRANCH_NAME: str = "feature/example"
|
|
807
|
+
UNPROTECTED_DEFAULT_BRANCH_NAME: str = "develop"
|
|
808
|
+
ORIGIN_DEFAULT_BRANCH_REFERENCE: str = (
|
|
809
|
+
git_hooks_constants.ORIGIN_REMOTE_TRACKING_REFERENCE_PREFIX + DEFAULT_BRANCH_NAME
|
|
810
|
+
)
|
|
811
|
+
ORIGIN_UNPROTECTED_DEFAULT_BRANCH_REFERENCE: str = (
|
|
812
|
+
git_hooks_constants.ORIGIN_REMOTE_TRACKING_REFERENCE_PREFIX
|
|
813
|
+
+ UNPROTECTED_DEFAULT_BRANCH_NAME
|
|
814
|
+
)
|
|
815
|
+
ORIGIN_HEAD_REFERENCE: str = git_hooks_constants.ORIGIN_HEAD_SYMBOLIC_REFERENCE
|
|
816
|
+
LOCAL_ALIAS_BRANCH_NAME: str = "release-candidate"
|
|
817
|
+
TOPIC_REMOTE_BRANCH_NAME: str = "topic-x"
|
|
818
|
+
BASE_FILE_NAME: str = "README.md"
|
|
819
|
+
FLAGGED_FILE_NAME: str = "flagged_module.py"
|
|
820
|
+
BRANCH_FILE_NAME: str = "branch_module.py"
|
|
821
|
+
DEFAULT_BRANCH_FILE_NAME: str = "default_branch_module.py"
|
|
822
|
+
ALL_FIXTURE_GIT_FLAGS: tuple[str, ...] = (
|
|
823
|
+
"-c",
|
|
824
|
+
"user.name=hooks-test",
|
|
825
|
+
"-c",
|
|
826
|
+
"user.email=hooks-test@example.invalid",
|
|
827
|
+
"-c",
|
|
828
|
+
"commit.gpgsign=false",
|
|
829
|
+
"-c",
|
|
830
|
+
"core.hooksPath=absent_fixture_hooks",
|
|
831
|
+
)
|
|
832
|
+
FIXTURE_GIT_TIMEOUT_SECONDS: int = 60
|
|
833
|
+
|
|
834
|
+
|
|
835
|
+
def _run_fixture_git(repository: Path, *all_arguments: str) -> str:
|
|
836
|
+
"""Run one git command inside a fixture repository, isolated from user hooks."""
|
|
837
|
+
completion = subprocess.run(
|
|
838
|
+
["git", *ALL_FIXTURE_GIT_FLAGS, *all_arguments],
|
|
839
|
+
cwd=str(repository),
|
|
840
|
+
capture_output=True,
|
|
841
|
+
text=True,
|
|
842
|
+
check=True,
|
|
843
|
+
timeout=FIXTURE_GIT_TIMEOUT_SECONDS,
|
|
844
|
+
)
|
|
845
|
+
return completion.stdout.strip()
|
|
846
|
+
|
|
847
|
+
|
|
848
|
+
def _commit_file(repository: Path, file_name: str) -> str:
|
|
849
|
+
"""Write and commit one file, returning the resulting commit object name."""
|
|
850
|
+
(repository / file_name).write_text(f"body of {file_name}\n", encoding="utf-8")
|
|
851
|
+
_run_fixture_git(repository, "add", file_name)
|
|
852
|
+
_run_fixture_git(repository, "commit", "-m", f"add {file_name}")
|
|
853
|
+
return _run_fixture_git(repository, "rev-parse", "HEAD")
|
|
854
|
+
|
|
855
|
+
|
|
856
|
+
def _set_origin_default_branch_tip(
|
|
857
|
+
repository: Path, default_branch_reference: str, default_branch_tip: str
|
|
858
|
+
) -> None:
|
|
859
|
+
"""Point an origin default-branch remote-tracking ref at a commit."""
|
|
860
|
+
_run_fixture_git(
|
|
861
|
+
repository, "update-ref", default_branch_reference, default_branch_tip
|
|
862
|
+
)
|
|
863
|
+
|
|
864
|
+
|
|
865
|
+
def _set_origin_head(repository: Path, default_branch_reference: str) -> None:
|
|
866
|
+
"""Point the origin HEAD symbolic ref at an origin default-branch ref."""
|
|
867
|
+
_run_fixture_git(
|
|
868
|
+
repository, "symbolic-ref", ORIGIN_HEAD_REFERENCE, default_branch_reference
|
|
869
|
+
)
|
|
870
|
+
|
|
871
|
+
|
|
872
|
+
def _push_stdin_line(
|
|
873
|
+
local_branch_name: str,
|
|
874
|
+
pushed_object_name: str,
|
|
875
|
+
remote_branch_name: str,
|
|
876
|
+
remote_object_name: str,
|
|
877
|
+
) -> str:
|
|
878
|
+
"""Build the pre-push stdin line git writes for one branch update."""
|
|
879
|
+
return (
|
|
880
|
+
f"refs/heads/{local_branch_name} {pushed_object_name} "
|
|
881
|
+
f"refs/heads/{remote_branch_name} {remote_object_name}\n"
|
|
882
|
+
)
|
|
883
|
+
|
|
884
|
+
|
|
885
|
+
def _build_rebased_branch_repository(
|
|
886
|
+
tmp_path: Path, default_branch_file_name: str, branch_file_name: str
|
|
887
|
+
) -> tuple[Path, str, str]:
|
|
888
|
+
"""Build a repository whose feature branch sits rebased onto an advanced main.
|
|
889
|
+
|
|
890
|
+
Args:
|
|
891
|
+
tmp_path: The pytest temporary directory the repository is built in.
|
|
892
|
+
default_branch_file_name: File the default branch gains after the branch forks.
|
|
893
|
+
branch_file_name: File the feature branch adds.
|
|
894
|
+
|
|
895
|
+
Returns:
|
|
896
|
+
The repository path, the rebased branch tip, and the pre-rebase tip the
|
|
897
|
+
remote still holds. The origin HEAD symbolic ref stays unset.
|
|
898
|
+
"""
|
|
899
|
+
repository = tmp_path / "rebased_branch_repository"
|
|
900
|
+
repository.mkdir()
|
|
901
|
+
_run_fixture_git(repository, "init", "-b", DEFAULT_BRANCH_NAME)
|
|
902
|
+
_commit_file(repository, BASE_FILE_NAME)
|
|
903
|
+
_run_fixture_git(repository, "checkout", "-b", FEATURE_BRANCH_NAME)
|
|
904
|
+
pre_rebase_tip = _commit_file(repository, branch_file_name)
|
|
905
|
+
_run_fixture_git(repository, "checkout", DEFAULT_BRANCH_NAME)
|
|
906
|
+
_set_origin_default_branch_tip(
|
|
907
|
+
repository,
|
|
908
|
+
ORIGIN_DEFAULT_BRANCH_REFERENCE,
|
|
909
|
+
_commit_file(repository, default_branch_file_name),
|
|
910
|
+
)
|
|
911
|
+
_run_fixture_git(repository, "checkout", FEATURE_BRANCH_NAME)
|
|
912
|
+
_run_fixture_git(repository, "rebase", DEFAULT_BRANCH_NAME)
|
|
913
|
+
return repository, _run_fixture_git(repository, "rev-parse", "HEAD"), pre_rebase_tip
|
|
914
|
+
|
|
915
|
+
|
|
916
|
+
def _write_diff_scoped_gate(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
917
|
+
"""Install a gate that flags the marker file when it sits in the base..HEAD diff."""
|
|
918
|
+
gate_script_path = tmp_path / "diff_scoped_gate.py"
|
|
919
|
+
gate_script_path.write_text(
|
|
920
|
+
"import subprocess\n"
|
|
921
|
+
"import sys\n"
|
|
922
|
+
"base_reference = sys.argv[2]\n"
|
|
923
|
+
"merge_base = subprocess.run(\n"
|
|
924
|
+
" ['git', 'merge-base', 'HEAD', base_reference],\n"
|
|
925
|
+
" capture_output=True, text=True, check=True,\n"
|
|
926
|
+
").stdout.strip()\n"
|
|
927
|
+
"all_changed_names = subprocess.run(\n"
|
|
928
|
+
" ['git', 'diff', '--name-only', merge_base, 'HEAD'],\n"
|
|
929
|
+
" capture_output=True, text=True, check=True,\n"
|
|
930
|
+
").stdout.split()\n"
|
|
931
|
+
f"sys.exit(1 if {FLAGGED_FILE_NAME!r} in all_changed_names else 0)\n",
|
|
932
|
+
encoding="utf-8",
|
|
933
|
+
)
|
|
934
|
+
monkeypatch.setenv("CODE_RULES_GATE_PATH", str(gate_script_path))
|
|
935
|
+
|
|
936
|
+
|
|
937
|
+
def _build_default_branch_repository(tmp_path: Path) -> tuple[Path, str, str]:
|
|
938
|
+
"""Build a repository with an origin default branch and a topic branch off it.
|
|
939
|
+
|
|
940
|
+
Args:
|
|
941
|
+
tmp_path: The pytest temporary directory the repository is built in.
|
|
942
|
+
|
|
943
|
+
Returns:
|
|
944
|
+
The repository path, the default branch tip, and the topic branch tip.
|
|
945
|
+
"""
|
|
946
|
+
repository = tmp_path / "default_branch_repository"
|
|
947
|
+
repository.mkdir()
|
|
948
|
+
_run_fixture_git(repository, "init", "-b", DEFAULT_BRANCH_NAME)
|
|
949
|
+
_commit_file(repository, BASE_FILE_NAME)
|
|
950
|
+
default_branch_tip = _commit_file(repository, DEFAULT_BRANCH_FILE_NAME)
|
|
951
|
+
_run_fixture_git(repository, "checkout", "-b", FEATURE_BRANCH_NAME)
|
|
952
|
+
topic_branch_tip = _commit_file(repository, BRANCH_FILE_NAME)
|
|
953
|
+
_set_origin_default_branch_tip(
|
|
954
|
+
repository, ORIGIN_DEFAULT_BRANCH_REFERENCE, default_branch_tip
|
|
955
|
+
)
|
|
956
|
+
_set_origin_head(repository, ORIGIN_DEFAULT_BRANCH_REFERENCE)
|
|
957
|
+
return repository, default_branch_tip, topic_branch_tip
|
|
958
|
+
|
|
959
|
+
|
|
960
|
+
def _isolate_from_default_branch_refs(
|
|
961
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
962
|
+
) -> None:
|
|
963
|
+
"""Move the test into a fresh repository that holds no origin default branch.
|
|
964
|
+
|
|
965
|
+
Tests that feed synthetic object names need a working directory where no
|
|
966
|
+
default-branch ref resolves, so the hook takes its stdin remote-object
|
|
967
|
+
fallback rather than reading the ambient repository's origin refs.
|
|
968
|
+
"""
|
|
969
|
+
isolated_repository = tmp_path / "repository_without_origin_refs"
|
|
970
|
+
isolated_repository.mkdir()
|
|
971
|
+
_run_fixture_git(isolated_repository, "init", "-b", DEFAULT_BRANCH_NAME)
|
|
972
|
+
monkeypatch.chdir(isolated_repository)
|
|
973
|
+
|
|
974
|
+
|
|
975
|
+
def _build_unrelated_history_repository(tmp_path: Path) -> tuple[Path, str]:
|
|
976
|
+
"""Build a repository whose feature branch shares no history with the default.
|
|
977
|
+
|
|
978
|
+
The origin default-branch ref and the origin HEAD symbolic ref both resolve,
|
|
979
|
+
so ``git merge-base`` runs and reports no merge base rather than the hook
|
|
980
|
+
failing to find a default branch at all.
|
|
981
|
+
|
|
982
|
+
Args:
|
|
983
|
+
tmp_path: The pytest temporary directory the repository is built in.
|
|
984
|
+
|
|
985
|
+
Returns:
|
|
986
|
+
The repository path and the orphan feature branch tip.
|
|
987
|
+
"""
|
|
988
|
+
repository = tmp_path / "unrelated_history_repository"
|
|
989
|
+
repository.mkdir()
|
|
990
|
+
_run_fixture_git(repository, "init", "-b", DEFAULT_BRANCH_NAME)
|
|
991
|
+
_commit_file(repository, BASE_FILE_NAME)
|
|
992
|
+
default_branch_tip = _commit_file(repository, DEFAULT_BRANCH_FILE_NAME)
|
|
993
|
+
_run_fixture_git(repository, "checkout", "--orphan", FEATURE_BRANCH_NAME)
|
|
994
|
+
_run_fixture_git(repository, "reset")
|
|
995
|
+
orphan_branch_tip = _commit_file(repository, BRANCH_FILE_NAME)
|
|
996
|
+
_set_origin_default_branch_tip(
|
|
997
|
+
repository, ORIGIN_DEFAULT_BRANCH_REFERENCE, default_branch_tip
|
|
998
|
+
)
|
|
999
|
+
_set_origin_head(repository, ORIGIN_DEFAULT_BRANCH_REFERENCE)
|
|
1000
|
+
return repository, orphan_branch_tip
|
|
1001
|
+
|
|
1002
|
+
|
|
1003
|
+
def _same_branch_push_line(
|
|
1004
|
+
local_object_name: str, remote_object_name: str
|
|
1005
|
+
) -> pre_push.PushLine:
|
|
1006
|
+
"""Build a PushLine whose local and remote refs name the same branch."""
|
|
1007
|
+
return pre_push.PushLine(
|
|
1008
|
+
local_branch_name=FEATURE_BRANCH_NAME,
|
|
1009
|
+
local_object_name=local_object_name,
|
|
1010
|
+
remote_branch_name=FEATURE_BRANCH_NAME,
|
|
1011
|
+
remote_object_name=remote_object_name,
|
|
1012
|
+
)
|
|
1013
|
+
|
|
1014
|
+
|
|
1015
|
+
def test_parse_push_line_reads_both_branch_names_and_both_object_names() -> None:
|
|
1016
|
+
parsed_line = pre_push.parse_push_line(
|
|
1017
|
+
f"refs/heads/{FEATURE_BRANCH_NAME} {NON_ZERO_LOCAL_SHA} "
|
|
1018
|
+
f"refs/heads/{DEFAULT_BRANCH_NAME} {NON_ZERO_REMOTE_SHA_ONE}"
|
|
1019
|
+
)
|
|
1020
|
+
|
|
1021
|
+
assert parsed_line == pre_push.PushLine(
|
|
1022
|
+
local_branch_name=FEATURE_BRANCH_NAME,
|
|
1023
|
+
local_object_name=NON_ZERO_LOCAL_SHA,
|
|
1024
|
+
remote_branch_name=DEFAULT_BRANCH_NAME,
|
|
1025
|
+
remote_object_name=NON_ZERO_REMOTE_SHA_ONE,
|
|
1026
|
+
)
|
|
1027
|
+
|
|
1028
|
+
|
|
1029
|
+
def test_parse_push_line_returns_none_for_a_line_missing_fields() -> None:
|
|
1030
|
+
assert pre_push.parse_push_line("refs/heads/feature one_more_field") is None
|
|
1031
|
+
|
|
1032
|
+
|
|
1033
|
+
def test_is_branch_update_separates_updates_from_deletions_and_new_branches() -> None:
|
|
1034
|
+
assert pre_push.is_branch_update(
|
|
1035
|
+
_same_branch_push_line(NON_ZERO_LOCAL_SHA, NON_ZERO_REMOTE_SHA_ONE)
|
|
1036
|
+
)
|
|
1037
|
+
assert not pre_push.is_branch_update(
|
|
1038
|
+
_same_branch_push_line(ALL_ZEROS_OBJECT_NAME, NON_ZERO_REMOTE_SHA_ONE)
|
|
1039
|
+
)
|
|
1040
|
+
assert not pre_push.is_branch_update(
|
|
1041
|
+
_same_branch_push_line(NON_ZERO_LOCAL_SHA, ALL_ZEROS_OBJECT_NAME)
|
|
1042
|
+
)
|
|
1043
|
+
|
|
1044
|
+
|
|
1045
|
+
def test_run_git_reference_query_returns_stdout_for_a_resolvable_reference(
|
|
1046
|
+
tmp_path: Path,
|
|
1047
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
1048
|
+
) -> None:
|
|
1049
|
+
repository, default_branch_tip, _topic_branch_tip = _build_default_branch_repository(
|
|
1050
|
+
tmp_path
|
|
1051
|
+
)
|
|
1052
|
+
monkeypatch.chdir(repository)
|
|
1053
|
+
|
|
1054
|
+
resolved_object_name = pre_push.run_git_reference_query(
|
|
1055
|
+
("git", "rev-parse", ORIGIN_DEFAULT_BRANCH_REFERENCE)
|
|
1056
|
+
)
|
|
1057
|
+
|
|
1058
|
+
assert resolved_object_name == default_branch_tip
|
|
1059
|
+
|
|
1060
|
+
|
|
1061
|
+
def test_run_git_reference_query_returns_none_for_an_absent_reference(
|
|
1062
|
+
tmp_path: Path,
|
|
1063
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
1064
|
+
) -> None:
|
|
1065
|
+
repository, _default_branch_tip, _topic_branch_tip = (
|
|
1066
|
+
_build_default_branch_repository(tmp_path)
|
|
1067
|
+
)
|
|
1068
|
+
monkeypatch.chdir(repository)
|
|
1069
|
+
|
|
1070
|
+
resolved_object_name = pre_push.run_git_reference_query(
|
|
1071
|
+
("git", "rev-parse", "--verify", "--quiet", "refs/remotes/origin/absent")
|
|
1072
|
+
)
|
|
1073
|
+
|
|
1074
|
+
assert resolved_object_name is None
|
|
1075
|
+
|
|
1076
|
+
|
|
1077
|
+
def test_resolve_default_branch_reference_reads_the_origin_head_symbolic_reference(
|
|
1078
|
+
tmp_path: Path,
|
|
1079
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
1080
|
+
) -> None:
|
|
1081
|
+
repository, _default_branch_tip, _topic_branch_tip = (
|
|
1082
|
+
_build_default_branch_repository(tmp_path)
|
|
1083
|
+
)
|
|
1084
|
+
monkeypatch.chdir(repository)
|
|
1085
|
+
|
|
1086
|
+
assert pre_push.resolve_default_branch_reference() == ORIGIN_DEFAULT_BRANCH_REFERENCE
|
|
1087
|
+
|
|
1088
|
+
|
|
1089
|
+
def test_resolve_default_branch_reference_returns_none_without_remote_tracking_refs(
|
|
1090
|
+
tmp_path: Path,
|
|
1091
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
1092
|
+
) -> None:
|
|
1093
|
+
repository = tmp_path / "repository_without_remote_tracking_refs"
|
|
1094
|
+
repository.mkdir()
|
|
1095
|
+
_run_fixture_git(repository, "init", "-b", DEFAULT_BRANCH_NAME)
|
|
1096
|
+
_commit_file(repository, BASE_FILE_NAME)
|
|
1097
|
+
monkeypatch.chdir(repository)
|
|
1098
|
+
|
|
1099
|
+
assert pre_push.resolve_default_branch_reference() is None
|
|
1100
|
+
|
|
1101
|
+
|
|
1102
|
+
def test_find_branch_update_push_returns_the_update_that_follows_a_deletion_line() -> (
|
|
1103
|
+
None
|
|
1104
|
+
):
|
|
1105
|
+
stdin_text = (
|
|
1106
|
+
f"(delete) {ALL_ZEROS_OBJECT_NAME} refs/heads/retired {NON_ZERO_REMOTE_SHA_ONE}\n"
|
|
1107
|
+
+ _push_stdin_line(
|
|
1108
|
+
FEATURE_BRANCH_NAME,
|
|
1109
|
+
NON_ZERO_LOCAL_SHA,
|
|
1110
|
+
DEFAULT_BRANCH_NAME,
|
|
1111
|
+
NON_ZERO_REMOTE_SHA_TWO,
|
|
1112
|
+
)
|
|
1113
|
+
)
|
|
1114
|
+
|
|
1115
|
+
branch_update = pre_push.find_branch_update_push(stdin_text)
|
|
1116
|
+
|
|
1117
|
+
assert branch_update == pre_push.PushLine(
|
|
1118
|
+
local_branch_name=FEATURE_BRANCH_NAME,
|
|
1119
|
+
local_object_name=NON_ZERO_LOCAL_SHA,
|
|
1120
|
+
remote_branch_name=DEFAULT_BRANCH_NAME,
|
|
1121
|
+
remote_object_name=NON_ZERO_REMOTE_SHA_TWO,
|
|
1122
|
+
)
|
|
1123
|
+
|
|
1124
|
+
|
|
1125
|
+
def test_find_branch_update_push_returns_none_when_every_line_is_a_deletion() -> None:
|
|
1126
|
+
deletion_only_stdin = _push_stdin_line(
|
|
1127
|
+
FEATURE_BRANCH_NAME,
|
|
1128
|
+
ALL_ZEROS_OBJECT_NAME,
|
|
1129
|
+
FEATURE_BRANCH_NAME,
|
|
1130
|
+
NON_ZERO_REMOTE_SHA_ONE,
|
|
1131
|
+
)
|
|
1132
|
+
|
|
1133
|
+
assert pre_push.find_branch_update_push(deletion_only_stdin) is None
|
|
1134
|
+
|
|
1135
|
+
|
|
1136
|
+
def test_resolve_default_branch_merge_base_returns_the_fork_point(
|
|
1137
|
+
tmp_path: Path,
|
|
1138
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
1139
|
+
) -> None:
|
|
1140
|
+
repository, default_branch_tip, topic_branch_tip = _build_default_branch_repository(
|
|
1141
|
+
tmp_path
|
|
1142
|
+
)
|
|
1143
|
+
monkeypatch.chdir(repository)
|
|
1144
|
+
|
|
1145
|
+
merge_base_object_name = pre_push.resolve_default_branch_merge_base(
|
|
1146
|
+
FEATURE_BRANCH_NAME, topic_branch_tip
|
|
1147
|
+
)
|
|
1148
|
+
|
|
1149
|
+
assert merge_base_object_name == default_branch_tip
|
|
1150
|
+
|
|
1151
|
+
|
|
1152
|
+
def test_resolve_default_branch_merge_base_returns_none_for_a_default_branch_update(
|
|
1153
|
+
tmp_path: Path,
|
|
1154
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
1155
|
+
) -> None:
|
|
1156
|
+
repository, _default_branch_tip, topic_branch_tip = (
|
|
1157
|
+
_build_default_branch_repository(tmp_path)
|
|
1158
|
+
)
|
|
1159
|
+
monkeypatch.chdir(repository)
|
|
1160
|
+
|
|
1161
|
+
merge_base_object_name = pre_push.resolve_default_branch_merge_base(
|
|
1162
|
+
DEFAULT_BRANCH_NAME, topic_branch_tip
|
|
1163
|
+
)
|
|
1164
|
+
|
|
1165
|
+
assert merge_base_object_name is None
|
|
1166
|
+
|
|
1167
|
+
|
|
1168
|
+
def test_resolve_gate_base_reference_returns_the_default_branch_merge_base(
|
|
1169
|
+
tmp_path: Path,
|
|
1170
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
1171
|
+
) -> None:
|
|
1172
|
+
repository, default_branch_tip, topic_branch_tip = _build_default_branch_repository(
|
|
1173
|
+
tmp_path
|
|
1174
|
+
)
|
|
1175
|
+
monkeypatch.chdir(repository)
|
|
1176
|
+
stdin_text = _push_stdin_line(
|
|
1177
|
+
FEATURE_BRANCH_NAME,
|
|
1178
|
+
topic_branch_tip,
|
|
1179
|
+
FEATURE_BRANCH_NAME,
|
|
1180
|
+
NON_ZERO_REMOTE_SHA_ONE,
|
|
1181
|
+
)
|
|
1182
|
+
|
|
1183
|
+
assert pre_push.resolve_gate_base_reference(stdin_text) == default_branch_tip
|
|
1184
|
+
|
|
1185
|
+
|
|
1186
|
+
def test_resolve_gate_base_reference_returns_none_for_a_deletion_only_push() -> None:
|
|
1187
|
+
deletion_only_stdin = _push_stdin_line(
|
|
1188
|
+
FEATURE_BRANCH_NAME,
|
|
1189
|
+
ALL_ZEROS_OBJECT_NAME,
|
|
1190
|
+
FEATURE_BRANCH_NAME,
|
|
1191
|
+
ALL_ZEROS_OBJECT_NAME,
|
|
1192
|
+
)
|
|
1193
|
+
|
|
1194
|
+
assert pre_push.resolve_gate_base_reference(deletion_only_stdin) is None
|
|
1195
|
+
|
|
1196
|
+
|
|
1197
|
+
def test_resolve_gate_base_reference_flags_an_unresolvable_merge_base(
|
|
1198
|
+
tmp_path: Path,
|
|
1199
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
1200
|
+
) -> None:
|
|
1201
|
+
repository, orphan_branch_tip = _build_unrelated_history_repository(tmp_path)
|
|
1202
|
+
monkeypatch.chdir(repository)
|
|
1203
|
+
assert (
|
|
1204
|
+
pre_push.run_git_reference_query(
|
|
1205
|
+
("git", "merge-base", orphan_branch_tip, ORIGIN_DEFAULT_BRANCH_REFERENCE)
|
|
1206
|
+
)
|
|
1207
|
+
is None
|
|
1208
|
+
), "fixture branch shares history with the default branch"
|
|
1209
|
+
stdin_text = _push_stdin_line(
|
|
1210
|
+
FEATURE_BRANCH_NAME,
|
|
1211
|
+
orphan_branch_tip,
|
|
1212
|
+
FEATURE_BRANCH_NAME,
|
|
1213
|
+
NON_ZERO_REMOTE_SHA_ONE,
|
|
1214
|
+
)
|
|
1215
|
+
|
|
1216
|
+
base_reference = pre_push.resolve_gate_base_reference(stdin_text)
|
|
1217
|
+
|
|
1218
|
+
assert base_reference == git_hooks_constants.UNRESOLVABLE_MERGE_BASE_SENTINEL
|
|
1219
|
+
|
|
1220
|
+
|
|
1221
|
+
def test_resolve_gate_base_reference_treats_a_dangling_origin_head_as_no_default(
|
|
1222
|
+
tmp_path: Path,
|
|
1223
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
1224
|
+
) -> None:
|
|
1225
|
+
"""A symbolic origin/HEAD whose target is missing falls through to no default branch ref.
|
|
1226
|
+
|
|
1227
|
+
The gate base then uses the stdin remote object, the same path as when no
|
|
1228
|
+
default-branch ref resolves at all.
|
|
1229
|
+
"""
|
|
1230
|
+
repository = tmp_path / "dangling_origin_head_repository"
|
|
1231
|
+
repository.mkdir()
|
|
1232
|
+
_run_fixture_git(repository, "init", "-b", DEFAULT_BRANCH_NAME)
|
|
1233
|
+
pushed_object_name = _commit_file(repository, BASE_FILE_NAME)
|
|
1234
|
+
_set_origin_head(repository, ORIGIN_DEFAULT_BRANCH_REFERENCE)
|
|
1235
|
+
monkeypatch.chdir(repository)
|
|
1236
|
+
stdin_text = _push_stdin_line(
|
|
1237
|
+
FEATURE_BRANCH_NAME,
|
|
1238
|
+
pushed_object_name,
|
|
1239
|
+
FEATURE_BRANCH_NAME,
|
|
1240
|
+
NON_ZERO_REMOTE_SHA_ONE,
|
|
1241
|
+
)
|
|
1242
|
+
|
|
1243
|
+
base_reference = pre_push.resolve_gate_base_reference(stdin_text)
|
|
1244
|
+
|
|
1245
|
+
assert base_reference == NON_ZERO_REMOTE_SHA_ONE
|
|
1246
|
+
|
|
1247
|
+
|
|
1248
|
+
def test_main_allows_the_push_when_origin_head_names_an_absent_reference(
|
|
1249
|
+
tmp_path: Path,
|
|
1250
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
1251
|
+
) -> None:
|
|
1252
|
+
"""Dangling origin/HEAD does not skip the gate; it uses the remote object base."""
|
|
1253
|
+
repository = tmp_path / "dangling_origin_head_push_repository"
|
|
1254
|
+
repository.mkdir()
|
|
1255
|
+
_run_fixture_git(repository, "init", "-b", DEFAULT_BRANCH_NAME)
|
|
1256
|
+
remote_object_name = _commit_file(repository, BASE_FILE_NAME)
|
|
1257
|
+
_run_fixture_git(repository, "checkout", "-b", FEATURE_BRANCH_NAME)
|
|
1258
|
+
pushed_object_name = _commit_file(repository, BRANCH_FILE_NAME)
|
|
1259
|
+
_set_origin_head(repository, ORIGIN_DEFAULT_BRANCH_REFERENCE)
|
|
1260
|
+
recorded_arguments_path = tmp_path / "recorded_arguments_dangling.txt"
|
|
1261
|
+
recording_gate_script_path = tmp_path / "recording_gate_dangling.py"
|
|
1262
|
+
recording_gate_script_path.write_text(
|
|
1263
|
+
"import sys, pathlib\n"
|
|
1264
|
+
f'pathlib.Path(r"{recorded_arguments_path}").write_text('
|
|
1265
|
+
"'\\n'.join(sys.argv[1:]), encoding='utf-8')\n"
|
|
1266
|
+
"sys.exit(0)\n",
|
|
1267
|
+
encoding="utf-8",
|
|
1268
|
+
)
|
|
1269
|
+
monkeypatch.setenv("CODE_RULES_GATE_PATH", str(recording_gate_script_path))
|
|
1270
|
+
_isolate_code_review_gate(tmp_path, monkeypatch)
|
|
1271
|
+
monkeypatch.chdir(repository)
|
|
1272
|
+
monkeypatch.setattr(
|
|
1273
|
+
sys,
|
|
1274
|
+
"stdin",
|
|
1275
|
+
io.StringIO(
|
|
1276
|
+
_push_stdin_line(
|
|
1277
|
+
FEATURE_BRANCH_NAME,
|
|
1278
|
+
pushed_object_name,
|
|
1279
|
+
FEATURE_BRANCH_NAME,
|
|
1280
|
+
remote_object_name,
|
|
1281
|
+
)
|
|
1282
|
+
),
|
|
1283
|
+
)
|
|
1284
|
+
|
|
1285
|
+
exit_code = pre_push.main()
|
|
1286
|
+
|
|
1287
|
+
assert exit_code == 0
|
|
1288
|
+
recorded_arguments = recorded_arguments_path.read_text(
|
|
1289
|
+
encoding="utf-8"
|
|
1290
|
+
).splitlines()
|
|
1291
|
+
assert recorded_arguments == ["--base", remote_object_name]
|
|
1292
|
+
|
|
1293
|
+
|
|
1294
|
+
def test_main_skips_the_gate_when_no_merge_base_resolves(
|
|
1295
|
+
tmp_path: Path,
|
|
1296
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
1297
|
+
capsys: pytest.CaptureFixture[str],
|
|
1298
|
+
) -> None:
|
|
1299
|
+
repository, orphan_branch_tip = _build_unrelated_history_repository(tmp_path)
|
|
1300
|
+
recorded_arguments_path = tmp_path / "unreached_gate_arguments.txt"
|
|
1301
|
+
recording_gate_script_path = tmp_path / "unreached_recording_gate.py"
|
|
1302
|
+
recording_gate_script_path.write_text(
|
|
1303
|
+
"import sys, pathlib\n"
|
|
1304
|
+
f'pathlib.Path(r"{recorded_arguments_path}").write_text('
|
|
1305
|
+
"'\\n'.join(sys.argv[1:]), encoding='utf-8')\n"
|
|
1306
|
+
"sys.exit(0)\n",
|
|
1307
|
+
encoding="utf-8",
|
|
1308
|
+
)
|
|
1309
|
+
monkeypatch.setenv("CODE_RULES_GATE_PATH", str(recording_gate_script_path))
|
|
1310
|
+
_isolate_code_review_gate(tmp_path, monkeypatch)
|
|
1311
|
+
monkeypatch.chdir(repository)
|
|
1312
|
+
monkeypatch.setattr(
|
|
1313
|
+
sys,
|
|
1314
|
+
"stdin",
|
|
1315
|
+
io.StringIO(
|
|
1316
|
+
_push_stdin_line(
|
|
1317
|
+
FEATURE_BRANCH_NAME,
|
|
1318
|
+
orphan_branch_tip,
|
|
1319
|
+
FEATURE_BRANCH_NAME,
|
|
1320
|
+
NON_ZERO_REMOTE_SHA_ONE,
|
|
1321
|
+
)
|
|
1322
|
+
),
|
|
1323
|
+
)
|
|
1324
|
+
|
|
1325
|
+
exit_code = pre_push.main()
|
|
1326
|
+
|
|
1327
|
+
assert exit_code == 0
|
|
1328
|
+
assert not recorded_arguments_path.exists(), (
|
|
1329
|
+
"the gate ran on a base the hook could not verify"
|
|
1330
|
+
)
|
|
1331
|
+
assert git_hooks_constants.UNRESOLVABLE_MERGE_BASE_MESSAGE in capsys.readouterr().err
|
|
1332
|
+
|
|
1333
|
+
|
|
1334
|
+
def test_main_allows_default_branch_file_the_rebase_replayed_onto(
|
|
1335
|
+
tmp_path: Path,
|
|
1336
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
1337
|
+
) -> None:
|
|
1338
|
+
repository, rebased_tip, pre_rebase_tip = _build_rebased_branch_repository(
|
|
1339
|
+
tmp_path,
|
|
1340
|
+
default_branch_file_name=FLAGGED_FILE_NAME,
|
|
1341
|
+
branch_file_name=BRANCH_FILE_NAME,
|
|
1342
|
+
)
|
|
1343
|
+
_set_origin_head(repository, ORIGIN_DEFAULT_BRANCH_REFERENCE)
|
|
1344
|
+
_write_diff_scoped_gate(tmp_path, monkeypatch)
|
|
1345
|
+
_isolate_code_review_gate(tmp_path, monkeypatch)
|
|
1346
|
+
monkeypatch.chdir(repository)
|
|
1347
|
+
monkeypatch.setattr(
|
|
1348
|
+
sys,
|
|
1349
|
+
"stdin",
|
|
1350
|
+
io.StringIO(
|
|
1351
|
+
_push_stdin_line(
|
|
1352
|
+
FEATURE_BRANCH_NAME, rebased_tip, FEATURE_BRANCH_NAME, pre_rebase_tip
|
|
1353
|
+
)
|
|
1354
|
+
),
|
|
1355
|
+
)
|
|
1356
|
+
|
|
1357
|
+
exit_code = pre_push.main()
|
|
1358
|
+
|
|
1359
|
+
assert exit_code == 0
|
|
1360
|
+
|
|
1361
|
+
|
|
1362
|
+
def test_main_blocks_violation_the_branch_itself_adds(
|
|
1363
|
+
tmp_path: Path,
|
|
1364
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
1365
|
+
) -> None:
|
|
1366
|
+
repository, rebased_tip, pre_rebase_tip = _build_rebased_branch_repository(
|
|
1367
|
+
tmp_path,
|
|
1368
|
+
default_branch_file_name=DEFAULT_BRANCH_FILE_NAME,
|
|
1369
|
+
branch_file_name=FLAGGED_FILE_NAME,
|
|
1370
|
+
)
|
|
1371
|
+
_set_origin_head(repository, ORIGIN_DEFAULT_BRANCH_REFERENCE)
|
|
1372
|
+
_write_diff_scoped_gate(tmp_path, monkeypatch)
|
|
1373
|
+
_isolate_code_review_gate(tmp_path, monkeypatch)
|
|
1374
|
+
monkeypatch.chdir(repository)
|
|
1375
|
+
monkeypatch.setattr(
|
|
1376
|
+
sys,
|
|
1377
|
+
"stdin",
|
|
1378
|
+
io.StringIO(
|
|
1379
|
+
_push_stdin_line(
|
|
1380
|
+
FEATURE_BRANCH_NAME, rebased_tip, FEATURE_BRANCH_NAME, pre_rebase_tip
|
|
1381
|
+
)
|
|
1382
|
+
),
|
|
1383
|
+
)
|
|
1384
|
+
|
|
1385
|
+
exit_code = pre_push.main()
|
|
1386
|
+
|
|
1387
|
+
assert exit_code == 1
|
|
1388
|
+
|
|
1389
|
+
|
|
1390
|
+
def test_main_uses_remote_object_when_no_default_branch_reference_resolves(
|
|
1391
|
+
tmp_path: Path,
|
|
1392
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
1393
|
+
) -> None:
|
|
1394
|
+
repository = tmp_path / "repository_without_remote"
|
|
1395
|
+
repository.mkdir()
|
|
1396
|
+
_run_fixture_git(repository, "init", "-b", DEFAULT_BRANCH_NAME)
|
|
1397
|
+
remote_object_name = _commit_file(repository, BASE_FILE_NAME)
|
|
1398
|
+
_run_fixture_git(repository, "checkout", "-b", FEATURE_BRANCH_NAME)
|
|
1399
|
+
pushed_object_name = _commit_file(repository, BRANCH_FILE_NAME)
|
|
1400
|
+
recorded_arguments_path = tmp_path / "recorded_arguments.txt"
|
|
1401
|
+
recording_gate_script_path = tmp_path / "recording_gate.py"
|
|
1402
|
+
recording_gate_script_path.write_text(
|
|
1403
|
+
"import sys, pathlib\n"
|
|
1404
|
+
f'pathlib.Path(r"{recorded_arguments_path}").write_text('
|
|
1405
|
+
"'\\n'.join(sys.argv[1:]), encoding='utf-8')\n"
|
|
1406
|
+
"sys.exit(0)\n",
|
|
1407
|
+
encoding="utf-8",
|
|
1408
|
+
)
|
|
1409
|
+
monkeypatch.setenv("CODE_RULES_GATE_PATH", str(recording_gate_script_path))
|
|
1410
|
+
_isolate_code_review_gate(tmp_path, monkeypatch)
|
|
1411
|
+
monkeypatch.chdir(repository)
|
|
1412
|
+
monkeypatch.setattr(
|
|
1413
|
+
sys,
|
|
1414
|
+
"stdin",
|
|
1415
|
+
io.StringIO(
|
|
1416
|
+
_push_stdin_line(
|
|
1417
|
+
FEATURE_BRANCH_NAME,
|
|
1418
|
+
pushed_object_name,
|
|
1419
|
+
FEATURE_BRANCH_NAME,
|
|
1420
|
+
remote_object_name,
|
|
1421
|
+
)
|
|
1422
|
+
),
|
|
1423
|
+
)
|
|
1424
|
+
|
|
1425
|
+
exit_code = pre_push.main()
|
|
1426
|
+
|
|
1427
|
+
assert exit_code == 0
|
|
1428
|
+
recorded_arguments = recorded_arguments_path.read_text(
|
|
1429
|
+
encoding="utf-8"
|
|
1430
|
+
).splitlines()
|
|
1431
|
+
assert recorded_arguments == ["--base", remote_object_name]
|
|
1432
|
+
|
|
1433
|
+
|
|
1434
|
+
def test_main_uses_remote_object_when_the_default_branch_is_pushed(
|
|
1435
|
+
tmp_path: Path,
|
|
1436
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
1437
|
+
) -> None:
|
|
1438
|
+
repository = tmp_path / "stale_tracking_repository"
|
|
1439
|
+
repository.mkdir()
|
|
1440
|
+
_run_fixture_git(repository, "init", "-b", DEFAULT_BRANCH_NAME)
|
|
1441
|
+
stale_tracking_tip = _commit_file(repository, BASE_FILE_NAME)
|
|
1442
|
+
remote_object_name = _commit_file(repository, FLAGGED_FILE_NAME)
|
|
1443
|
+
pushed_object_name = _commit_file(repository, DEFAULT_BRANCH_FILE_NAME)
|
|
1444
|
+
_set_origin_default_branch_tip(
|
|
1445
|
+
repository, ORIGIN_DEFAULT_BRANCH_REFERENCE, stale_tracking_tip
|
|
1446
|
+
)
|
|
1447
|
+
_set_origin_head(repository, ORIGIN_DEFAULT_BRANCH_REFERENCE)
|
|
1448
|
+
_write_diff_scoped_gate(tmp_path, monkeypatch)
|
|
1449
|
+
_isolate_code_review_gate(tmp_path, monkeypatch)
|
|
1450
|
+
monkeypatch.chdir(repository)
|
|
1451
|
+
monkeypatch.setattr(
|
|
1452
|
+
sys,
|
|
1453
|
+
"stdin",
|
|
1454
|
+
io.StringIO(
|
|
1455
|
+
_push_stdin_line(
|
|
1456
|
+
DEFAULT_BRANCH_NAME,
|
|
1457
|
+
pushed_object_name,
|
|
1458
|
+
DEFAULT_BRANCH_NAME,
|
|
1459
|
+
remote_object_name,
|
|
1460
|
+
)
|
|
1461
|
+
),
|
|
1462
|
+
)
|
|
1463
|
+
|
|
1464
|
+
exit_code = pre_push.main()
|
|
1465
|
+
|
|
1466
|
+
assert exit_code == 0
|
|
1467
|
+
|
|
1468
|
+
|
|
1469
|
+
def test_main_uses_merge_base_when_a_default_named_local_branch_updates_a_topic_branch(
|
|
1470
|
+
tmp_path: Path,
|
|
1471
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
1472
|
+
) -> None:
|
|
1473
|
+
repository = tmp_path / "default_named_local_branch_repository"
|
|
1474
|
+
repository.mkdir()
|
|
1475
|
+
_run_fixture_git(repository, "init", "-b", UNPROTECTED_DEFAULT_BRANCH_NAME)
|
|
1476
|
+
remote_object_name = _commit_file(repository, BASE_FILE_NAME)
|
|
1477
|
+
default_branch_tip = _commit_file(repository, FLAGGED_FILE_NAME)
|
|
1478
|
+
pushed_object_name = _commit_file(repository, BRANCH_FILE_NAME)
|
|
1479
|
+
_set_origin_default_branch_tip(
|
|
1480
|
+
repository, ORIGIN_UNPROTECTED_DEFAULT_BRANCH_REFERENCE, default_branch_tip
|
|
1481
|
+
)
|
|
1482
|
+
_set_origin_head(repository, ORIGIN_UNPROTECTED_DEFAULT_BRANCH_REFERENCE)
|
|
1483
|
+
_write_diff_scoped_gate(tmp_path, monkeypatch)
|
|
1484
|
+
_isolate_code_review_gate(tmp_path, monkeypatch)
|
|
1485
|
+
monkeypatch.chdir(repository)
|
|
1486
|
+
monkeypatch.setattr(
|
|
1487
|
+
sys,
|
|
1488
|
+
"stdin",
|
|
1489
|
+
io.StringIO(
|
|
1490
|
+
_push_stdin_line(
|
|
1491
|
+
UNPROTECTED_DEFAULT_BRANCH_NAME,
|
|
1492
|
+
pushed_object_name,
|
|
1493
|
+
TOPIC_REMOTE_BRANCH_NAME,
|
|
1494
|
+
remote_object_name,
|
|
1495
|
+
)
|
|
1496
|
+
),
|
|
1497
|
+
)
|
|
1498
|
+
|
|
1499
|
+
exit_code = pre_push.main()
|
|
1500
|
+
|
|
1501
|
+
assert exit_code == 0
|
|
1502
|
+
|
|
1503
|
+
|
|
1504
|
+
def test_main_uses_remote_object_when_a_renamed_local_branch_updates_the_default_branch(
|
|
1505
|
+
tmp_path: Path,
|
|
1506
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
1507
|
+
) -> None:
|
|
1508
|
+
repository = tmp_path / "renamed_local_branch_repository"
|
|
1509
|
+
repository.mkdir()
|
|
1510
|
+
_run_fixture_git(repository, "init", "-b", UNPROTECTED_DEFAULT_BRANCH_NAME)
|
|
1511
|
+
stale_tracking_tip = _commit_file(repository, BASE_FILE_NAME)
|
|
1512
|
+
remote_object_name = _commit_file(repository, FLAGGED_FILE_NAME)
|
|
1513
|
+
pushed_object_name = _commit_file(repository, DEFAULT_BRANCH_FILE_NAME)
|
|
1514
|
+
_run_fixture_git(repository, "checkout", "-b", LOCAL_ALIAS_BRANCH_NAME)
|
|
1515
|
+
_set_origin_default_branch_tip(
|
|
1516
|
+
repository, ORIGIN_UNPROTECTED_DEFAULT_BRANCH_REFERENCE, stale_tracking_tip
|
|
1517
|
+
)
|
|
1518
|
+
_set_origin_head(repository, ORIGIN_UNPROTECTED_DEFAULT_BRANCH_REFERENCE)
|
|
1519
|
+
_write_diff_scoped_gate(tmp_path, monkeypatch)
|
|
1520
|
+
_isolate_code_review_gate(tmp_path, monkeypatch)
|
|
1521
|
+
monkeypatch.chdir(repository)
|
|
1522
|
+
monkeypatch.setattr(
|
|
1523
|
+
sys,
|
|
1524
|
+
"stdin",
|
|
1525
|
+
io.StringIO(
|
|
1526
|
+
_push_stdin_line(
|
|
1527
|
+
LOCAL_ALIAS_BRANCH_NAME,
|
|
1528
|
+
pushed_object_name,
|
|
1529
|
+
UNPROTECTED_DEFAULT_BRANCH_NAME,
|
|
1530
|
+
remote_object_name,
|
|
1531
|
+
)
|
|
1532
|
+
),
|
|
1533
|
+
)
|
|
1534
|
+
|
|
1535
|
+
exit_code = pre_push.main()
|
|
1536
|
+
|
|
1537
|
+
assert exit_code == 0
|
|
1538
|
+
|
|
1539
|
+
|
|
1540
|
+
def test_main_reads_default_branch_tracking_reference_when_origin_head_is_unset(
|
|
1541
|
+
tmp_path: Path,
|
|
1542
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
1543
|
+
) -> None:
|
|
1544
|
+
repository, rebased_tip, pre_rebase_tip = _build_rebased_branch_repository(
|
|
1545
|
+
tmp_path,
|
|
1546
|
+
default_branch_file_name=FLAGGED_FILE_NAME,
|
|
1547
|
+
branch_file_name=BRANCH_FILE_NAME,
|
|
1548
|
+
)
|
|
1549
|
+
_write_diff_scoped_gate(tmp_path, monkeypatch)
|
|
1550
|
+
_isolate_code_review_gate(tmp_path, monkeypatch)
|
|
1551
|
+
monkeypatch.chdir(repository)
|
|
1552
|
+
monkeypatch.setattr(
|
|
1553
|
+
sys,
|
|
1554
|
+
"stdin",
|
|
1555
|
+
io.StringIO(
|
|
1556
|
+
_push_stdin_line(
|
|
1557
|
+
FEATURE_BRANCH_NAME, rebased_tip, FEATURE_BRANCH_NAME, pre_rebase_tip
|
|
1558
|
+
)
|
|
1559
|
+
),
|
|
1560
|
+
)
|
|
1561
|
+
|
|
1562
|
+
exit_code = pre_push.main()
|
|
1563
|
+
|
|
1564
|
+
assert exit_code == 0
|
|
1565
|
+
|
|
1566
|
+
|
|
1567
|
+
def test_main_skips_a_deletion_line_to_reach_the_branch_update(
|
|
1568
|
+
tmp_path: Path,
|
|
1569
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
1570
|
+
) -> None:
|
|
1571
|
+
repository, rebased_tip, pre_rebase_tip = _build_rebased_branch_repository(
|
|
1572
|
+
tmp_path,
|
|
1573
|
+
default_branch_file_name=FLAGGED_FILE_NAME,
|
|
1574
|
+
branch_file_name=BRANCH_FILE_NAME,
|
|
1575
|
+
)
|
|
1576
|
+
_set_origin_head(repository, ORIGIN_DEFAULT_BRANCH_REFERENCE)
|
|
1577
|
+
_write_diff_scoped_gate(tmp_path, monkeypatch)
|
|
1578
|
+
_isolate_code_review_gate(tmp_path, monkeypatch)
|
|
1579
|
+
monkeypatch.chdir(repository)
|
|
1580
|
+
deletion_line = (
|
|
1581
|
+
f"(delete) {ALL_ZEROS_OBJECT_NAME} refs/heads/retired {NON_ZERO_REMOTE_SHA_ONE}\n"
|
|
1582
|
+
)
|
|
1583
|
+
monkeypatch.setattr(
|
|
1584
|
+
sys,
|
|
1585
|
+
"stdin",
|
|
1586
|
+
io.StringIO(
|
|
1587
|
+
deletion_line
|
|
1588
|
+
+ _push_stdin_line(
|
|
1589
|
+
FEATURE_BRANCH_NAME, rebased_tip, FEATURE_BRANCH_NAME, pre_rebase_tip
|
|
1590
|
+
)
|
|
1591
|
+
),
|
|
1592
|
+
)
|
|
1593
|
+
|
|
1594
|
+
exit_code = pre_push.main()
|
|
1595
|
+
|
|
1596
|
+
assert exit_code == 0
|
|
1597
|
+
|
|
1598
|
+
|
|
753
1599
|
def test_main_allows_when_code_review_enforcement_flag_is_off(
|
|
754
1600
|
tmp_path: Path,
|
|
755
1601
|
monkeypatch: pytest.MonkeyPatch,
|
|
@@ -764,7 +1610,7 @@ def test_main_allows_when_code_review_enforcement_flag_is_off(
|
|
|
764
1610
|
"""
|
|
765
1611
|
monkeypatch.delenv(CODE_REVIEW_ENFORCEMENT_ENV_VAR, raising=False)
|
|
766
1612
|
monkeypatch.delitem(sys.modules, ENFORCEMENT_CONSTANTS_MODULE_NAME, raising=False)
|
|
767
|
-
|
|
1613
|
+
_isolate_repository_and_write_passing_code_rules_gate(tmp_path, monkeypatch)
|
|
768
1614
|
real_gate_path = (
|
|
769
1615
|
Path(__file__).resolve().parent.parent
|
|
770
1616
|
/ "blocking"
|