claude-dev-env 2.7.1 → 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.
Files changed (85) hide show
  1. package/_shared/CLAUDE.md +1 -0
  2. package/_shared/advisor/advisor-protocol.md +19 -9
  3. package/_shared/pr-loop/audit-contract.md +4 -4
  4. package/_shared/pr-loop/precatch-rubric.md +2 -2
  5. package/_shared/process-tree/CLAUDE.md +41 -0
  6. package/_shared/process-tree/scripts/config/process_tree_scripts_constants/__init__.py +1 -0
  7. package/_shared/process-tree/scripts/config/process_tree_scripts_constants/process_tree_kill_constants.py +27 -0
  8. package/_shared/process-tree/scripts/process_tree_kill.py +141 -0
  9. package/_shared/process-tree/scripts/pyproject.toml +16 -0
  10. package/_shared/process-tree/scripts/test_process_tree_kill.py +278 -0
  11. package/agents/code-quality-agent.md +6 -5
  12. package/agents/deep-research.md +7 -24
  13. package/agents/docs-agent.md +1 -27
  14. package/agents/issue-tracker.md +1 -7
  15. package/agents/skill-writer-agent.md +1 -2
  16. package/agents/test_agent_frontmatter.py +309 -12
  17. package/hooks/blocking/CLAUDE.md +2 -0
  18. package/hooks/blocking/fable_spawn_gate.py +187 -0
  19. package/hooks/blocking/piped_pytest_blocker.py +1223 -0
  20. package/hooks/blocking/plain_language_blocker.py +287 -15
  21. package/hooks/blocking/test_fable_spawn_gate.py +374 -0
  22. package/hooks/blocking/test_piped_pytest_blocker.py +587 -0
  23. package/hooks/blocking/test_plain_language_blocker.py +277 -2
  24. package/hooks/blocking/test_pre_tool_use_dispatcher.py +34 -2
  25. package/hooks/git-hooks/CLAUDE.md +2 -2
  26. package/hooks/git-hooks/git_hooks_constants/__init__.py +28 -0
  27. package/hooks/git-hooks/pre_push.py +343 -54
  28. package/hooks/git-hooks/test_pre_push.py +852 -6
  29. package/hooks/hooks.json +9 -19
  30. package/hooks/hooks_constants/CLAUDE.md +2 -0
  31. package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +1 -0
  32. package/hooks/hooks_constants/fable_spawn_gate_constants.py +62 -0
  33. package/hooks/hooks_constants/piped_pytest_blocker_constants.py +360 -0
  34. package/hooks/hooks_constants/plain_language_blocker_constants.py +64 -1
  35. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +10 -0
  36. package/hooks/hooks_constants/shell_command_segments.py +1 -1
  37. package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +1 -0
  38. package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +27 -0
  39. package/hooks/hooks_constants/test_prose_metrics_parity.py +124 -0
  40. package/package.json +1 -1
  41. package/rules/CLAUDE.md +1 -0
  42. package/rules/ask-user-question-required.md +26 -0
  43. package/rules/claims-as-quotes.md +65 -0
  44. package/scripts/CLAUDE.md +1 -1
  45. package/scripts/_code_review_test_support.py +6 -0
  46. package/scripts/check.ps1 +18 -5
  47. package/scripts/claude_chain_runner.py +203 -31
  48. package/scripts/codec_forwarding_test_support.py +2 -0
  49. package/scripts/dev_env_scripts_constants/CLAUDE.md +4 -4
  50. package/scripts/dev_env_scripts_constants/claude_chain_constants.py +38 -0
  51. package/scripts/dev_env_scripts_constants/code_review_constants.py +403 -2
  52. package/scripts/dev_env_scripts_constants/grok_worker_constants.py +28 -24
  53. package/scripts/grok_headless_runner.py +16 -83
  54. package/scripts/spawn_grok_batch.py +38 -1
  55. package/scripts/test_claude_chain_runner.py +358 -0
  56. package/scripts/test_grok_headless_runner.py +18 -63
  57. package/scripts/test_invoke_code_review.py +298 -0
  58. package/scripts/test_resolve_worker_spawn.py +6 -0
  59. package/scripts/test_spawn_grok_batch.py +101 -0
  60. package/scripts/tests/CLAUDE.md +1 -0
  61. package/scripts/tests/test_grok_worker_constants.py +59 -0
  62. package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +46 -0
  63. package/skills/autoconverge/workflow/converge.contract.test.mjs +105 -2
  64. package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +34 -1
  65. package/skills/autoconverge/workflow/converge.mjs +66 -33
  66. package/skills/codex-review/scripts/codex_review_scripts_constants/codex_usage_probe_constants.py +0 -4
  67. package/skills/codex-review/scripts/codex_usage_probe.py +20 -33
  68. package/skills/codex-review/scripts/run_codex_review.py +16 -64
  69. package/skills/codex-review/scripts/test_codex_usage_probe.py +46 -41
  70. package/skills/codex-review/scripts/test_run_codex_review.py +1 -33
  71. package/skills/e-code-review/SKILL.md +9 -8
  72. package/skills/e-code-review/reference/fix.md +29 -7
  73. package/skills/e-code-review/reference/loop.md +230 -14
  74. package/skills/e-code-review/reference/low.md +33 -15
  75. package/skills/e-code-review/reference/medium.md +55 -21
  76. package/skills/e-code-review/reference/xhigh.md +30 -12
  77. package/skills/fresh-branch/CLAUDE.md +5 -5
  78. package/skills/fresh-branch/SKILL.md +14 -6
  79. package/skills/fresh-branch/scripts/create_fresh_branch.py +122 -39
  80. package/skills/fresh-branch/scripts/fresh_branch_scripts_constants/fresh_branch_cli_constants.py +10 -3
  81. package/skills/fresh-branch/scripts/test_create_fresh_branch.py +251 -0
  82. package/skills/grok-spawn/SKILL.md +5 -0
  83. package/skills/orchestrator/SKILL.md +4 -1
  84. package/skills/orchestrator-refresh/SKILL.md +5 -1
  85. 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
- _write_passing_code_rules_gate(tmp_path, monkeypatch)
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(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
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
- _write_passing_code_rules_gate(tmp_path, monkeypatch)
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
- _write_passing_code_rules_gate(tmp_path, monkeypatch)
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
- _write_passing_code_rules_gate(tmp_path, monkeypatch)
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
- _write_passing_code_rules_gate(tmp_path, monkeypatch)
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"