claude-dev-env 7.0.0 → 7.1.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/.agents/skills/pull-request/scripts/pull_request.py +2 -0
- package/.agents/skills/pull-request/scripts/test_pull_request.py +34 -0
- package/.agents/skills/session-continuity/AGENTS.md +1 -0
- package/.agents/skills/session-continuity/CLAUDE.md +1 -0
- package/.agents/skills/session-continuity/README.md +189 -0
- package/.agents/skills/session-continuity/SKILL.md +114 -0
- package/.agents/skills/session-continuity/continuity.mjs +375 -0
- package/.agents/skills/session-continuity/continuity.test.mjs +303 -0
- package/.agents/skills/syncing-submodules/scripts/test_sync_parent_pointer.py +0 -7
- package/.agents/skills/windows-scheduled-task/SKILL.md +153 -0
- package/bin/cde.mjs +36 -8
- package/bin/cde.test.mjs +48 -0
- package/bin/ever-shipped-skills.mjs +2 -20
- package/bin/ever-shipped-skills.test.mjs +8 -0
- package/bin/expand_home_directory_tokens.mjs +4 -5
- package/bin/expand_home_directory_tokens.test.mjs +128 -0
- package/bin/git_hooks_installer.mjs +22 -3
- package/bin/git_hooks_installer.test.mjs +159 -5
- package/bin/install-session-continuity.mjs +78 -0
- package/bin/install-session-continuity.test.mjs +42 -0
- package/bin/install.codex-rules.test.mjs +5 -2
- package/bin/install.cursor-rules.test.mjs +5 -2
- package/bin/install.mjs +478 -63
- package/bin/install.policy-lint.test.mjs +56 -4
- package/bin/install.profiles.test.mjs +10 -0
- package/bin/install.prune.test.mjs +227 -17
- package/bin/install.test.mjs +500 -120
- package/bin/install.transaction.test.mjs +3 -3
- package/docs/CODE_RULES.md +1 -1
- package/hooks/blocking/code_rules_banned_identifiers.py +3 -0
- package/hooks/blocking/precommit_code_rules_gate.py +5 -8
- package/hooks/blocking/test_bash_dispatcher_interpreter_starts.py +7 -52
- package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +0 -69
- package/hooks/blocking/test_code_rules_banned_identifiers.py +64 -0
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +10 -244
- package/hooks/blocking/test_shared_stdin_adoption.py +1 -47
- package/hooks/git-hooks/git_hooks_constants/verification_notice_constants.py +116 -0
- package/hooks/git-hooks/post_commit.py +33 -0
- package/hooks/git-hooks/pre_commit.py +18 -90
- package/hooks/git-hooks/pre_push.py +45 -541
- package/hooks/git-hooks/test_installed_policy_lint.py +55 -6
- package/hooks/git-hooks/test_native_hook_support.py +129 -0
- package/hooks/git-hooks/test_native_policy_commit.py +39 -6
- package/hooks/git-hooks/test_post_commit.py +43 -0
- package/hooks/git-hooks/test_pre_commit.py +36 -392
- package/hooks/git-hooks/test_pre_push.py +48 -1631
- package/hooks/git-hooks/test_verification_notice.py +60 -0
- package/hooks/git-hooks/test_verification_start.py +173 -0
- package/hooks/git-hooks/verification_notice.py +192 -0
- package/hooks/git-hooks/verification_notice_context.py +253 -0
- package/hooks/git-hooks/verification_notice_state.py +237 -0
- package/hooks/git-hooks/verification_start.py +100 -0
- package/hooks/hooks.json +0 -119
- package/hooks/hooks_constants/banned_identifiers_constants.py +4 -0
- package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +1 -15
- package/hooks/hooks_constants/post_tool_use_dispatcher_constants.py +0 -4
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +0 -50
- package/hooks/hooks_constants/stop_dispatcher_constants.py +1 -4
- package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +3 -66
- package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +42 -185
- package/hooks/hooks_constants/test_stop_dispatcher_constants.py +3 -25
- package/hooks/validation/test_post_tool_use_dispatcher.py +35 -298
- package/hooks/validators/run_all_validators.py +5 -1
- package/hooks/validators/security_checks.py +57 -2
- package/hooks/validators/test_hook_timing_harness.py +2 -5
- package/hooks/validators/test_run_all_validators_mutation_tool_parity.py +3 -11
- package/hooks/validators/test_run_all_validators_policy_lint.py +36 -0
- package/hooks/validators/test_security_checks.py +57 -0
- package/package.json +1 -1
- package/rules/confirm-implementation-forks.md +2 -0
- package/rules/git-workflow.md +12 -0
- package/rules/pstack-host-mapping.md +48 -0
- package/rules/vault-context.md +3 -5
- package/rules/verify-runtime-state.md +7 -0
- package/scripts/audit-hooks-cli.mjs +5 -0
- package/scripts/audit-hooks.mjs +72 -16
- package/scripts/automatic_advisory/__init__.py +1 -0
- package/scripts/automatic_advisory/checkout.py +51 -0
- package/scripts/automatic_advisory/cli.py +334 -0
- package/scripts/automatic_advisory/config/__init__.py +1 -0
- package/scripts/automatic_advisory/config/constants.py +93 -0
- package/scripts/automatic_advisory/config/timing.py +4 -0
- package/scripts/automatic_advisory/configuration.py +197 -0
- package/scripts/automatic_advisory/execution.py +350 -0
- package/scripts/automatic_advisory/git.py +69 -0
- package/scripts/automatic_advisory/model.py +107 -0
- package/scripts/automatic_advisory/process_host.py +32 -0
- package/scripts/automatic_advisory/publisher.py +84 -0
- package/scripts/automatic_advisory/runner.py +399 -0
- package/scripts/automatic_advisory/state.py +116 -0
- package/scripts/automatic_advisory/window_flags.py +41 -0
- package/scripts/automatic_advisory/windows_job.py +122 -0
- package/scripts/codex_compat_materializer.py +99 -13
- package/scripts/durable_post_lint.py +30 -2
- package/scripts/durable_post_lint_config/config/constants.py +10 -0
- package/scripts/durable_post_lint_config/config/test_constants.py +13 -0
- package/scripts/local_report_cli.py +184 -0
- package/scripts/local_report_core.py +247 -0
- package/scripts/local_report_publisher.py +12 -0
- package/scripts/local_report_validation.py +357 -0
- package/scripts/local_verification/__init__.py +23 -0
- package/scripts/local_verification/check_runner.py +364 -0
- package/scripts/local_verification/check_support.py +99 -0
- package/scripts/local_verification/cli.py +158 -0
- package/scripts/local_verification/command_runner.py +84 -0
- package/scripts/local_verification/config/__init__.py +1 -0
- package/scripts/local_verification/config/constants.py +89 -0
- package/scripts/local_verification/config/timing.py +1 -0
- package/scripts/local_verification/git_state.py +146 -0
- package/scripts/local_verification/manifest.py +224 -0
- package/scripts/local_verification/model.py +174 -0
- package/scripts/local_verification/runner.py +234 -0
- package/scripts/policy_lint/adapter_pairing.py +58 -3
- package/scripts/policy_lint/config/approved_test_pairs.py +323 -0
- package/scripts/pr_verification/__init__.py +1 -0
- package/scripts/pr_verification/branch_refs.py +114 -0
- package/scripts/pr_verification/config/__init__.py +1 -0
- package/scripts/pr_verification/config/constants.py +84 -0
- package/scripts/pr_verification/config/timing.py +5 -0
- package/scripts/pr_verification/github.py +137 -0
- package/scripts/pr_verification/github_api.py +376 -0
- package/scripts/pr_verification/github_labels.py +88 -0
- package/scripts/pr_verification/github_parsing.py +182 -0
- package/scripts/pr_verification/github_transport.py +91 -0
- package/scripts/pr_verification/lock.py +122 -0
- package/scripts/pr_verification/model.py +43 -0
- package/scripts/pr_verification/requirements.txt +1 -0
- package/scripts/test_durable_post_lint.py +97 -0
- package/scripts/tests/test-audit-hooks-cli.mjs +150 -0
- package/scripts/tests/test-audit-hooks.mjs +94 -2
- package/scripts/tests/test_automatic_advisory.py +376 -0
- package/scripts/tests/test_automatic_advisory_job_cleanup.py +71 -0
- package/scripts/tests/test_automatic_advisory_polling.py +216 -0
- package/scripts/tests/test_automatic_advisory_recovery.py +320 -0
- package/scripts/tests/test_automatic_advisory_repairs.py +400 -0
- package/scripts/tests/test_automatic_advisory_settings.py +48 -0
- package/scripts/tests/test_automatic_advisory_window_flags.py +163 -0
- package/scripts/tests/test_closed_pr_label.py +157 -0
- package/scripts/tests/test_codex_compat_materializer.py +189 -6
- package/scripts/tests/test_github_labels.py +101 -0
- package/scripts/tests/test_github_transport.py +125 -0
- package/scripts/tests/test_local_report_publisher.py +396 -0
- package/scripts/tests/test_local_report_publisher_cli.py +85 -0
- package/scripts/tests/test_local_verification.py +348 -0
- package/scripts/tests/test_local_verification_cli.py +98 -0
- package/scripts/tests/test_policy_lint_rules_pairing.py +163 -0
- package/scripts/tests/test_pr_verification_github.py +392 -0
- package/scripts/tests/test_pr_verification_lock.py +168 -0
- package/scripts/tests/test_verification_notice.py +347 -0
- package/hooks/blocking/ask_user_question_shape_blocker.py +0 -83
- package/hooks/blocking/bot_mention_comment_blocker.py +0 -76
- package/hooks/blocking/claude_md_orphan_file_blocker.py +0 -129
- package/hooks/blocking/cursor_cli_python_misfire_blocker.py +0 -108
- package/hooks/blocking/destructive_command_blocker.py +0 -2073
- package/hooks/blocking/fable_spawn_gate.py +0 -187
- package/hooks/blocking/luna_fast_mode_gate.py +0 -160
- package/hooks/blocking/nas_ssh_binary_enforcer.py +0 -163
- package/hooks/blocking/orchestrator_refresh_reschedule_gate.py +0 -260
- package/hooks/blocking/package_inventory_stale_blocker.py +0 -107
- package/hooks/blocking/pii_prevention_blocker.py +0 -365
- package/hooks/blocking/piped_pytest_blocker.py +0 -1223
- package/hooks/blocking/question_to_user_enforcer.py +0 -130
- package/hooks/blocking/send_user_file_open_locally_blocker.py +0 -71
- package/hooks/blocking/sensitive_file_protector.py +0 -167
- package/hooks/blocking/session_edit_stage_gate.py +0 -684
- package/hooks/blocking/session_handoff_blocker.py +0 -182
- package/hooks/blocking/shell_substitution_blocker.py +0 -350
- package/hooks/blocking/test_ask_user_question_shape.py +0 -69
- package/hooks/blocking/test_bot_mention_comment_blocker.py +0 -131
- package/hooks/blocking/test_claude_md_orphan_file_blocker.py +0 -1277
- package/hooks/blocking/test_cursor_cli_python_misfire_blocker.py +0 -208
- package/hooks/blocking/test_destructive_command_blocker.py +0 -2024
- package/hooks/blocking/test_destructive_command_blocker_deny_mode.py +0 -105
- package/hooks/blocking/test_destructive_command_blocker_no_verify.py +0 -122
- package/hooks/blocking/test_destructive_command_blocker_patterns.py +0 -133
- package/hooks/blocking/test_fable_spawn_gate.py +0 -381
- package/hooks/blocking/test_inventory_deadlock_resolution.py +0 -154
- package/hooks/blocking/test_luna_fast_mode_gate.py +0 -211
- package/hooks/blocking/test_nas_ssh_binary_enforcer.py +0 -225
- package/hooks/blocking/test_orchestrator_refresh_reschedule_gate.py +0 -231
- package/hooks/blocking/test_package_inventory_stale_blocker.py +0 -382
- package/hooks/blocking/test_pii_prevention_blocker.py +0 -1222
- package/hooks/blocking/test_pii_prevention_windows_git_path.py +0 -41
- package/hooks/blocking/test_pii_write_surface_ephemeral.py +0 -221
- package/hooks/blocking/test_piped_pytest_blocker.py +0 -587
- package/hooks/blocking/test_precommit_code_rules_gate_native_owner.py +0 -233
- package/hooks/blocking/test_question_to_user_enforcer.py +0 -169
- package/hooks/blocking/test_send_user_file_open_locally_blocker.py +0 -118
- package/hooks/blocking/test_sensitive_file_protector.py +0 -365
- package/hooks/blocking/test_session_edit_stage_gate.py +0 -537
- package/hooks/blocking/test_session_edit_stage_gate_staging.py +0 -64
- package/hooks/blocking/test_session_edit_stage_gate_support.py +0 -204
- package/hooks/blocking/test_session_edit_stage_gate_tracker.py +0 -116
- package/hooks/blocking/test_session_handoff_blocker.py +0 -318
- package/hooks/blocking/test_shell_substitution_blocker.py +0 -298
- package/hooks/blocking/test_unscoped_search_blocker.py +0 -102
- package/hooks/blocking/test_write_existing_file_blocker.py +0 -122
- package/hooks/blocking/tests/test_pii_prevention_blocker.py +0 -259
- package/hooks/blocking/unscoped_search_blocker.py +0 -391
- package/hooks/blocking/write_existing_file_blocker.py +0 -124
- package/hooks/hooks_constants/bot_mention_comment_blocker_constants.py +0 -19
- package/hooks/hooks_constants/cursor_cli_python_misfire_blocker_constants.py +0 -79
- package/hooks/hooks_constants/fable_spawn_gate_constants.py +0 -61
- package/hooks/hooks_constants/luna_fast_mode_gate_constants.py +0 -45
- package/hooks/hooks_constants/nas_ssh_binary_enforcer_constants.py +0 -20
- package/hooks/hooks_constants/orchestrator_refresh_reschedule_gate_constants.py +0 -57
- package/hooks/hooks_constants/send_user_file_open_locally_blocker_constants.py +0 -17
- package/hooks/hooks_constants/sensitive_file_protector_constants.py +0 -40
- package/hooks/hooks_constants/session_handoff_blocker_constants.py +0 -10
- package/hooks/hooks_constants/shell_substitution_blocker_constants.py +0 -80
|
@@ -1,1655 +1,72 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import io
|
|
4
|
-
import subprocess
|
|
5
4
|
import sys
|
|
6
|
-
from collections.abc import Callable
|
|
7
5
|
from pathlib import Path
|
|
8
6
|
|
|
9
|
-
import
|
|
10
|
-
|
|
11
|
-
import
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
ALL_ZEROS_OBJECT_NAME: str = "0" * 40
|
|
21
|
-
NON_ZERO_LOCAL_SHA: str = "a" * 40
|
|
22
|
-
NON_ZERO_REMOTE_SHA_ONE: str = "1" * 40
|
|
23
|
-
NON_ZERO_REMOTE_SHA_TWO: str = "2" * 40
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
GIT_RESOLVED_EXIT_CODE: int = 0
|
|
27
|
-
RESOLVED_COMMIT_OBJECT_NAME: str = "c" * 40
|
|
28
|
-
HOOK_INVOCATION_NAME: str = "pre-push"
|
|
29
|
-
PUSHED_REMOTE_NAME: str = "upstream"
|
|
30
|
-
PUSHED_REMOTE_URL: str = "https://example.invalid/owner/repository.git"
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
LOCALE_INVALID_REFERENCE_LISTING_BYTES: bytes = b"origin/caf\xe9\norigin/main\n"
|
|
34
|
-
EMPTY_COMMAND_STREAM_BYTES: bytes = b""
|
|
35
|
-
STRICT_DECODER_NAME: str = "utf-8"
|
|
36
|
-
UNDECODABLE_BYTE_START_INDEX: int = 10
|
|
37
|
-
UNDECODABLE_BYTE_END_INDEX: int = 11
|
|
38
|
-
UNDECODABLE_BYTE_REASON: str = "invalid continuation byte"
|
|
39
|
-
TEXT_DECODING_KEYWORD: str = "text"
|
|
40
|
-
GIT_MISSING_REFERENCE_EXIT_CODE: int = 1
|
|
41
|
-
GIT_LAUNCH_FAILURE_DETAIL: str = "git executable not found"
|
|
42
|
-
RESOLVED_REMOTE_MAIN_REFERENCE: str = "origin/main"
|
|
43
|
-
UNRESOLVABLE_BASE_REPORT_MARKER: str = "no usable gate base"
|
|
44
|
-
GIT_UNAVAILABLE_REPORT_MARKER: str = "could not run git"
|
|
45
|
-
NEW_BRANCH_PUSH_STDIN: str = (
|
|
46
|
-
f"refs/heads/feature {NON_ZERO_LOCAL_SHA} refs/heads/feature {ALL_ZEROS_OBJECT_NAME}\n"
|
|
47
|
-
)
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
def _refuse_locale_decoding() -> UnicodeDecodeError:
|
|
51
|
-
"""Build the error a text-mode subprocess raises on undecodable output."""
|
|
52
|
-
return UnicodeDecodeError(
|
|
53
|
-
STRICT_DECODER_NAME,
|
|
54
|
-
LOCALE_INVALID_REFERENCE_LISTING_BYTES,
|
|
55
|
-
UNDECODABLE_BYTE_START_INDEX,
|
|
56
|
-
UNDECODABLE_BYTE_END_INDEX,
|
|
57
|
-
UNDECODABLE_BYTE_REASON,
|
|
58
|
-
)
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
def _completed_git_process(
|
|
62
|
-
all_command_arguments: list[str], exit_code: int, standard_output_bytes: bytes
|
|
63
|
-
) -> subprocess.CompletedProcess[bytes]:
|
|
64
|
-
"""Build a completed git process carrying raw bytes on standard output."""
|
|
65
|
-
return subprocess.CompletedProcess(
|
|
66
|
-
all_command_arguments,
|
|
67
|
-
exit_code,
|
|
68
|
-
standard_output_bytes,
|
|
69
|
-
EMPTY_COMMAND_STREAM_BYTES,
|
|
70
|
-
)
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
def _answer_git_with_locale_invalid_bytes(
|
|
74
|
-
all_command_arguments: list[str],
|
|
75
|
-
**all_keyword_arguments: object,
|
|
76
|
-
) -> subprocess.CompletedProcess[bytes]:
|
|
77
|
-
"""Stand in for a git whose output the process locale cannot decode.
|
|
78
|
-
|
|
79
|
-
::
|
|
80
|
-
|
|
81
|
-
text=True -> UnicodeDecodeError bytes mode -> the raw listing
|
|
82
|
-
|
|
83
|
-
Args:
|
|
84
|
-
all_command_arguments: The full git command the seam assembled.
|
|
85
|
-
all_keyword_arguments: The keyword arguments the seam passed.
|
|
86
|
-
|
|
87
|
-
Returns:
|
|
88
|
-
A completed process carrying the raw listing bytes.
|
|
89
|
-
"""
|
|
90
|
-
if all_keyword_arguments.get(TEXT_DECODING_KEYWORD):
|
|
91
|
-
raise _refuse_locale_decoding()
|
|
92
|
-
if git_hooks_constants.GIT_FOR_EACH_REF_SUBCOMMAND in all_command_arguments:
|
|
93
|
-
return _completed_git_process(
|
|
94
|
-
all_command_arguments,
|
|
95
|
-
GIT_RESOLVED_EXIT_CODE,
|
|
96
|
-
LOCALE_INVALID_REFERENCE_LISTING_BYTES,
|
|
97
|
-
)
|
|
98
|
-
return _completed_git_process(
|
|
99
|
-
all_command_arguments,
|
|
100
|
-
GIT_MISSING_REFERENCE_EXIT_CODE,
|
|
101
|
-
EMPTY_COMMAND_STREAM_BYTES,
|
|
102
|
-
)
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
def _write_gate_that_allows_every_push(
|
|
106
|
-
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
107
|
-
) -> None:
|
|
108
|
-
"""Point both gates at stubs that allow, with a new-branch push on stdin."""
|
|
109
|
-
passing_gate_path = tmp_path / "code_rules_gate.py"
|
|
110
|
-
passing_gate_path.write_text("import sys\nsys.exit(0)\n", encoding="utf-8")
|
|
111
|
-
monkeypatch.setenv("CODE_RULES_GATE_PATH", str(passing_gate_path))
|
|
112
|
-
monkeypatch.setattr(sys, "stdin", io.StringIO(NEW_BRANCH_PUSH_STDIN))
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
def test_reference_listing_with_locale_invalid_bytes_resolves_a_base(
|
|
116
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
117
|
-
) -> None:
|
|
118
|
-
monkeypatch.setattr(subprocess, "run", _answer_git_with_locale_invalid_bytes)
|
|
119
|
-
|
|
120
|
-
resolved_reference = pre_push.resolve_usable_base_reference(
|
|
121
|
-
git_hooks_constants.DEFAULT_REMOTE_BASE_REFERENCE,
|
|
122
|
-
git_hooks_constants.DEFAULT_REMOTE_NAME,
|
|
123
|
-
REAL_RUN_GIT_TEXT_COMMAND,
|
|
124
|
-
)
|
|
125
|
-
|
|
126
|
-
assert resolved_reference == RESOLVED_REMOTE_MAIN_REFERENCE
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
def test_run_git_reference_query_returns_replaced_text_on_locale_invalid_bytes(
|
|
130
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
131
|
-
) -> None:
|
|
132
|
-
"""text=True would raise; bytes mode returns the replacing-policy decode."""
|
|
133
|
-
locale_invalid_reference_bytes = b"refs/remotes/origin/caf\xe9"
|
|
134
|
-
expected_replaced_reference = locale_invalid_reference_bytes.decode(
|
|
135
|
-
git_hooks_constants.GIT_OUTPUT_ENCODING_NAME,
|
|
136
|
-
errors=git_hooks_constants.GIT_OUTPUT_DECODE_ERRORS_POLICY,
|
|
137
|
-
)
|
|
138
|
-
|
|
139
|
-
def answer_with_locale_invalid_reference(
|
|
140
|
-
all_command_arguments: list[str], **all_keyword_arguments: object
|
|
141
|
-
) -> subprocess.CompletedProcess[bytes]:
|
|
142
|
-
if all_keyword_arguments.get(TEXT_DECODING_KEYWORD):
|
|
143
|
-
raise _refuse_locale_decoding()
|
|
144
|
-
return _completed_git_process(
|
|
145
|
-
all_command_arguments,
|
|
146
|
-
GIT_RESOLVED_EXIT_CODE,
|
|
147
|
-
locale_invalid_reference_bytes,
|
|
148
|
-
)
|
|
149
|
-
|
|
150
|
-
monkeypatch.setattr(subprocess, "run", answer_with_locale_invalid_reference)
|
|
151
|
-
|
|
152
|
-
resolved_output = pre_push.run_git_reference_query(
|
|
153
|
-
("git", "rev-parse", "--verify", "--quiet", "refs/remotes/origin/HEAD")
|
|
154
|
-
)
|
|
155
|
-
|
|
156
|
-
assert resolved_output == expected_replaced_reference
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
def test_unresolvable_merge_base_message_describes_pending_validation() -> None:
|
|
160
|
-
skip_message = git_hooks_constants.UNRESOLVABLE_MERGE_BASE_MESSAGE
|
|
161
|
-
assert "CODE_RULES validation is pending" in skip_message
|
|
162
|
-
assert "Restore shared history" in skip_message
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
def test_run_git_text_command_raises_when_git_cannot_launch(
|
|
166
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
167
|
-
) -> None:
|
|
168
|
-
def raising_subprocess_run(
|
|
169
|
-
all_command_arguments: list[str], **all_keyword_arguments: object
|
|
170
|
-
) -> subprocess.CompletedProcess[bytes]:
|
|
171
|
-
raise OSError(GIT_LAUNCH_FAILURE_DETAIL)
|
|
172
|
-
|
|
173
|
-
monkeypatch.setattr(subprocess, "run", raising_subprocess_run)
|
|
174
|
-
|
|
175
|
-
with pytest.raises(pre_push.GitCommandUnavailable):
|
|
176
|
-
REAL_RUN_GIT_TEXT_COMMAND([git_hooks_constants.GIT_FOR_EACH_REF_SUBCOMMAND])
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
def test_main_reports_infrastructure_failure_when_git_cannot_launch(
|
|
180
|
-
tmp_path: Path,
|
|
181
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
182
|
-
capsys: pytest.CaptureFixture[str],
|
|
183
|
-
) -> None:
|
|
184
|
-
_write_gate_that_allows_every_push(tmp_path, monkeypatch)
|
|
185
|
-
|
|
186
|
-
def unavailable_run_git_text_command(
|
|
187
|
-
all_command_arguments: list[str],
|
|
188
|
-
) -> tuple[int, str]:
|
|
189
|
-
raise pre_push.GitCommandUnavailable(GIT_LAUNCH_FAILURE_DETAIL)
|
|
190
|
-
|
|
191
|
-
monkeypatch.setattr(
|
|
192
|
-
pre_push, "run_git_text_command", unavailable_run_git_text_command
|
|
193
|
-
)
|
|
194
|
-
|
|
195
|
-
exit_code = pre_push.main()
|
|
196
|
-
|
|
197
|
-
captured_streams = capsys.readouterr()
|
|
198
|
-
assert exit_code == git_hooks_constants.GATE_INFRASTRUCTURE_FAILURE_EXIT_CODE
|
|
199
|
-
assert GIT_UNAVAILABLE_REPORT_MARKER in captured_streams.err
|
|
200
|
-
assert GIT_LAUNCH_FAILURE_DETAIL in captured_streams.err
|
|
201
|
-
assert UNRESOLVABLE_BASE_REPORT_MARKER not in captured_streams.err
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
def test_main_reports_a_missing_default_branch_as_absence(
|
|
205
|
-
tmp_path: Path,
|
|
206
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
207
|
-
capsys: pytest.CaptureFixture[str],
|
|
208
|
-
) -> None:
|
|
209
|
-
_write_gate_that_allows_every_push(tmp_path, monkeypatch)
|
|
210
|
-
|
|
211
|
-
def empty_run_git_text_command(
|
|
212
|
-
all_command_arguments: list[str],
|
|
213
|
-
) -> tuple[int, str]:
|
|
214
|
-
return GIT_MISSING_REFERENCE_EXIT_CODE, ""
|
|
215
|
-
|
|
216
|
-
monkeypatch.setattr(pre_push, "run_git_text_command", empty_run_git_text_command)
|
|
217
|
-
|
|
218
|
-
exit_code = pre_push.main()
|
|
219
|
-
|
|
220
|
-
captured_streams = capsys.readouterr()
|
|
221
|
-
assert exit_code == git_hooks_constants.GATE_INFRASTRUCTURE_FAILURE_EXIT_CODE
|
|
222
|
-
assert UNRESOLVABLE_BASE_REPORT_MARKER in captured_streams.err
|
|
223
|
-
assert GIT_UNAVAILABLE_REPORT_MARKER not in captured_streams.err
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
@pytest.fixture(autouse=True)
|
|
227
|
-
def resolve_remote_head_reference(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
228
|
-
"""Make every reference resolve, so these tests ignore the clone's remote state.
|
|
229
|
-
|
|
230
|
-
``main`` resolves the gate base through git before it invokes the gate. A
|
|
231
|
-
clone with no ``origin/HEAD`` would send these tests down the unresolvable
|
|
232
|
-
path and change what ``main`` returns. Stubbing the seam keeps each test
|
|
233
|
-
about the flow it names. Base resolution itself is covered by
|
|
234
|
-
``test_pre_push_base_reference.py``.
|
|
235
|
-
"""
|
|
236
|
-
|
|
237
|
-
def fake_run_git_text_command(all_command_arguments: list[str]) -> tuple[int, str]:
|
|
238
|
-
return GIT_RESOLVED_EXIT_CODE, RESOLVED_COMMIT_OBJECT_NAME
|
|
239
|
-
|
|
240
|
-
monkeypatch.setattr(pre_push, "run_git_text_command", fake_run_git_text_command)
|
|
241
|
-
monkeypatch.setattr(
|
|
242
|
-
sys,
|
|
243
|
-
"argv",
|
|
244
|
-
[HOOK_INVOCATION_NAME, git_hooks_constants.DEFAULT_REMOTE_NAME, PUSHED_REMOTE_URL],
|
|
245
|
-
)
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
def test_resolve_base_reference_from_stdin_uses_remote_object_when_non_zero() -> None:
|
|
250
|
-
stdin_text = (
|
|
251
|
-
f"refs/heads/feature {NON_ZERO_LOCAL_SHA} refs/heads/feature {NON_ZERO_REMOTE_SHA_ONE}\n"
|
|
252
|
-
)
|
|
253
|
-
|
|
254
|
-
base_reference = pre_push.resolve_base_reference_from_stdin(stdin_text)
|
|
255
|
-
|
|
256
|
-
assert base_reference == NON_ZERO_REMOTE_SHA_ONE
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
def test_resolve_base_reference_from_stdin_falls_back_when_remote_is_all_zeros() -> None:
|
|
260
|
-
stdin_text = f"refs/heads/feature {NON_ZERO_LOCAL_SHA} refs/heads/feature {ALL_ZEROS_OBJECT_NAME}\n"
|
|
261
|
-
|
|
262
|
-
base_reference = pre_push.resolve_base_reference_from_stdin(stdin_text)
|
|
263
|
-
|
|
264
|
-
assert base_reference == pre_push.DEFAULT_REMOTE_BASE_REFERENCE
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
def test_resolve_base_reference_from_stdin_falls_back_when_stdin_is_empty() -> None:
|
|
268
|
-
base_reference = pre_push.resolve_base_reference_from_stdin("")
|
|
269
|
-
|
|
270
|
-
assert base_reference == pre_push.DEFAULT_REMOTE_BASE_REFERENCE
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
def test_resolve_base_reference_from_stdin_prefers_first_non_zero_remote_object() -> (
|
|
274
|
-
None
|
|
275
|
-
):
|
|
276
|
-
stdin_text = (
|
|
277
|
-
f"refs/heads/new_branch {ALL_ZEROS_OBJECT_NAME} refs/heads/new_branch {ALL_ZEROS_OBJECT_NAME}\n"
|
|
278
|
-
f"refs/heads/existing {NON_ZERO_LOCAL_SHA} refs/heads/existing {NON_ZERO_REMOTE_SHA_TWO}\n"
|
|
279
|
-
)
|
|
280
|
-
|
|
281
|
-
base_reference = pre_push.resolve_base_reference_from_stdin(stdin_text)
|
|
282
|
-
|
|
283
|
-
assert base_reference == NON_ZERO_REMOTE_SHA_TWO
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
def test_main_exits_zero_when_gate_script_missing(
|
|
287
|
-
tmp_path: Path,
|
|
288
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
289
|
-
) -> None:
|
|
290
|
-
monkeypatch.setenv(
|
|
291
|
-
"CODE_RULES_GATE_PATH",
|
|
292
|
-
str(tmp_path / "does_not_exist.py"),
|
|
293
|
-
)
|
|
294
|
-
monkeypatch.setattr(sys, "stdin", io.StringIO(""))
|
|
295
|
-
|
|
296
|
-
exit_code = pre_push.main()
|
|
297
|
-
|
|
298
|
-
assert exit_code == 0
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
def test_main_invokes_gate_with_resolved_base_reference(
|
|
302
|
-
tmp_path: Path,
|
|
303
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
304
|
-
) -> None:
|
|
305
|
-
recorded_arguments_path = tmp_path / "recorded_arguments.txt"
|
|
306
|
-
recording_gate_script_path = tmp_path / "recording_gate.py"
|
|
307
|
-
recording_gate_script_path.write_text(
|
|
308
|
-
"import sys, pathlib\n"
|
|
309
|
-
f'pathlib.Path(r"{recorded_arguments_path}").write_text('
|
|
310
|
-
"'\\n'.join(sys.argv[1:]), encoding='utf-8')\n"
|
|
311
|
-
"sys.exit(0)\n",
|
|
312
|
-
encoding="utf-8",
|
|
313
|
-
)
|
|
314
|
-
monkeypatch.setenv("CODE_RULES_GATE_PATH", str(recording_gate_script_path))
|
|
315
|
-
_isolate_from_default_branch_refs(tmp_path, monkeypatch)
|
|
316
|
-
remote_sha = "9" * 40
|
|
317
|
-
monkeypatch.setattr(
|
|
318
|
-
sys,
|
|
319
|
-
"stdin",
|
|
320
|
-
io.StringIO(f"refs/heads/feature {NON_ZERO_LOCAL_SHA} refs/heads/feature {remote_sha}\n"),
|
|
321
|
-
)
|
|
322
|
-
|
|
323
|
-
exit_code = pre_push.main()
|
|
324
|
-
|
|
325
|
-
assert exit_code == 0
|
|
326
|
-
assert recorded_arguments_path.exists(), (
|
|
327
|
-
f"recording gate did not write to {recorded_arguments_path}"
|
|
328
|
-
)
|
|
329
|
-
recorded_arguments = recorded_arguments_path.read_text(
|
|
330
|
-
encoding="utf-8"
|
|
331
|
-
).splitlines()
|
|
332
|
-
assert recorded_arguments == ["--base", remote_sha]
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
def test_main_passes_the_pushed_remote_name_to_base_resolution(
|
|
336
|
-
tmp_path: Path,
|
|
337
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
338
|
-
) -> None:
|
|
339
|
-
# No default-branch ref so the gate base is the stdin remote object, not a
|
|
340
|
-
# merge-base skip; usable-base resolution then sees the pushed remote name.
|
|
341
|
-
_isolate_repository_and_write_passing_code_rules_gate(tmp_path, monkeypatch)
|
|
342
|
-
all_recorded_remote_names: list[str] = []
|
|
343
|
-
|
|
344
|
-
def recording_resolve_usable_base_reference(
|
|
345
|
-
base_reference: str,
|
|
346
|
-
remote_name: str,
|
|
347
|
-
ask_git: Callable[[list[str]], tuple[int, str]],
|
|
348
|
-
) -> str:
|
|
349
|
-
all_recorded_remote_names.append(remote_name)
|
|
350
|
-
return base_reference
|
|
351
|
-
|
|
352
|
-
monkeypatch.setattr(
|
|
353
|
-
pre_push,
|
|
354
|
-
"resolve_usable_base_reference",
|
|
355
|
-
recording_resolve_usable_base_reference,
|
|
356
|
-
)
|
|
357
|
-
monkeypatch.setattr(
|
|
358
|
-
sys, "argv", [HOOK_INVOCATION_NAME, PUSHED_REMOTE_NAME, PUSHED_REMOTE_URL]
|
|
359
|
-
)
|
|
360
|
-
|
|
361
|
-
exit_code = pre_push.main()
|
|
362
|
-
|
|
363
|
-
assert exit_code == 0
|
|
364
|
-
assert all_recorded_remote_names == [PUSHED_REMOTE_NAME]
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
def test_main_propagates_blocking_exit_code_from_gate(
|
|
368
|
-
tmp_path: Path,
|
|
369
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
370
|
-
) -> None:
|
|
371
|
-
blocking_gate_script_path = tmp_path / "blocking_gate.py"
|
|
372
|
-
blocking_gate_script_path.write_text(
|
|
373
|
-
"import sys\nsys.exit(1)\n",
|
|
374
|
-
encoding="utf-8",
|
|
375
|
-
)
|
|
376
|
-
monkeypatch.setenv("CODE_RULES_GATE_PATH", str(blocking_gate_script_path))
|
|
377
|
-
monkeypatch.setattr(sys, "stdin", io.StringIO(""))
|
|
378
|
-
|
|
379
|
-
exit_code = pre_push.main()
|
|
380
|
-
|
|
381
|
-
assert exit_code == 1
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
def test_main_propagates_infrastructure_failure_exit_code_from_gate(
|
|
385
|
-
tmp_path: Path,
|
|
386
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
387
|
-
) -> None:
|
|
388
|
-
infrastructure_failure_gate_path = tmp_path / "infrastructure_failure_gate.py"
|
|
389
|
-
infrastructure_failure_gate_path.write_text(
|
|
390
|
-
"import sys\nsys.exit(2)\n",
|
|
391
|
-
encoding="utf-8",
|
|
392
|
-
)
|
|
393
|
-
monkeypatch.setenv("CODE_RULES_GATE_PATH", str(infrastructure_failure_gate_path))
|
|
394
|
-
monkeypatch.setattr(sys, "stdin", io.StringIO(""))
|
|
395
|
-
|
|
396
|
-
exit_code = pre_push.main()
|
|
397
|
-
|
|
398
|
-
assert exit_code == 2
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
def test_main_exits_two_when_stdin_raises_ioerror(
|
|
402
|
-
tmp_path: Path,
|
|
403
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
404
|
-
) -> None:
|
|
405
|
-
gate_path = tmp_path / "gate.py"
|
|
406
|
-
gate_path.write_text("import sys\nsys.exit(0)\n", encoding="utf-8")
|
|
407
|
-
monkeypatch.setenv("CODE_RULES_GATE_PATH", str(gate_path))
|
|
408
|
-
|
|
409
|
-
class RaisingStdin:
|
|
410
|
-
def read(self) -> str:
|
|
411
|
-
raise IOError("broken pipe")
|
|
412
|
-
|
|
413
|
-
monkeypatch.setattr(sys, "stdin", RaisingStdin())
|
|
414
|
-
|
|
415
|
-
exit_code = pre_push.main()
|
|
416
|
-
|
|
417
|
-
assert exit_code == git_hooks_constants.GATE_INFRASTRUCTURE_FAILURE_EXIT_CODE
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
def test_main_exits_two_when_invoke_gate_raises_oserror(
|
|
421
|
-
tmp_path: Path,
|
|
422
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
423
|
-
) -> None:
|
|
424
|
-
gate_path = tmp_path / "gate.py"
|
|
425
|
-
gate_path.write_text("import sys\nsys.exit(0)\n", encoding="utf-8")
|
|
426
|
-
monkeypatch.setenv("CODE_RULES_GATE_PATH", str(gate_path))
|
|
427
|
-
monkeypatch.setattr(sys, "stdin", io.StringIO(""))
|
|
428
|
-
|
|
429
|
-
def raising_run(*args: object, **kwargs: object) -> object:
|
|
430
|
-
raise OSError("no such file")
|
|
431
|
-
|
|
432
|
-
monkeypatch.setattr(__import__("subprocess"), "run", raising_run)
|
|
433
|
-
|
|
434
|
-
exit_code = pre_push.main()
|
|
435
|
-
|
|
436
|
-
assert exit_code == git_hooks_constants.GATE_INFRASTRUCTURE_FAILURE_EXIT_CODE
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
def test_resolve_base_reference_from_stdin_emits_warning_for_malformed_line(
|
|
440
|
-
capsys: pytest.CaptureFixture[str],
|
|
441
|
-
) -> None:
|
|
442
|
-
malformed_stdin_text = "only_one_field\n"
|
|
443
|
-
|
|
444
|
-
pre_push.resolve_base_reference_from_stdin(malformed_stdin_text)
|
|
445
|
-
|
|
446
|
-
captured = capsys.readouterr()
|
|
447
|
-
assert "malformed" in captured.err
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
def test_resolve_base_reference_from_stdin_returns_none_when_local_sha_is_all_zeros() -> None:
|
|
451
|
-
stdin_text = f"refs/heads/feature {ALL_ZEROS_OBJECT_NAME} refs/heads/feature {ALL_ZEROS_OBJECT_NAME}\n"
|
|
452
|
-
|
|
453
|
-
base_reference = pre_push.resolve_base_reference_from_stdin(stdin_text)
|
|
454
|
-
|
|
455
|
-
assert base_reference is None
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
def test_main_exits_zero_immediately_when_push_is_deletion(
|
|
459
|
-
tmp_path: Path,
|
|
460
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
461
|
-
) -> None:
|
|
462
|
-
gate_path = tmp_path / "gate.py"
|
|
463
|
-
gate_path.write_text("import sys\nsys.exit(1)\n", encoding="utf-8")
|
|
464
|
-
monkeypatch.setenv("CODE_RULES_GATE_PATH", str(gate_path))
|
|
465
|
-
deletion_stdin = f"refs/heads/feature {ALL_ZEROS_OBJECT_NAME} refs/heads/feature {ALL_ZEROS_OBJECT_NAME}\n"
|
|
466
|
-
monkeypatch.setattr(sys, "stdin", io.StringIO(deletion_stdin))
|
|
467
|
-
|
|
468
|
-
exit_code = pre_push.main()
|
|
469
|
-
|
|
470
|
-
assert exit_code == 0
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
def test_resolve_base_reference_from_stdin_returns_sentinel_for_malformed_lines() -> None:
|
|
474
|
-
malformed_only_stdin = "one_field_only\nalso_malformed\n"
|
|
475
|
-
|
|
476
|
-
base_reference = pre_push.resolve_base_reference_from_stdin(malformed_only_stdin)
|
|
477
|
-
|
|
478
|
-
assert base_reference == git_hooks_constants.NO_PARSEABLE_STDIN_LINES_SENTINEL
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
def test_main_prints_stderr_when_gate_script_missing(
|
|
482
|
-
tmp_path: Path,
|
|
483
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
484
|
-
capsys: pytest.CaptureFixture[str],
|
|
485
|
-
) -> None:
|
|
486
|
-
monkeypatch.setenv(
|
|
487
|
-
"CODE_RULES_GATE_PATH",
|
|
488
|
-
str(tmp_path / "does_not_exist.py"),
|
|
489
|
-
)
|
|
490
|
-
monkeypatch.setattr(sys, "stdin", io.StringIO(""))
|
|
491
|
-
|
|
492
|
-
pre_push.main()
|
|
493
|
-
|
|
494
|
-
captured = capsys.readouterr()
|
|
495
|
-
assert captured.err != ""
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
def test_resolve_base_reference_from_stdin_reports_sentinel_with_no_valid_lines() -> None:
|
|
499
|
-
malformed_only_stdin = "one_field_only\nalso_malformed\n"
|
|
500
|
-
|
|
501
|
-
result = pre_push.resolve_base_reference_from_stdin(malformed_only_stdin)
|
|
502
|
-
|
|
503
|
-
assert result == git_hooks_constants.NO_PARSEABLE_STDIN_LINES_SENTINEL
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
def test_main_exits_two_when_all_stdin_lines_are_malformed(
|
|
507
|
-
tmp_path: Path,
|
|
508
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
509
|
-
capsys: pytest.CaptureFixture[str],
|
|
510
|
-
) -> None:
|
|
511
|
-
gate_path = tmp_path / "gate.py"
|
|
512
|
-
gate_path.write_text("import sys\nsys.exit(0)\n", encoding="utf-8")
|
|
513
|
-
monkeypatch.setenv("CODE_RULES_GATE_PATH", str(gate_path))
|
|
514
|
-
monkeypatch.setattr(sys, "stdin", io.StringIO("one_field_only\nalso_malformed\n"))
|
|
515
|
-
|
|
516
|
-
exit_code = pre_push.main()
|
|
517
|
-
|
|
518
|
-
assert exit_code == git_hooks_constants.GATE_INFRASTRUCTURE_FAILURE_EXIT_CODE
|
|
519
|
-
captured = capsys.readouterr()
|
|
520
|
-
assert "no parseable stdin lines" in captured.err
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
def test_invoke_gate_returns_infrastructure_failure_when_strict_resolve_raises(
|
|
524
|
-
tmp_path: Path,
|
|
525
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
526
|
-
) -> None:
|
|
527
|
-
gate_path = tmp_path / "gate.py"
|
|
528
|
-
gate_path.write_text("import sys\nsys.exit(0)\n", encoding="utf-8")
|
|
529
|
-
|
|
530
|
-
original_resolve = Path.resolve
|
|
531
|
-
|
|
532
|
-
def raising_resolve(self: Path, strict: bool = False) -> Path:
|
|
533
|
-
if strict and self == gate_path.resolve():
|
|
534
|
-
raise FileNotFoundError("not found")
|
|
535
|
-
return original_resolve(self, strict=strict)
|
|
536
|
-
|
|
537
|
-
monkeypatch.setattr(Path, "resolve", raising_resolve)
|
|
538
|
-
|
|
539
|
-
exit_code = pre_push.invoke_gate(gate_path, "origin/main")
|
|
540
|
-
|
|
541
|
-
assert exit_code == 2
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
def test_invoke_gate_uses_resolved_path(
|
|
545
|
-
tmp_path: Path,
|
|
546
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
547
|
-
) -> None:
|
|
548
|
-
real_gate_dir = tmp_path / "real"
|
|
549
|
-
real_gate_dir.mkdir()
|
|
550
|
-
real_gate_path = real_gate_dir / "gate.py"
|
|
551
|
-
recorded_path_file = tmp_path / "recorded_path.txt"
|
|
552
|
-
real_gate_path.write_text(
|
|
553
|
-
"import sys, pathlib\n"
|
|
554
|
-
f'pathlib.Path(r"{recorded_path_file}").write_text(sys.argv[0], encoding="utf-8")\n'
|
|
555
|
-
"sys.exit(0)\n",
|
|
556
|
-
encoding="utf-8",
|
|
557
|
-
)
|
|
558
|
-
symlink_gate_path = tmp_path / "link_gate.py"
|
|
559
|
-
symlink_gate_path.symlink_to(real_gate_path)
|
|
560
|
-
resolved_path = symlink_gate_path.resolve()
|
|
561
|
-
monkeypatch.setenv("CODE_RULES_GATE_PATH", str(symlink_gate_path))
|
|
562
|
-
_isolate_from_default_branch_refs(tmp_path, monkeypatch)
|
|
563
|
-
monkeypatch.setattr(sys, "stdin", io.StringIO(
|
|
564
|
-
f"refs/heads/feature {NON_ZERO_LOCAL_SHA} refs/heads/feature {NON_ZERO_REMOTE_SHA_ONE}\n"
|
|
565
|
-
))
|
|
566
|
-
|
|
567
|
-
exit_code = pre_push.main()
|
|
568
|
-
|
|
569
|
-
assert exit_code == 0
|
|
570
|
-
executed_path = recorded_path_file.read_text(encoding="utf-8")
|
|
571
|
-
assert executed_path == str(resolved_path)
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
def test_find_protected_branch_push_violation_flags_feature_branch_to_main() -> None:
|
|
575
|
-
stdin_text = (
|
|
576
|
-
f"refs/heads/feat/example {NON_ZERO_LOCAL_SHA} refs/heads/main {NON_ZERO_REMOTE_SHA_ONE}\n"
|
|
577
|
-
)
|
|
578
|
-
|
|
579
|
-
violation = pre_push.find_protected_branch_push_violation(stdin_text)
|
|
580
|
-
|
|
581
|
-
assert violation == ("feat/example", "main")
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
def test_find_protected_branch_push_violation_flags_feature_branch_to_master() -> None:
|
|
585
|
-
stdin_text = (
|
|
586
|
-
f"refs/heads/topic {NON_ZERO_LOCAL_SHA} refs/heads/master {NON_ZERO_REMOTE_SHA_ONE}\n"
|
|
587
|
-
)
|
|
588
|
-
|
|
589
|
-
violation = pre_push.find_protected_branch_push_violation(stdin_text)
|
|
590
|
-
|
|
591
|
-
assert violation == ("topic", "master")
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
def test_find_protected_branch_push_violation_allows_main_onto_main() -> None:
|
|
595
|
-
stdin_text = (
|
|
596
|
-
f"refs/heads/main {NON_ZERO_LOCAL_SHA} refs/heads/main {NON_ZERO_REMOTE_SHA_ONE}\n"
|
|
597
|
-
)
|
|
598
|
-
|
|
599
|
-
violation = pre_push.find_protected_branch_push_violation(stdin_text)
|
|
600
|
-
|
|
601
|
-
assert violation is None
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
def test_find_protected_branch_push_violation_allows_feature_onto_own_ref() -> None:
|
|
605
|
-
stdin_text = (
|
|
606
|
-
f"refs/heads/feat/example {NON_ZERO_LOCAL_SHA} refs/heads/feat/example {NON_ZERO_REMOTE_SHA_ONE}\n"
|
|
607
|
-
)
|
|
608
|
-
|
|
609
|
-
violation = pre_push.find_protected_branch_push_violation(stdin_text)
|
|
610
|
-
|
|
611
|
-
assert violation is None
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
def test_find_protected_branch_push_violation_ignores_deletion_of_main() -> None:
|
|
615
|
-
stdin_text = (
|
|
616
|
-
f"(delete) {ALL_ZEROS_OBJECT_NAME} refs/heads/main {NON_ZERO_REMOTE_SHA_ONE}\n"
|
|
617
|
-
)
|
|
618
|
-
|
|
619
|
-
violation = pre_push.find_protected_branch_push_violation(stdin_text)
|
|
620
|
-
|
|
621
|
-
assert violation is None
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
def test_main_blocks_feature_branch_push_onto_main(
|
|
625
|
-
tmp_path: Path,
|
|
626
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
627
|
-
capsys: pytest.CaptureFixture[str],
|
|
628
|
-
) -> None:
|
|
629
|
-
passing_gate_path = tmp_path / "gate.py"
|
|
630
|
-
passing_gate_path.write_text("import sys\nsys.exit(0)\n", encoding="utf-8")
|
|
631
|
-
monkeypatch.setenv("CODE_RULES_GATE_PATH", str(passing_gate_path))
|
|
632
|
-
monkeypatch.setattr(
|
|
633
|
-
sys,
|
|
634
|
-
"stdin",
|
|
635
|
-
io.StringIO(
|
|
636
|
-
f"refs/heads/feat/example {NON_ZERO_LOCAL_SHA} refs/heads/main {NON_ZERO_REMOTE_SHA_ONE}\n"
|
|
637
|
-
),
|
|
638
|
-
)
|
|
639
|
-
|
|
640
|
-
exit_code = pre_push.main()
|
|
641
|
-
|
|
642
|
-
assert exit_code == git_hooks_constants.PROTECTED_BRANCH_PUSH_BLOCK_EXIT_CODE
|
|
643
|
-
captured = capsys.readouterr()
|
|
644
|
-
assert "feat/example" in captured.err
|
|
645
|
-
assert "main" in captured.err
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
def test_main_blocks_protected_push_even_when_gate_script_missing(
|
|
649
|
-
tmp_path: Path,
|
|
650
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
651
|
-
capsys: pytest.CaptureFixture[str],
|
|
652
|
-
) -> None:
|
|
653
|
-
monkeypatch.setenv(
|
|
654
|
-
"CODE_RULES_GATE_PATH",
|
|
655
|
-
str(tmp_path / "does_not_exist.py"),
|
|
656
|
-
)
|
|
657
|
-
monkeypatch.setattr(
|
|
658
|
-
sys,
|
|
659
|
-
"stdin",
|
|
660
|
-
io.StringIO(
|
|
661
|
-
f"refs/heads/feat/example {NON_ZERO_LOCAL_SHA} refs/heads/main {NON_ZERO_REMOTE_SHA_ONE}\n"
|
|
662
|
-
),
|
|
663
|
-
)
|
|
664
|
-
|
|
665
|
-
exit_code = pre_push.main()
|
|
666
|
-
|
|
667
|
-
assert exit_code == git_hooks_constants.PROTECTED_BRANCH_PUSH_BLOCK_EXIT_CODE
|
|
668
|
-
captured = capsys.readouterr()
|
|
669
|
-
assert "feat/example" in captured.err
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
def test_main_allows_main_onto_main_push(
|
|
673
|
-
tmp_path: Path,
|
|
674
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
675
|
-
) -> None:
|
|
676
|
-
passing_gate_path = tmp_path / "gate.py"
|
|
677
|
-
passing_gate_path.write_text("import sys\nsys.exit(0)\n", encoding="utf-8")
|
|
678
|
-
monkeypatch.setenv("CODE_RULES_GATE_PATH", str(passing_gate_path))
|
|
679
|
-
monkeypatch.setattr(
|
|
680
|
-
sys,
|
|
681
|
-
"stdin",
|
|
682
|
-
io.StringIO(
|
|
683
|
-
f"refs/heads/main {NON_ZERO_LOCAL_SHA} refs/heads/main {NON_ZERO_REMOTE_SHA_ONE}\n"
|
|
684
|
-
),
|
|
685
|
-
)
|
|
686
|
-
|
|
687
|
-
exit_code = pre_push.main()
|
|
688
|
-
|
|
689
|
-
assert exit_code == 0
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
def _write_passing_code_rules_gate(
|
|
694
|
-
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
695
|
-
) -> None:
|
|
696
|
-
"""Install a passing CODE_RULES gate and a one-line feature push on stdin."""
|
|
697
|
-
passing_gate_path = tmp_path / "code_rules_gate.py"
|
|
698
|
-
passing_gate_path.write_text("import sys\nsys.exit(0)\n", encoding="utf-8")
|
|
699
|
-
monkeypatch.setenv("CODE_RULES_GATE_PATH", str(passing_gate_path))
|
|
700
|
-
monkeypatch.setattr(
|
|
701
|
-
sys,
|
|
702
|
-
"stdin",
|
|
703
|
-
io.StringIO(
|
|
704
|
-
f"refs/heads/feature {NON_ZERO_LOCAL_SHA} refs/heads/feature {NON_ZERO_REMOTE_SHA_ONE}\n"
|
|
705
|
-
),
|
|
706
|
-
)
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
def _isolate_repository_and_write_passing_code_rules_gate(
|
|
710
|
-
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
711
|
-
) -> None:
|
|
712
|
-
"""Move into a repository without origin refs, then install a passing gate."""
|
|
713
|
-
_isolate_from_default_branch_refs(tmp_path, monkeypatch)
|
|
714
|
-
_write_passing_code_rules_gate(tmp_path, monkeypatch)
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
DEFAULT_BRANCH_NAME: str = "main"
|
|
718
|
-
FEATURE_BRANCH_NAME: str = "feature/example"
|
|
719
|
-
UNPROTECTED_DEFAULT_BRANCH_NAME: str = "develop"
|
|
720
|
-
ORIGIN_DEFAULT_BRANCH_REFERENCE: str = (
|
|
721
|
-
git_hooks_constants.ORIGIN_REMOTE_TRACKING_REFERENCE_PREFIX + DEFAULT_BRANCH_NAME
|
|
722
|
-
)
|
|
723
|
-
ORIGIN_UNPROTECTED_DEFAULT_BRANCH_REFERENCE: str = (
|
|
724
|
-
git_hooks_constants.ORIGIN_REMOTE_TRACKING_REFERENCE_PREFIX
|
|
725
|
-
+ UNPROTECTED_DEFAULT_BRANCH_NAME
|
|
726
|
-
)
|
|
727
|
-
ORIGIN_HEAD_REFERENCE: str = git_hooks_constants.ORIGIN_HEAD_SYMBOLIC_REFERENCE
|
|
728
|
-
LOCAL_ALIAS_BRANCH_NAME: str = "release-candidate"
|
|
729
|
-
TOPIC_REMOTE_BRANCH_NAME: str = "topic-x"
|
|
730
|
-
BASE_FILE_NAME: str = "README.md"
|
|
731
|
-
FLAGGED_FILE_NAME: str = "flagged_module.py"
|
|
732
|
-
BRANCH_FILE_NAME: str = "branch_module.py"
|
|
733
|
-
DEFAULT_BRANCH_FILE_NAME: str = "default_branch_module.py"
|
|
734
|
-
ALL_FIXTURE_GIT_FLAGS: tuple[str, ...] = (
|
|
735
|
-
"-c",
|
|
736
|
-
"user.name=hooks-test",
|
|
737
|
-
"-c",
|
|
738
|
-
"user.email=hooks-test@example.invalid",
|
|
739
|
-
"-c",
|
|
740
|
-
"commit.gpgsign=false",
|
|
741
|
-
"-c",
|
|
742
|
-
"core.hooksPath=absent_fixture_hooks",
|
|
743
|
-
)
|
|
744
|
-
FIXTURE_GIT_TIMEOUT_SECONDS: int = 60
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
def _run_fixture_git(repository: Path, *all_arguments: str) -> str:
|
|
748
|
-
"""Run one git command inside a fixture repository, isolated from user hooks."""
|
|
749
|
-
completion = subprocess.run(
|
|
750
|
-
["git", *ALL_FIXTURE_GIT_FLAGS, *all_arguments],
|
|
751
|
-
cwd=str(repository),
|
|
752
|
-
capture_output=True,
|
|
753
|
-
text=True,
|
|
754
|
-
check=True,
|
|
755
|
-
timeout=FIXTURE_GIT_TIMEOUT_SECONDS,
|
|
756
|
-
)
|
|
757
|
-
return completion.stdout.strip()
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
def _commit_file(repository: Path, file_name: str) -> str:
|
|
761
|
-
"""Write and commit one file, returning the resulting commit object name."""
|
|
762
|
-
(repository / file_name).write_text(f"body of {file_name}\n", encoding="utf-8")
|
|
763
|
-
_run_fixture_git(repository, "add", file_name)
|
|
764
|
-
_run_fixture_git(repository, "commit", "-m", f"add {file_name}")
|
|
765
|
-
return _run_fixture_git(repository, "rev-parse", "HEAD")
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
def _set_origin_default_branch_tip(
|
|
769
|
-
repository: Path, default_branch_reference: str, default_branch_tip: str
|
|
770
|
-
) -> None:
|
|
771
|
-
"""Point an origin default-branch remote-tracking ref at a commit."""
|
|
772
|
-
_run_fixture_git(
|
|
773
|
-
repository, "update-ref", default_branch_reference, default_branch_tip
|
|
774
|
-
)
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
def _set_origin_head(repository: Path, default_branch_reference: str) -> None:
|
|
778
|
-
"""Point the origin HEAD symbolic ref at an origin default-branch ref."""
|
|
779
|
-
_run_fixture_git(
|
|
780
|
-
repository, "symbolic-ref", ORIGIN_HEAD_REFERENCE, default_branch_reference
|
|
781
|
-
)
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
def _push_stdin_line(
|
|
785
|
-
local_branch_name: str,
|
|
786
|
-
pushed_object_name: str,
|
|
787
|
-
remote_branch_name: str,
|
|
788
|
-
remote_object_name: str,
|
|
789
|
-
) -> str:
|
|
790
|
-
"""Build the pre-push stdin line git writes for one branch update."""
|
|
791
|
-
return (
|
|
792
|
-
f"refs/heads/{local_branch_name} {pushed_object_name} "
|
|
793
|
-
f"refs/heads/{remote_branch_name} {remote_object_name}\n"
|
|
794
|
-
)
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
def _build_rebased_branch_repository(
|
|
798
|
-
tmp_path: Path, default_branch_file_name: str, branch_file_name: str
|
|
799
|
-
) -> tuple[Path, str, str]:
|
|
800
|
-
"""Build a repository whose feature branch sits rebased onto an advanced main.
|
|
801
|
-
|
|
802
|
-
Args:
|
|
803
|
-
tmp_path: The pytest temporary directory the repository is built in.
|
|
804
|
-
default_branch_file_name: File the default branch gains after the branch forks.
|
|
805
|
-
branch_file_name: File the feature branch adds.
|
|
806
|
-
|
|
807
|
-
Returns:
|
|
808
|
-
The repository path, the rebased branch tip, and the pre-rebase tip the
|
|
809
|
-
remote still holds. The origin HEAD symbolic ref stays unset.
|
|
810
|
-
"""
|
|
811
|
-
repository = tmp_path / "rebased_branch_repository"
|
|
812
|
-
repository.mkdir()
|
|
813
|
-
_run_fixture_git(repository, "init", "-b", DEFAULT_BRANCH_NAME)
|
|
814
|
-
_commit_file(repository, BASE_FILE_NAME)
|
|
815
|
-
_run_fixture_git(repository, "checkout", "-b", FEATURE_BRANCH_NAME)
|
|
816
|
-
pre_rebase_tip = _commit_file(repository, branch_file_name)
|
|
817
|
-
_run_fixture_git(repository, "checkout", DEFAULT_BRANCH_NAME)
|
|
818
|
-
_set_origin_default_branch_tip(
|
|
819
|
-
repository,
|
|
820
|
-
ORIGIN_DEFAULT_BRANCH_REFERENCE,
|
|
821
|
-
_commit_file(repository, default_branch_file_name),
|
|
822
|
-
)
|
|
823
|
-
_run_fixture_git(repository, "checkout", FEATURE_BRANCH_NAME)
|
|
824
|
-
_run_fixture_git(repository, "rebase", DEFAULT_BRANCH_NAME)
|
|
825
|
-
return repository, _run_fixture_git(repository, "rev-parse", "HEAD"), pre_rebase_tip
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
def _write_diff_scoped_gate(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
829
|
-
"""Install a gate that flags the marker file when it sits in the base..HEAD diff."""
|
|
830
|
-
gate_script_path = tmp_path / "diff_scoped_gate.py"
|
|
831
|
-
gate_script_path.write_text(
|
|
832
|
-
"import subprocess\n"
|
|
833
|
-
"import sys\n"
|
|
834
|
-
"base_reference = sys.argv[2]\n"
|
|
835
|
-
"merge_base = subprocess.run(\n"
|
|
836
|
-
" ['git', 'merge-base', 'HEAD', base_reference],\n"
|
|
837
|
-
" capture_output=True, text=True, check=True,\n"
|
|
838
|
-
").stdout.strip()\n"
|
|
839
|
-
"all_changed_names = subprocess.run(\n"
|
|
840
|
-
" ['git', 'diff', '--name-only', merge_base, 'HEAD'],\n"
|
|
841
|
-
" capture_output=True, text=True, check=True,\n"
|
|
842
|
-
").stdout.split()\n"
|
|
843
|
-
f"sys.exit(1 if {FLAGGED_FILE_NAME!r} in all_changed_names else 0)\n",
|
|
844
|
-
encoding="utf-8",
|
|
845
|
-
)
|
|
846
|
-
monkeypatch.setenv("CODE_RULES_GATE_PATH", str(gate_script_path))
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
def _build_default_branch_repository(tmp_path: Path) -> tuple[Path, str, str]:
|
|
850
|
-
"""Build a repository with an origin default branch and a topic branch off it.
|
|
851
|
-
|
|
852
|
-
Args:
|
|
853
|
-
tmp_path: The pytest temporary directory the repository is built in.
|
|
854
|
-
|
|
855
|
-
Returns:
|
|
856
|
-
The repository path, the default branch tip, and the topic branch tip.
|
|
857
|
-
"""
|
|
858
|
-
repository = tmp_path / "default_branch_repository"
|
|
859
|
-
repository.mkdir()
|
|
860
|
-
_run_fixture_git(repository, "init", "-b", DEFAULT_BRANCH_NAME)
|
|
861
|
-
_commit_file(repository, BASE_FILE_NAME)
|
|
862
|
-
default_branch_tip = _commit_file(repository, DEFAULT_BRANCH_FILE_NAME)
|
|
863
|
-
_run_fixture_git(repository, "checkout", "-b", FEATURE_BRANCH_NAME)
|
|
864
|
-
topic_branch_tip = _commit_file(repository, BRANCH_FILE_NAME)
|
|
865
|
-
_set_origin_default_branch_tip(
|
|
866
|
-
repository, ORIGIN_DEFAULT_BRANCH_REFERENCE, default_branch_tip
|
|
867
|
-
)
|
|
868
|
-
_set_origin_head(repository, ORIGIN_DEFAULT_BRANCH_REFERENCE)
|
|
869
|
-
return repository, default_branch_tip, topic_branch_tip
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
def _isolate_from_default_branch_refs(
|
|
873
|
-
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
874
|
-
) -> None:
|
|
875
|
-
"""Move the test into a fresh repository that holds no origin default branch.
|
|
876
|
-
|
|
877
|
-
Tests that feed synthetic object names need a working directory where no
|
|
878
|
-
default-branch ref resolves, so the hook takes its stdin remote-object
|
|
879
|
-
fallback rather than reading the ambient repository's origin refs.
|
|
880
|
-
"""
|
|
881
|
-
isolated_repository = tmp_path / "repository_without_origin_refs"
|
|
882
|
-
isolated_repository.mkdir()
|
|
883
|
-
_run_fixture_git(isolated_repository, "init", "-b", DEFAULT_BRANCH_NAME)
|
|
884
|
-
monkeypatch.chdir(isolated_repository)
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
def _build_unrelated_history_repository(tmp_path: Path) -> tuple[Path, str]:
|
|
888
|
-
"""Build a repository whose feature branch shares no history with the default.
|
|
889
|
-
|
|
890
|
-
The origin default-branch ref and the origin HEAD symbolic ref both resolve,
|
|
891
|
-
so ``git merge-base`` runs and reports no merge base rather than the hook
|
|
892
|
-
failing to find a default branch at all.
|
|
893
|
-
|
|
894
|
-
Args:
|
|
895
|
-
tmp_path: The pytest temporary directory the repository is built in.
|
|
896
|
-
|
|
897
|
-
Returns:
|
|
898
|
-
The repository path and the orphan feature branch tip.
|
|
899
|
-
"""
|
|
900
|
-
repository = tmp_path / "unrelated_history_repository"
|
|
901
|
-
repository.mkdir()
|
|
902
|
-
_run_fixture_git(repository, "init", "-b", DEFAULT_BRANCH_NAME)
|
|
903
|
-
_commit_file(repository, BASE_FILE_NAME)
|
|
904
|
-
default_branch_tip = _commit_file(repository, DEFAULT_BRANCH_FILE_NAME)
|
|
905
|
-
_run_fixture_git(repository, "checkout", "--orphan", FEATURE_BRANCH_NAME)
|
|
906
|
-
_run_fixture_git(repository, "reset")
|
|
907
|
-
orphan_branch_tip = _commit_file(repository, BRANCH_FILE_NAME)
|
|
908
|
-
_set_origin_default_branch_tip(
|
|
909
|
-
repository, ORIGIN_DEFAULT_BRANCH_REFERENCE, default_branch_tip
|
|
910
|
-
)
|
|
911
|
-
_set_origin_head(repository, ORIGIN_DEFAULT_BRANCH_REFERENCE)
|
|
912
|
-
return repository, orphan_branch_tip
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
def _same_branch_push_line(
|
|
916
|
-
local_object_name: str, remote_object_name: str
|
|
917
|
-
) -> pre_push.PushLine:
|
|
918
|
-
"""Build a PushLine whose local and remote refs name the same branch."""
|
|
919
|
-
return pre_push.PushLine(
|
|
920
|
-
local_branch_name=FEATURE_BRANCH_NAME,
|
|
921
|
-
local_object_name=local_object_name,
|
|
922
|
-
remote_branch_name=FEATURE_BRANCH_NAME,
|
|
923
|
-
remote_object_name=remote_object_name,
|
|
924
|
-
)
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
def test_parse_push_line_reads_both_branch_names_and_both_object_names() -> None:
|
|
928
|
-
parsed_line = pre_push.parse_push_line(
|
|
929
|
-
f"refs/heads/{FEATURE_BRANCH_NAME} {NON_ZERO_LOCAL_SHA} "
|
|
930
|
-
f"refs/heads/{DEFAULT_BRANCH_NAME} {NON_ZERO_REMOTE_SHA_ONE}"
|
|
931
|
-
)
|
|
932
|
-
|
|
933
|
-
assert parsed_line == pre_push.PushLine(
|
|
934
|
-
local_branch_name=FEATURE_BRANCH_NAME,
|
|
935
|
-
local_object_name=NON_ZERO_LOCAL_SHA,
|
|
936
|
-
remote_branch_name=DEFAULT_BRANCH_NAME,
|
|
937
|
-
remote_object_name=NON_ZERO_REMOTE_SHA_ONE,
|
|
938
|
-
)
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
def test_parse_push_line_returns_none_for_a_line_missing_fields() -> None:
|
|
942
|
-
assert pre_push.parse_push_line("refs/heads/feature one_more_field") is None
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
def test_is_branch_update_separates_updates_from_deletions_and_new_branches() -> None:
|
|
946
|
-
assert pre_push.is_branch_update(
|
|
947
|
-
_same_branch_push_line(NON_ZERO_LOCAL_SHA, NON_ZERO_REMOTE_SHA_ONE)
|
|
948
|
-
)
|
|
949
|
-
assert not pre_push.is_branch_update(
|
|
950
|
-
_same_branch_push_line(ALL_ZEROS_OBJECT_NAME, NON_ZERO_REMOTE_SHA_ONE)
|
|
951
|
-
)
|
|
952
|
-
assert not pre_push.is_branch_update(
|
|
953
|
-
_same_branch_push_line(NON_ZERO_LOCAL_SHA, ALL_ZEROS_OBJECT_NAME)
|
|
954
|
-
)
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
def test_run_git_reference_query_returns_stdout_for_a_resolvable_reference(
|
|
958
|
-
tmp_path: Path,
|
|
959
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
960
|
-
) -> None:
|
|
961
|
-
repository, default_branch_tip, _topic_branch_tip = _build_default_branch_repository(
|
|
962
|
-
tmp_path
|
|
963
|
-
)
|
|
964
|
-
monkeypatch.chdir(repository)
|
|
965
|
-
|
|
966
|
-
resolved_object_name = pre_push.run_git_reference_query(
|
|
967
|
-
("git", "rev-parse", ORIGIN_DEFAULT_BRANCH_REFERENCE)
|
|
968
|
-
)
|
|
969
|
-
|
|
970
|
-
assert resolved_object_name == default_branch_tip
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
def test_run_git_reference_query_returns_none_for_an_absent_reference(
|
|
974
|
-
tmp_path: Path,
|
|
975
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
976
|
-
) -> None:
|
|
977
|
-
repository, _default_branch_tip, _topic_branch_tip = (
|
|
978
|
-
_build_default_branch_repository(tmp_path)
|
|
979
|
-
)
|
|
980
|
-
monkeypatch.chdir(repository)
|
|
981
|
-
|
|
982
|
-
resolved_object_name = pre_push.run_git_reference_query(
|
|
983
|
-
("git", "rev-parse", "--verify", "--quiet", "refs/remotes/origin/absent")
|
|
984
|
-
)
|
|
985
|
-
|
|
986
|
-
assert resolved_object_name is None
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
def test_resolve_default_branch_reference_reads_the_origin_head_symbolic_reference(
|
|
990
|
-
tmp_path: Path,
|
|
991
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
992
|
-
) -> None:
|
|
993
|
-
repository, _default_branch_tip, _topic_branch_tip = (
|
|
994
|
-
_build_default_branch_repository(tmp_path)
|
|
995
|
-
)
|
|
996
|
-
monkeypatch.chdir(repository)
|
|
997
|
-
|
|
998
|
-
assert pre_push.resolve_default_branch_reference() == ORIGIN_DEFAULT_BRANCH_REFERENCE
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
def test_resolve_default_branch_reference_reads_the_requested_remote_head(
|
|
1002
|
-
tmp_path: Path,
|
|
1003
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
1004
|
-
) -> None:
|
|
1005
|
-
repository, default_branch_tip, _topic_branch_tip = _build_default_branch_repository(
|
|
1006
|
-
tmp_path
|
|
1007
|
-
)
|
|
1008
|
-
upstream_reference = "refs/remotes/upstream/main"
|
|
1009
|
-
upstream_head_reference = "refs/remotes/upstream/HEAD"
|
|
1010
|
-
_run_fixture_git(repository, "update-ref", upstream_reference, default_branch_tip)
|
|
1011
|
-
_run_fixture_git(
|
|
1012
|
-
repository, "symbolic-ref", upstream_head_reference, upstream_reference
|
|
1013
|
-
)
|
|
1014
|
-
monkeypatch.chdir(repository)
|
|
1015
|
-
|
|
1016
|
-
assert pre_push.resolve_default_branch_reference("upstream") == upstream_reference
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
def test_resolve_default_branch_merge_base_uses_requested_remote_trunk_fallback(
|
|
1020
|
-
tmp_path: Path,
|
|
1021
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
1022
|
-
) -> None:
|
|
1023
|
-
repository, default_branch_tip, topic_branch_tip = _build_default_branch_repository(
|
|
1024
|
-
tmp_path
|
|
1025
|
-
)
|
|
1026
|
-
upstream_trunk_reference = "refs/remotes/upstream/trunk"
|
|
1027
|
-
_run_fixture_git(
|
|
1028
|
-
repository, "update-ref", upstream_trunk_reference, default_branch_tip
|
|
1029
|
-
)
|
|
1030
|
-
monkeypatch.chdir(repository)
|
|
1031
|
-
|
|
1032
|
-
merge_base_object_name = pre_push.resolve_default_branch_merge_base(
|
|
1033
|
-
FEATURE_BRANCH_NAME,
|
|
1034
|
-
topic_branch_tip,
|
|
1035
|
-
"upstream",
|
|
1036
|
-
)
|
|
1037
|
-
|
|
1038
|
-
assert merge_base_object_name == default_branch_tip
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
def test_resolve_default_branch_reference_returns_none_without_remote_tracking_refs(
|
|
1042
|
-
tmp_path: Path,
|
|
1043
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
1044
|
-
) -> None:
|
|
1045
|
-
repository = tmp_path / "repository_without_remote_tracking_refs"
|
|
1046
|
-
repository.mkdir()
|
|
1047
|
-
_run_fixture_git(repository, "init", "-b", DEFAULT_BRANCH_NAME)
|
|
1048
|
-
_commit_file(repository, BASE_FILE_NAME)
|
|
1049
|
-
monkeypatch.chdir(repository)
|
|
1050
|
-
|
|
1051
|
-
assert pre_push.resolve_default_branch_reference() is None
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
def test_find_branch_update_push_returns_the_update_that_follows_a_deletion_line() -> (
|
|
1055
|
-
None
|
|
1056
|
-
):
|
|
1057
|
-
stdin_text = (
|
|
1058
|
-
f"(delete) {ALL_ZEROS_OBJECT_NAME} refs/heads/retired {NON_ZERO_REMOTE_SHA_ONE}\n"
|
|
1059
|
-
+ _push_stdin_line(
|
|
1060
|
-
FEATURE_BRANCH_NAME,
|
|
1061
|
-
NON_ZERO_LOCAL_SHA,
|
|
1062
|
-
DEFAULT_BRANCH_NAME,
|
|
1063
|
-
NON_ZERO_REMOTE_SHA_TWO,
|
|
1064
|
-
)
|
|
1065
|
-
)
|
|
1066
|
-
|
|
1067
|
-
branch_update = pre_push.find_branch_update_push(stdin_text)
|
|
1068
|
-
|
|
1069
|
-
assert branch_update == pre_push.PushLine(
|
|
1070
|
-
local_branch_name=FEATURE_BRANCH_NAME,
|
|
1071
|
-
local_object_name=NON_ZERO_LOCAL_SHA,
|
|
1072
|
-
remote_branch_name=DEFAULT_BRANCH_NAME,
|
|
1073
|
-
remote_object_name=NON_ZERO_REMOTE_SHA_TWO,
|
|
1074
|
-
)
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
def test_find_branch_update_push_returns_none_when_every_line_is_a_deletion() -> None:
|
|
1078
|
-
deletion_only_stdin = _push_stdin_line(
|
|
1079
|
-
FEATURE_BRANCH_NAME,
|
|
1080
|
-
ALL_ZEROS_OBJECT_NAME,
|
|
1081
|
-
FEATURE_BRANCH_NAME,
|
|
1082
|
-
NON_ZERO_REMOTE_SHA_ONE,
|
|
1083
|
-
)
|
|
1084
|
-
|
|
1085
|
-
assert pre_push.find_branch_update_push(deletion_only_stdin) is None
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
def test_resolve_default_branch_merge_base_returns_the_fork_point(
|
|
1089
|
-
tmp_path: Path,
|
|
1090
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
1091
|
-
) -> None:
|
|
1092
|
-
repository, default_branch_tip, topic_branch_tip = _build_default_branch_repository(
|
|
1093
|
-
tmp_path
|
|
1094
|
-
)
|
|
1095
|
-
monkeypatch.chdir(repository)
|
|
1096
|
-
|
|
1097
|
-
merge_base_object_name = pre_push.resolve_default_branch_merge_base(
|
|
1098
|
-
FEATURE_BRANCH_NAME, topic_branch_tip
|
|
1099
|
-
)
|
|
1100
|
-
|
|
1101
|
-
assert merge_base_object_name == default_branch_tip
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
def test_resolve_default_branch_merge_base_returns_none_for_a_default_branch_update(
|
|
1105
|
-
tmp_path: Path,
|
|
1106
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
1107
|
-
) -> None:
|
|
1108
|
-
repository, _default_branch_tip, topic_branch_tip = (
|
|
1109
|
-
_build_default_branch_repository(tmp_path)
|
|
1110
|
-
)
|
|
1111
|
-
monkeypatch.chdir(repository)
|
|
1112
|
-
|
|
1113
|
-
merge_base_object_name = pre_push.resolve_default_branch_merge_base(
|
|
1114
|
-
DEFAULT_BRANCH_NAME, topic_branch_tip
|
|
1115
|
-
)
|
|
1116
|
-
|
|
1117
|
-
assert merge_base_object_name is None
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
def test_resolve_gate_base_reference_returns_the_default_branch_merge_base(
|
|
1121
|
-
tmp_path: Path,
|
|
1122
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
1123
|
-
) -> None:
|
|
1124
|
-
repository, default_branch_tip, topic_branch_tip = _build_default_branch_repository(
|
|
1125
|
-
tmp_path
|
|
1126
|
-
)
|
|
1127
|
-
monkeypatch.chdir(repository)
|
|
1128
|
-
stdin_text = _push_stdin_line(
|
|
1129
|
-
FEATURE_BRANCH_NAME,
|
|
1130
|
-
topic_branch_tip,
|
|
1131
|
-
FEATURE_BRANCH_NAME,
|
|
1132
|
-
NON_ZERO_REMOTE_SHA_ONE,
|
|
1133
|
-
)
|
|
1134
|
-
|
|
1135
|
-
assert pre_push.resolve_gate_base_reference(stdin_text) == default_branch_tip
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
def test_resolve_gate_base_reference_returns_none_for_a_deletion_only_push() -> None:
|
|
1139
|
-
deletion_only_stdin = _push_stdin_line(
|
|
1140
|
-
FEATURE_BRANCH_NAME,
|
|
1141
|
-
ALL_ZEROS_OBJECT_NAME,
|
|
1142
|
-
FEATURE_BRANCH_NAME,
|
|
1143
|
-
ALL_ZEROS_OBJECT_NAME,
|
|
1144
|
-
)
|
|
1145
|
-
|
|
1146
|
-
assert pre_push.resolve_gate_base_reference(deletion_only_stdin) is None
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
def test_resolve_gate_base_reference_flags_an_unresolvable_merge_base(
|
|
1150
|
-
tmp_path: Path,
|
|
1151
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
1152
|
-
) -> None:
|
|
1153
|
-
repository, orphan_branch_tip = _build_unrelated_history_repository(tmp_path)
|
|
1154
|
-
monkeypatch.chdir(repository)
|
|
1155
|
-
assert (
|
|
1156
|
-
pre_push.run_git_reference_query(
|
|
1157
|
-
("git", "merge-base", orphan_branch_tip, ORIGIN_DEFAULT_BRANCH_REFERENCE)
|
|
1158
|
-
)
|
|
1159
|
-
is None
|
|
1160
|
-
), "fixture branch shares history with the default branch"
|
|
1161
|
-
stdin_text = _push_stdin_line(
|
|
1162
|
-
FEATURE_BRANCH_NAME,
|
|
1163
|
-
orphan_branch_tip,
|
|
1164
|
-
FEATURE_BRANCH_NAME,
|
|
1165
|
-
NON_ZERO_REMOTE_SHA_ONE,
|
|
1166
|
-
)
|
|
1167
|
-
|
|
1168
|
-
base_reference = pre_push.resolve_gate_base_reference(stdin_text)
|
|
1169
|
-
|
|
1170
|
-
assert base_reference == git_hooks_constants.UNRESOLVABLE_MERGE_BASE_SENTINEL
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
def test_resolve_gate_base_reference_treats_a_dangling_origin_head_as_no_default(
|
|
1174
|
-
tmp_path: Path,
|
|
1175
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
1176
|
-
) -> None:
|
|
1177
|
-
"""A symbolic origin/HEAD whose target is missing falls through to no default branch ref.
|
|
1178
|
-
|
|
1179
|
-
The gate base then uses the stdin remote object, the same path as when no
|
|
1180
|
-
default-branch ref resolves at all.
|
|
1181
|
-
"""
|
|
1182
|
-
repository = tmp_path / "dangling_origin_head_repository"
|
|
1183
|
-
repository.mkdir()
|
|
1184
|
-
_run_fixture_git(repository, "init", "-b", DEFAULT_BRANCH_NAME)
|
|
1185
|
-
pushed_object_name = _commit_file(repository, BASE_FILE_NAME)
|
|
1186
|
-
_set_origin_head(repository, ORIGIN_DEFAULT_BRANCH_REFERENCE)
|
|
1187
|
-
monkeypatch.chdir(repository)
|
|
1188
|
-
stdin_text = _push_stdin_line(
|
|
1189
|
-
FEATURE_BRANCH_NAME,
|
|
1190
|
-
pushed_object_name,
|
|
1191
|
-
FEATURE_BRANCH_NAME,
|
|
1192
|
-
NON_ZERO_REMOTE_SHA_ONE,
|
|
1193
|
-
)
|
|
1194
|
-
|
|
1195
|
-
base_reference = pre_push.resolve_gate_base_reference(stdin_text)
|
|
1196
|
-
|
|
1197
|
-
assert base_reference == NON_ZERO_REMOTE_SHA_ONE
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
def test_main_allows_the_push_when_origin_head_names_an_absent_reference(
|
|
1201
|
-
tmp_path: Path,
|
|
1202
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
1203
|
-
) -> None:
|
|
1204
|
-
"""Dangling origin/HEAD does not skip the gate; it uses the remote object base."""
|
|
1205
|
-
repository = tmp_path / "dangling_origin_head_push_repository"
|
|
1206
|
-
repository.mkdir()
|
|
1207
|
-
_run_fixture_git(repository, "init", "-b", DEFAULT_BRANCH_NAME)
|
|
1208
|
-
remote_object_name = _commit_file(repository, BASE_FILE_NAME)
|
|
1209
|
-
_run_fixture_git(repository, "checkout", "-b", FEATURE_BRANCH_NAME)
|
|
1210
|
-
pushed_object_name = _commit_file(repository, BRANCH_FILE_NAME)
|
|
1211
|
-
_set_origin_head(repository, ORIGIN_DEFAULT_BRANCH_REFERENCE)
|
|
1212
|
-
recorded_arguments_path = tmp_path / "recorded_arguments_dangling.txt"
|
|
1213
|
-
recording_gate_script_path = tmp_path / "recording_gate_dangling.py"
|
|
1214
|
-
recording_gate_script_path.write_text(
|
|
1215
|
-
"import sys, pathlib\n"
|
|
1216
|
-
f'pathlib.Path(r"{recorded_arguments_path}").write_text('
|
|
1217
|
-
"'\\n'.join(sys.argv[1:]), encoding='utf-8')\n"
|
|
1218
|
-
"sys.exit(0)\n",
|
|
1219
|
-
encoding="utf-8",
|
|
1220
|
-
)
|
|
1221
|
-
monkeypatch.setenv("CODE_RULES_GATE_PATH", str(recording_gate_script_path))
|
|
1222
|
-
monkeypatch.chdir(repository)
|
|
1223
|
-
monkeypatch.setattr(
|
|
1224
|
-
sys,
|
|
1225
|
-
"stdin",
|
|
1226
|
-
io.StringIO(
|
|
1227
|
-
_push_stdin_line(
|
|
1228
|
-
FEATURE_BRANCH_NAME,
|
|
1229
|
-
pushed_object_name,
|
|
1230
|
-
FEATURE_BRANCH_NAME,
|
|
1231
|
-
remote_object_name,
|
|
1232
|
-
)
|
|
1233
|
-
),
|
|
1234
|
-
)
|
|
1235
|
-
|
|
1236
|
-
exit_code = pre_push.main()
|
|
1237
|
-
|
|
1238
|
-
assert exit_code == 0
|
|
1239
|
-
recorded_arguments = recorded_arguments_path.read_text(
|
|
1240
|
-
encoding="utf-8"
|
|
1241
|
-
).splitlines()
|
|
1242
|
-
assert recorded_arguments == ["--base", remote_object_name]
|
|
7
|
+
import pre_push
|
|
8
|
+
import pytest
|
|
9
|
+
from test_native_hook_support import (
|
|
10
|
+
configure_remote_url_rewrite,
|
|
11
|
+
create_bare_repository,
|
|
12
|
+
create_native_repository,
|
|
13
|
+
install_native_hook,
|
|
14
|
+
read_git_head,
|
|
15
|
+
run_git,
|
|
16
|
+
run_native_git_action,
|
|
17
|
+
)
|
|
1243
18
|
|
|
1244
19
|
|
|
1245
|
-
def
|
|
1246
|
-
tmp_path: Path,
|
|
20
|
+
def test_main_keeps_protected_destination_as_advisory(
|
|
1247
21
|
monkeypatch: pytest.MonkeyPatch,
|
|
1248
22
|
capsys: pytest.CaptureFixture[str],
|
|
1249
23
|
) -> None:
|
|
1250
|
-
repository, orphan_branch_tip = _build_unrelated_history_repository(tmp_path)
|
|
1251
|
-
recorded_arguments_path = tmp_path / "unreached_gate_arguments.txt"
|
|
1252
|
-
recording_gate_script_path = tmp_path / "unreached_recording_gate.py"
|
|
1253
|
-
recording_gate_script_path.write_text(
|
|
1254
|
-
"import sys, pathlib\n"
|
|
1255
|
-
f'pathlib.Path(r"{recorded_arguments_path}").write_text('
|
|
1256
|
-
"'\\n'.join(sys.argv[1:]), encoding='utf-8')\n"
|
|
1257
|
-
"sys.exit(0)\n",
|
|
1258
|
-
encoding="utf-8",
|
|
1259
|
-
)
|
|
1260
|
-
monkeypatch.setenv("CODE_RULES_GATE_PATH", str(recording_gate_script_path))
|
|
1261
|
-
monkeypatch.chdir(repository)
|
|
1262
|
-
monkeypatch.setattr(
|
|
1263
|
-
sys,
|
|
1264
|
-
"stdin",
|
|
1265
|
-
io.StringIO(
|
|
1266
|
-
_push_stdin_line(
|
|
1267
|
-
FEATURE_BRANCH_NAME,
|
|
1268
|
-
orphan_branch_tip,
|
|
1269
|
-
FEATURE_BRANCH_NAME,
|
|
1270
|
-
NON_ZERO_REMOTE_SHA_ONE,
|
|
1271
|
-
)
|
|
1272
|
-
),
|
|
1273
|
-
)
|
|
1274
|
-
|
|
1275
|
-
exit_code = pre_push.main()
|
|
1276
|
-
|
|
1277
|
-
assert exit_code == git_hooks_constants.GATE_INFRASTRUCTURE_FAILURE_EXIT_CODE
|
|
1278
|
-
assert not recorded_arguments_path.exists(), (
|
|
1279
|
-
"the gate remains pending until the hook verifies a usable base"
|
|
1280
|
-
)
|
|
1281
|
-
assert git_hooks_constants.UNRESOLVABLE_MERGE_BASE_MESSAGE in capsys.readouterr().err
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
def test_main_uses_the_pushed_remote_when_origin_has_unrelated_history(
|
|
1285
|
-
tmp_path: Path,
|
|
1286
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
1287
|
-
) -> None:
|
|
1288
|
-
repository, default_branch_tip, topic_branch_tip = _build_default_branch_repository(
|
|
1289
|
-
tmp_path
|
|
1290
|
-
)
|
|
1291
|
-
_run_fixture_git(repository, "checkout", "--orphan", "unrelated-origin")
|
|
1292
|
-
_run_fixture_git(repository, "rm", "-rf", ".")
|
|
1293
|
-
unrelated_origin_tip = _commit_file(repository, "unrelated.txt")
|
|
1294
|
-
_run_fixture_git(repository, "checkout", FEATURE_BRANCH_NAME)
|
|
1295
|
-
_run_fixture_git(
|
|
1296
|
-
repository,
|
|
1297
|
-
"update-ref",
|
|
1298
|
-
ORIGIN_DEFAULT_BRANCH_REFERENCE,
|
|
1299
|
-
unrelated_origin_tip,
|
|
1300
|
-
)
|
|
1301
|
-
_set_origin_head(repository, ORIGIN_DEFAULT_BRANCH_REFERENCE)
|
|
1302
|
-
upstream_reference = "refs/remotes/upstream/main"
|
|
1303
|
-
upstream_head_reference = "refs/remotes/upstream/HEAD"
|
|
1304
|
-
_run_fixture_git(repository, "update-ref", upstream_reference, default_branch_tip)
|
|
1305
|
-
_run_fixture_git(
|
|
1306
|
-
repository, "symbolic-ref", upstream_head_reference, upstream_reference
|
|
1307
|
-
)
|
|
1308
|
-
recorded_arguments_path = tmp_path / "upstream_recorded_arguments.txt"
|
|
1309
|
-
recording_gate_script_path = tmp_path / "upstream_recording_gate.py"
|
|
1310
|
-
recording_gate_script_path.write_text(
|
|
1311
|
-
"import pathlib, sys\n"
|
|
1312
|
-
f'pathlib.Path(r"{recorded_arguments_path}").write_text('
|
|
1313
|
-
"'\\n'.join(sys.argv[1:]), encoding='utf-8')\n"
|
|
1314
|
-
"sys.exit(0)\n",
|
|
1315
|
-
encoding="utf-8",
|
|
1316
|
-
)
|
|
1317
|
-
monkeypatch.setenv("CODE_RULES_GATE_PATH", str(recording_gate_script_path))
|
|
1318
|
-
monkeypatch.chdir(repository)
|
|
1319
|
-
monkeypatch.setattr(
|
|
1320
|
-
sys, "argv", [HOOK_INVOCATION_NAME, PUSHED_REMOTE_NAME, PUSHED_REMOTE_URL]
|
|
1321
|
-
)
|
|
1322
|
-
monkeypatch.setattr(
|
|
1323
|
-
sys,
|
|
1324
|
-
"stdin",
|
|
1325
|
-
io.StringIO(
|
|
1326
|
-
_push_stdin_line(
|
|
1327
|
-
FEATURE_BRANCH_NAME,
|
|
1328
|
-
topic_branch_tip,
|
|
1329
|
-
FEATURE_BRANCH_NAME,
|
|
1330
|
-
NON_ZERO_REMOTE_SHA_ONE,
|
|
1331
|
-
)
|
|
1332
|
-
),
|
|
1333
|
-
)
|
|
1334
|
-
|
|
1335
|
-
exit_code = pre_push.main()
|
|
1336
|
-
|
|
1337
|
-
assert exit_code == 0
|
|
1338
|
-
assert recorded_arguments_path.read_text(encoding="utf-8").splitlines() == [
|
|
1339
|
-
"--base",
|
|
1340
|
-
default_branch_tip,
|
|
1341
|
-
]
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
def test_main_allows_default_branch_file_the_rebase_replayed_onto(
|
|
1345
|
-
tmp_path: Path,
|
|
1346
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
1347
|
-
) -> None:
|
|
1348
|
-
repository, rebased_tip, pre_rebase_tip = _build_rebased_branch_repository(
|
|
1349
|
-
tmp_path,
|
|
1350
|
-
default_branch_file_name=FLAGGED_FILE_NAME,
|
|
1351
|
-
branch_file_name=BRANCH_FILE_NAME,
|
|
1352
|
-
)
|
|
1353
|
-
_set_origin_head(repository, ORIGIN_DEFAULT_BRANCH_REFERENCE)
|
|
1354
|
-
_write_diff_scoped_gate(tmp_path, monkeypatch)
|
|
1355
|
-
monkeypatch.chdir(repository)
|
|
1356
|
-
monkeypatch.setattr(
|
|
1357
|
-
sys,
|
|
1358
|
-
"stdin",
|
|
1359
|
-
io.StringIO(
|
|
1360
|
-
_push_stdin_line(
|
|
1361
|
-
FEATURE_BRANCH_NAME, rebased_tip, FEATURE_BRANCH_NAME, pre_rebase_tip
|
|
1362
|
-
)
|
|
1363
|
-
),
|
|
1364
|
-
)
|
|
1365
|
-
|
|
1366
|
-
exit_code = pre_push.main()
|
|
1367
|
-
|
|
1368
|
-
assert exit_code == 0
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
def test_main_blocks_violation_the_branch_itself_adds(
|
|
1372
|
-
tmp_path: Path,
|
|
1373
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
1374
|
-
) -> None:
|
|
1375
|
-
repository, rebased_tip, pre_rebase_tip = _build_rebased_branch_repository(
|
|
1376
|
-
tmp_path,
|
|
1377
|
-
default_branch_file_name=DEFAULT_BRANCH_FILE_NAME,
|
|
1378
|
-
branch_file_name=FLAGGED_FILE_NAME,
|
|
1379
|
-
)
|
|
1380
|
-
_set_origin_head(repository, ORIGIN_DEFAULT_BRANCH_REFERENCE)
|
|
1381
|
-
_write_diff_scoped_gate(tmp_path, monkeypatch)
|
|
1382
|
-
monkeypatch.chdir(repository)
|
|
1383
|
-
monkeypatch.setattr(
|
|
1384
|
-
sys,
|
|
1385
|
-
"stdin",
|
|
1386
|
-
io.StringIO(
|
|
1387
|
-
_push_stdin_line(
|
|
1388
|
-
FEATURE_BRANCH_NAME, rebased_tip, FEATURE_BRANCH_NAME, pre_rebase_tip
|
|
1389
|
-
)
|
|
1390
|
-
),
|
|
1391
|
-
)
|
|
1392
|
-
|
|
1393
|
-
exit_code = pre_push.main()
|
|
1394
|
-
|
|
1395
|
-
assert exit_code == 1
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
def test_main_uses_remote_object_when_no_default_branch_reference_resolves(
|
|
1399
|
-
tmp_path: Path,
|
|
1400
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
1401
|
-
) -> None:
|
|
1402
|
-
repository = tmp_path / "repository_without_remote"
|
|
1403
|
-
repository.mkdir()
|
|
1404
|
-
_run_fixture_git(repository, "init", "-b", DEFAULT_BRANCH_NAME)
|
|
1405
|
-
remote_object_name = _commit_file(repository, BASE_FILE_NAME)
|
|
1406
|
-
_run_fixture_git(repository, "checkout", "-b", FEATURE_BRANCH_NAME)
|
|
1407
|
-
pushed_object_name = _commit_file(repository, BRANCH_FILE_NAME)
|
|
1408
|
-
recorded_arguments_path = tmp_path / "recorded_arguments.txt"
|
|
1409
|
-
recording_gate_script_path = tmp_path / "recording_gate.py"
|
|
1410
|
-
recording_gate_script_path.write_text(
|
|
1411
|
-
"import sys, pathlib\n"
|
|
1412
|
-
f'pathlib.Path(r"{recorded_arguments_path}").write_text('
|
|
1413
|
-
"'\\n'.join(sys.argv[1:]), encoding='utf-8')\n"
|
|
1414
|
-
"sys.exit(0)\n",
|
|
1415
|
-
encoding="utf-8",
|
|
1416
|
-
)
|
|
1417
|
-
monkeypatch.setenv("CODE_RULES_GATE_PATH", str(recording_gate_script_path))
|
|
1418
|
-
monkeypatch.chdir(repository)
|
|
1419
|
-
monkeypatch.setattr(
|
|
1420
|
-
sys,
|
|
1421
|
-
"stdin",
|
|
1422
|
-
io.StringIO(
|
|
1423
|
-
_push_stdin_line(
|
|
1424
|
-
FEATURE_BRANCH_NAME,
|
|
1425
|
-
pushed_object_name,
|
|
1426
|
-
FEATURE_BRANCH_NAME,
|
|
1427
|
-
remote_object_name,
|
|
1428
|
-
)
|
|
1429
|
-
),
|
|
1430
|
-
)
|
|
1431
|
-
|
|
1432
|
-
exit_code = pre_push.main()
|
|
1433
|
-
|
|
1434
|
-
assert exit_code == 0
|
|
1435
|
-
recorded_arguments = recorded_arguments_path.read_text(
|
|
1436
|
-
encoding="utf-8"
|
|
1437
|
-
).splitlines()
|
|
1438
|
-
assert recorded_arguments == ["--base", remote_object_name]
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
def test_main_uses_remote_object_when_the_default_branch_is_pushed(
|
|
1442
|
-
tmp_path: Path,
|
|
1443
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
1444
|
-
) -> None:
|
|
1445
|
-
repository = tmp_path / "stale_tracking_repository"
|
|
1446
|
-
repository.mkdir()
|
|
1447
|
-
_run_fixture_git(repository, "init", "-b", DEFAULT_BRANCH_NAME)
|
|
1448
|
-
stale_tracking_tip = _commit_file(repository, BASE_FILE_NAME)
|
|
1449
|
-
remote_object_name = _commit_file(repository, FLAGGED_FILE_NAME)
|
|
1450
|
-
pushed_object_name = _commit_file(repository, DEFAULT_BRANCH_FILE_NAME)
|
|
1451
|
-
_set_origin_default_branch_tip(
|
|
1452
|
-
repository, ORIGIN_DEFAULT_BRANCH_REFERENCE, stale_tracking_tip
|
|
1453
|
-
)
|
|
1454
|
-
_set_origin_head(repository, ORIGIN_DEFAULT_BRANCH_REFERENCE)
|
|
1455
|
-
_write_diff_scoped_gate(tmp_path, monkeypatch)
|
|
1456
|
-
monkeypatch.chdir(repository)
|
|
1457
|
-
monkeypatch.setattr(
|
|
1458
|
-
sys,
|
|
1459
|
-
"stdin",
|
|
1460
|
-
io.StringIO(
|
|
1461
|
-
_push_stdin_line(
|
|
1462
|
-
DEFAULT_BRANCH_NAME,
|
|
1463
|
-
pushed_object_name,
|
|
1464
|
-
DEFAULT_BRANCH_NAME,
|
|
1465
|
-
remote_object_name,
|
|
1466
|
-
)
|
|
1467
|
-
),
|
|
1468
|
-
)
|
|
1469
|
-
|
|
1470
|
-
exit_code = pre_push.main()
|
|
1471
|
-
|
|
1472
|
-
assert exit_code == 0
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
def test_main_uses_merge_base_when_a_default_named_local_branch_updates_a_topic_branch(
|
|
1476
|
-
tmp_path: Path,
|
|
1477
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
1478
|
-
) -> None:
|
|
1479
|
-
repository = tmp_path / "default_named_local_branch_repository"
|
|
1480
|
-
repository.mkdir()
|
|
1481
|
-
_run_fixture_git(repository, "init", "-b", UNPROTECTED_DEFAULT_BRANCH_NAME)
|
|
1482
|
-
remote_object_name = _commit_file(repository, BASE_FILE_NAME)
|
|
1483
|
-
default_branch_tip = _commit_file(repository, FLAGGED_FILE_NAME)
|
|
1484
|
-
pushed_object_name = _commit_file(repository, BRANCH_FILE_NAME)
|
|
1485
|
-
_set_origin_default_branch_tip(
|
|
1486
|
-
repository, ORIGIN_UNPROTECTED_DEFAULT_BRANCH_REFERENCE, default_branch_tip
|
|
1487
|
-
)
|
|
1488
|
-
_set_origin_head(repository, ORIGIN_UNPROTECTED_DEFAULT_BRANCH_REFERENCE)
|
|
1489
|
-
_write_diff_scoped_gate(tmp_path, monkeypatch)
|
|
1490
|
-
monkeypatch.chdir(repository)
|
|
1491
|
-
monkeypatch.setattr(
|
|
1492
|
-
sys,
|
|
1493
|
-
"stdin",
|
|
1494
|
-
io.StringIO(
|
|
1495
|
-
_push_stdin_line(
|
|
1496
|
-
UNPROTECTED_DEFAULT_BRANCH_NAME,
|
|
1497
|
-
pushed_object_name,
|
|
1498
|
-
TOPIC_REMOTE_BRANCH_NAME,
|
|
1499
|
-
remote_object_name,
|
|
1500
|
-
)
|
|
1501
|
-
),
|
|
1502
|
-
)
|
|
1503
|
-
|
|
1504
|
-
exit_code = pre_push.main()
|
|
1505
|
-
|
|
1506
|
-
assert exit_code == 0
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
def test_main_uses_remote_object_when_a_renamed_local_branch_updates_the_default_branch(
|
|
1510
|
-
tmp_path: Path,
|
|
1511
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
1512
|
-
) -> None:
|
|
1513
|
-
repository = tmp_path / "renamed_local_branch_repository"
|
|
1514
|
-
repository.mkdir()
|
|
1515
|
-
_run_fixture_git(repository, "init", "-b", UNPROTECTED_DEFAULT_BRANCH_NAME)
|
|
1516
|
-
stale_tracking_tip = _commit_file(repository, BASE_FILE_NAME)
|
|
1517
|
-
remote_object_name = _commit_file(repository, FLAGGED_FILE_NAME)
|
|
1518
|
-
pushed_object_name = _commit_file(repository, DEFAULT_BRANCH_FILE_NAME)
|
|
1519
|
-
_run_fixture_git(repository, "checkout", "-b", LOCAL_ALIAS_BRANCH_NAME)
|
|
1520
|
-
_set_origin_default_branch_tip(
|
|
1521
|
-
repository, ORIGIN_UNPROTECTED_DEFAULT_BRANCH_REFERENCE, stale_tracking_tip
|
|
1522
|
-
)
|
|
1523
|
-
_set_origin_head(repository, ORIGIN_UNPROTECTED_DEFAULT_BRANCH_REFERENCE)
|
|
1524
|
-
_write_diff_scoped_gate(tmp_path, monkeypatch)
|
|
1525
|
-
monkeypatch.chdir(repository)
|
|
1526
|
-
monkeypatch.setattr(
|
|
1527
|
-
sys,
|
|
1528
|
-
"stdin",
|
|
1529
|
-
io.StringIO(
|
|
1530
|
-
_push_stdin_line(
|
|
1531
|
-
LOCAL_ALIAS_BRANCH_NAME,
|
|
1532
|
-
pushed_object_name,
|
|
1533
|
-
UNPROTECTED_DEFAULT_BRANCH_NAME,
|
|
1534
|
-
remote_object_name,
|
|
1535
|
-
)
|
|
1536
|
-
),
|
|
1537
|
-
)
|
|
1538
|
-
|
|
1539
|
-
exit_code = pre_push.main()
|
|
1540
|
-
|
|
1541
|
-
assert exit_code == 0
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
def test_main_reads_default_branch_tracking_reference_when_origin_head_is_unset(
|
|
1545
|
-
tmp_path: Path,
|
|
1546
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
1547
|
-
) -> None:
|
|
1548
|
-
repository, rebased_tip, pre_rebase_tip = _build_rebased_branch_repository(
|
|
1549
|
-
tmp_path,
|
|
1550
|
-
default_branch_file_name=FLAGGED_FILE_NAME,
|
|
1551
|
-
branch_file_name=BRANCH_FILE_NAME,
|
|
1552
|
-
)
|
|
1553
|
-
_write_diff_scoped_gate(tmp_path, monkeypatch)
|
|
1554
|
-
monkeypatch.chdir(repository)
|
|
1555
24
|
monkeypatch.setattr(
|
|
1556
25
|
sys,
|
|
1557
26
|
"stdin",
|
|
1558
27
|
io.StringIO(
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
28
|
+
"refs/heads/topic "
|
|
29
|
+
+ "a" * 40
|
|
30
|
+
+ " refs/heads/main "
|
|
31
|
+
+ "b" * 40
|
|
32
|
+
+ "\n"
|
|
1562
33
|
),
|
|
1563
34
|
)
|
|
1564
35
|
|
|
1565
36
|
exit_code = pre_push.main()
|
|
1566
37
|
|
|
38
|
+
captured_streams = capsys.readouterr()
|
|
1567
39
|
assert exit_code == 0
|
|
40
|
+
assert "push would send local branch 'topic'" in captured_streams.err
|
|
1568
41
|
|
|
1569
42
|
|
|
1570
|
-
def
|
|
1571
|
-
|
|
1572
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
1573
|
-
) -> None:
|
|
1574
|
-
repository, default_branch_tip, topic_branch_tip = _build_default_branch_repository(
|
|
1575
|
-
tmp_path
|
|
1576
|
-
)
|
|
1577
|
-
recorded_arguments_path = tmp_path / "recorded_arguments.txt"
|
|
1578
|
-
recording_gate_script_path = tmp_path / "recording_gate.py"
|
|
1579
|
-
recording_gate_script_path.write_text(
|
|
1580
|
-
"import pathlib, sys\n"
|
|
1581
|
-
f'pathlib.Path(r"{recorded_arguments_path}").write_text('
|
|
1582
|
-
"'\\n'.join(sys.argv[1:]), encoding='utf-8')\n"
|
|
1583
|
-
"sys.exit(0)\n",
|
|
1584
|
-
encoding="utf-8",
|
|
1585
|
-
)
|
|
1586
|
-
monkeypatch.setenv("CODE_RULES_GATE_PATH", str(recording_gate_script_path))
|
|
1587
|
-
monkeypatch.chdir(repository)
|
|
1588
|
-
monkeypatch.setattr(
|
|
1589
|
-
sys,
|
|
1590
|
-
"stdin",
|
|
1591
|
-
io.StringIO(
|
|
1592
|
-
_push_stdin_line(
|
|
1593
|
-
FEATURE_BRANCH_NAME,
|
|
1594
|
-
topic_branch_tip,
|
|
1595
|
-
FEATURE_BRANCH_NAME,
|
|
1596
|
-
ALL_ZEROS_OBJECT_NAME,
|
|
1597
|
-
)
|
|
1598
|
-
),
|
|
1599
|
-
)
|
|
1600
|
-
|
|
1601
|
-
assert pre_push.main() == 0
|
|
1602
|
-
assert recorded_arguments_path.read_text(encoding="utf-8").splitlines() == [
|
|
1603
|
-
"--base",
|
|
1604
|
-
git_hooks_constants.DEFAULT_REMOTE_BASE_REFERENCE,
|
|
1605
|
-
]
|
|
1606
|
-
|
|
1607
|
-
monkeypatch.setattr(
|
|
1608
|
-
sys,
|
|
1609
|
-
"stdin",
|
|
1610
|
-
io.StringIO(
|
|
1611
|
-
_push_stdin_line(
|
|
1612
|
-
FEATURE_BRANCH_NAME,
|
|
1613
|
-
topic_branch_tip,
|
|
1614
|
-
FEATURE_BRANCH_NAME,
|
|
1615
|
-
NON_ZERO_REMOTE_SHA_ONE,
|
|
1616
|
-
)
|
|
1617
|
-
),
|
|
1618
|
-
)
|
|
1619
|
-
|
|
1620
|
-
assert pre_push.main() == 0
|
|
1621
|
-
assert recorded_arguments_path.read_text(encoding="utf-8").splitlines() == [
|
|
1622
|
-
"--base",
|
|
1623
|
-
default_branch_tip,
|
|
1624
|
-
]
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
def test_main_skips_a_deletion_line_to_reach_the_branch_update(
|
|
1628
|
-
tmp_path: Path,
|
|
1629
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
1630
|
-
) -> None:
|
|
1631
|
-
repository, rebased_tip, pre_rebase_tip = _build_rebased_branch_repository(
|
|
43
|
+
def test_native_git_push_to_local_bare_remote_allows_missing_setup(tmp_path: Path) -> None:
|
|
44
|
+
repository_path = create_native_repository(
|
|
1632
45
|
tmp_path,
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
"
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
)
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
assert
|
|
46
|
+
"repo",
|
|
47
|
+
"https://github.com/JonEcho/python-automation.git",
|
|
48
|
+
initial_branch="main",
|
|
49
|
+
)
|
|
50
|
+
run_git(repository_path, "add", "README.md")
|
|
51
|
+
run_git(repository_path, "commit", "--quiet", "-m", "initial")
|
|
52
|
+
|
|
53
|
+
bare_repository_path = create_bare_repository(tmp_path)
|
|
54
|
+
configure_remote_url_rewrite(
|
|
55
|
+
repository_path,
|
|
56
|
+
bare_repository_path,
|
|
57
|
+
"https://github.com/JonEcho/python-automation.git",
|
|
58
|
+
)
|
|
59
|
+
hook_directory = tmp_path / "hooks"
|
|
60
|
+
install_native_hook(repository_path, hook_directory, "pre-push", "pre_push")
|
|
61
|
+
|
|
62
|
+
completed_push = run_native_git_action(repository_path, "push", "origin", "main")
|
|
63
|
+
|
|
64
|
+
pushed_head = read_git_head(bare_repository_path, "refs/heads/main")
|
|
65
|
+
current_head = read_git_head(repository_path, "HEAD")
|
|
66
|
+
combined_output = completed_push.stdout + completed_push.stderr
|
|
67
|
+
assert completed_push.returncode == 0
|
|
68
|
+
assert pushed_head == current_head
|
|
69
|
+
assert "LOCAL VERIFICATION ADVISORY" in combined_output
|
|
70
|
+
assert "Event: push" in combined_output
|
|
71
|
+
assert f"Current SHA: {current_head}" in combined_output
|
|
72
|
+
assert "State: pending" in combined_output
|