claude-dev-env 7.0.0 → 7.2.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 +213 -0
- package/.agents/skills/session-continuity/SKILL.md +125 -0
- package/.agents/skills/session-continuity/continuity.mjs +447 -0
- package/.agents/skills/session-continuity/continuity.test.mjs +373 -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 +88 -0
- package/bin/install-session-continuity.test.mjs +55 -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,580 +1,84 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
|
|
4
|
-
Installed to the user's shared git-hooks directory via the claude-dev-env
|
|
5
|
-
installer; git invokes this file as `pre-push` (the installer strips the
|
|
6
|
-
`_` and `.py` suffix when copying into the live hooks path).
|
|
7
|
-
|
|
8
|
-
Protocol: git pre-push provides remote name and URL as argv, then writes
|
|
9
|
-
`<local-ref> <local-sha> <remote-ref> <remote-sha>` lines on stdin.
|
|
10
|
-
|
|
11
|
-
Destination guard: any line that pushes a local branch onto a protected
|
|
12
|
-
remote branch (`main` or `master`) whose name differs from the local
|
|
13
|
-
branch is blocked before the gate runs. This catches a branch that tracks
|
|
14
|
-
`origin/main` under `push.default=upstream`, where a bare `git push`
|
|
15
|
-
resolves to `main`. The guard runs whether or not the CODE_RULES gate is
|
|
16
|
-
installed; deletions and same-name pushes pass.
|
|
17
|
-
|
|
18
|
-
Gate base: a branch push takes its `--base` from the merge base between the
|
|
19
|
-
pushed object and the remote default branch, so a rebase onto the default
|
|
20
|
-
branch leaves the gate reading only the branch's own work. The stdin remote
|
|
21
|
-
object name is the base when no default-branch ref resolves and when the
|
|
22
|
-
pushed branch is the default branch. When every remote object name is zero
|
|
23
|
-
(new branch) or stdin is empty, the gate falls back to the remote's default
|
|
24
|
-
branch symbolic ref. When a default-branch ref resolves and git still reports
|
|
25
|
-
no merge base, the base stays pending and the hook reports the infrastructure
|
|
26
|
-
status with a printed reason, preserving the scope boundary for rebased tips.
|
|
27
|
-
|
|
28
|
-
Gate scope: the gate process this hook launches diffs its `--base` against the
|
|
29
|
-
current checkout's HEAD, so the surface is the commits between that base and
|
|
30
|
-
HEAD. A push whose pushed object is HEAD reads as the branch's own work; a
|
|
31
|
-
push of any other object is still scoped to HEAD.
|
|
32
|
-
|
|
33
|
-
Exit codes:
|
|
34
|
-
0 - the push destination is allowed and its commits pass the gate, or the
|
|
35
|
-
gate is not installed.
|
|
36
|
-
1 - the push would land a non-protected local branch onto a protected
|
|
37
|
-
remote branch, or a commit introduces a blocking violation.
|
|
38
|
-
2 - infrastructure attention is required because a subprocess could not
|
|
39
|
-
launch, stdin carried no parseable line, a base could not be resolved,
|
|
40
|
-
or a usable commit name could not be produced.
|
|
41
|
-
"""
|
|
2
|
+
"""Show the local verification reminder before a native Git push."""
|
|
42
3
|
|
|
43
4
|
from __future__ import annotations
|
|
44
5
|
|
|
45
|
-
import subprocess
|
|
46
6
|
import sys
|
|
47
7
|
from pathlib import Path
|
|
48
|
-
from typing import NamedTuple
|
|
49
8
|
|
|
50
|
-
from gate_utils import is_safe_regular_file, resolve_gate_script_path
|
|
51
9
|
from git_hooks_constants import (
|
|
52
|
-
ALL_FALLBACK_REMOTE_DEFAULT_BRANCH_NAMES,
|
|
53
|
-
ALL_GIT_MERGE_BASE_COMMAND_PREFIX,
|
|
54
|
-
ALL_GIT_SYMBOLIC_REFERENCE_COMMAND_PREFIX,
|
|
55
|
-
ALL_GIT_VERIFY_REFERENCE_COMMAND_PREFIX,
|
|
56
10
|
ALL_PROTECTED_BRANCH_PUSH_NAMES,
|
|
57
11
|
ALL_ZEROS_OBJECT_NAME_CHARACTER,
|
|
58
|
-
BASE_REFERENCE_ARGUMENT,
|
|
59
|
-
DEFAULT_REMOTE_BASE_REFERENCE,
|
|
60
|
-
DEFAULT_REMOTE_NAME,
|
|
61
|
-
GATE_INFRASTRUCTURE_FAILURE_EXIT_CODE,
|
|
62
|
-
GIT_COMMAND_TIMEOUT_SECONDS,
|
|
63
|
-
GIT_COMMAND_UNAVAILABLE_MESSAGE,
|
|
64
|
-
GIT_EXECUTABLE_NAME,
|
|
65
|
-
GIT_OUTPUT_DECODE_ERRORS_POLICY,
|
|
66
|
-
GIT_OUTPUT_ENCODING_NAME,
|
|
67
|
-
GIT_REFERENCE_QUERY_TIMEOUT_SECONDS,
|
|
68
|
-
INVOKE_GATE_FAILURE_MESSAGE,
|
|
69
12
|
LOCAL_BRANCH_REFERENCE_PREFIX,
|
|
70
13
|
LOCAL_REFERENCE_FIELD_INDEX,
|
|
71
14
|
LOCAL_SHA_FIELD_INDEX,
|
|
72
|
-
MALFORMED_STDIN_LINE_MESSAGE,
|
|
73
|
-
NO_PARSEABLE_STDIN_LINES_MESSAGE,
|
|
74
|
-
NO_PARSEABLE_STDIN_LINES_SENTINEL,
|
|
75
|
-
PRE_PUSH_GATE_SCRIPT_NOT_FOUND_MESSAGE,
|
|
76
|
-
PROTECTED_BRANCH_PUSH_BLOCK_EXIT_CODE,
|
|
77
|
-
PROTECTED_BRANCH_PUSH_BLOCK_MESSAGE,
|
|
78
|
-
REMOTE_BRANCH_REFERENCE_TEMPLATE,
|
|
79
15
|
REMOTE_REFERENCE_FIELD_INDEX,
|
|
80
16
|
STDIN_LINE_FIELD_COUNT,
|
|
81
|
-
STDIN_READ_FAILURE_MESSAGE,
|
|
82
|
-
STDIN_REMOTE_OBJECT_FIELD_INDEX,
|
|
83
|
-
UNRESOLVABLE_MERGE_BASE_MESSAGE,
|
|
84
|
-
UNRESOLVABLE_MERGE_BASE_SENTINEL,
|
|
85
17
|
)
|
|
86
|
-
from
|
|
87
|
-
|
|
88
|
-
|
|
18
|
+
from git_hooks_constants.verification_notice_constants import (
|
|
19
|
+
NOTICE_EVENT_PUSH,
|
|
20
|
+
PROTECTED_BRANCH_PUSH_ADVISORY_MESSAGE,
|
|
21
|
+
REPOSITORY_ARGUMENT,
|
|
89
22
|
)
|
|
90
|
-
from
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
def _report_unavailable_git(launch_error: Exception) -> int:
|
|
94
|
-
"""Report a git that would not run, and hand back the exit code to use."""
|
|
95
|
-
git_command_unavailable_message = GIT_COMMAND_UNAVAILABLE_MESSAGE
|
|
96
|
-
sys.stderr.write(git_command_unavailable_message.format(error=launch_error) + "\n")
|
|
97
|
-
return GATE_INFRASTRUCTURE_FAILURE_EXIT_CODE
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
class GitCommandUnavailable(RuntimeError):
|
|
101
|
-
"""Git itself could not run: it failed to launch, or it timed out.
|
|
102
|
-
|
|
103
|
-
A git that runs and reports nothing is ordinary absence, which stays a
|
|
104
|
-
plain return. Separating the two keeps a broken toolchain from reading
|
|
105
|
-
as a repository that simply has no default branch.
|
|
106
|
-
"""
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
def run_git_text_command(all_command_arguments: list[str]) -> tuple[int, str]:
|
|
110
|
-
"""Ask git a question and read back its answer.
|
|
111
|
-
|
|
112
|
-
Output decodes with a replacing policy, so a reference whose bytes are
|
|
113
|
-
invalid in this encoding becomes a marked string and matches nothing.
|
|
114
|
-
|
|
115
|
-
Args:
|
|
116
|
-
all_command_arguments: The git arguments following the executable name.
|
|
23
|
+
from verification_notice import main as verification_notice_main
|
|
117
24
|
|
|
118
|
-
Returns:
|
|
119
|
-
The exit code paired with the trimmed standard output.
|
|
120
|
-
|
|
121
|
-
Raises:
|
|
122
|
-
GitCommandUnavailable: Git failed to launch or exceeded its timeout.
|
|
123
|
-
"""
|
|
124
|
-
git_executable_name = GIT_EXECUTABLE_NAME
|
|
125
|
-
git_command_timeout_seconds = GIT_COMMAND_TIMEOUT_SECONDS
|
|
126
|
-
try:
|
|
127
|
-
completion = subprocess.run(
|
|
128
|
-
[git_executable_name, *all_command_arguments],
|
|
129
|
-
check=False,
|
|
130
|
-
capture_output=True,
|
|
131
|
-
timeout=git_command_timeout_seconds,
|
|
132
|
-
)
|
|
133
|
-
except (OSError, subprocess.SubprocessError) as launch_error:
|
|
134
|
-
raise GitCommandUnavailable(str(launch_error)) from launch_error
|
|
135
|
-
decoded_output = completion.stdout.decode(
|
|
136
|
-
GIT_OUTPUT_ENCODING_NAME, errors=GIT_OUTPUT_DECODE_ERRORS_POLICY
|
|
137
|
-
)
|
|
138
|
-
return completion.returncode, decoded_output.strip()
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
def is_all_zeros_object_name(object_name: str) -> bool:
|
|
142
|
-
all_zeros_object_name_character = ALL_ZEROS_OBJECT_NAME_CHARACTER
|
|
143
|
-
stripped_object_name = object_name.strip()
|
|
144
|
-
if not stripped_object_name:
|
|
145
|
-
return True
|
|
146
|
-
return all(
|
|
147
|
-
each_character == all_zeros_object_name_character for each_character in stripped_object_name
|
|
148
|
-
)
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
class PushLine(NamedTuple):
|
|
152
|
-
"""One parsed ``<local-ref> <local-sha> <remote-ref> <remote-sha>`` stdin line."""
|
|
153
|
-
|
|
154
|
-
local_branch_name: str
|
|
155
|
-
local_object_name: str
|
|
156
|
-
remote_branch_name: str
|
|
157
|
-
remote_object_name: str
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
def parse_push_line(stripped_line: str) -> PushLine | None:
|
|
161
|
-
"""Return the four fields of one push line, or None when the line is malformed.
|
|
162
|
-
|
|
163
|
-
Args:
|
|
164
|
-
stripped_line: One whitespace-stripped stdin line.
|
|
165
|
-
|
|
166
|
-
Returns:
|
|
167
|
-
The parsed line with both ref names reduced to branch names, or None
|
|
168
|
-
when the line carries fewer than the four fields git writes.
|
|
169
|
-
"""
|
|
170
|
-
fields = stripped_line.split()
|
|
171
|
-
if len(fields) < STDIN_LINE_FIELD_COUNT:
|
|
172
|
-
return None
|
|
173
|
-
return PushLine(
|
|
174
|
-
local_branch_name=fields[LOCAL_REFERENCE_FIELD_INDEX].removeprefix(
|
|
175
|
-
LOCAL_BRANCH_REFERENCE_PREFIX
|
|
176
|
-
),
|
|
177
|
-
local_object_name=fields[LOCAL_SHA_FIELD_INDEX],
|
|
178
|
-
remote_branch_name=fields[REMOTE_REFERENCE_FIELD_INDEX].removeprefix(
|
|
179
|
-
LOCAL_BRANCH_REFERENCE_PREFIX
|
|
180
|
-
),
|
|
181
|
-
remote_object_name=fields[STDIN_REMOTE_OBJECT_FIELD_INDEX],
|
|
182
|
-
)
|
|
183
25
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
Args:
|
|
189
|
-
push_line: One parsed push line.
|
|
190
|
-
|
|
191
|
-
Returns:
|
|
192
|
-
True when the local and the remote object names are both non-zero.
|
|
193
|
-
"""
|
|
194
|
-
if is_all_zeros_object_name(push_line.local_object_name):
|
|
195
|
-
return False
|
|
196
|
-
return not is_all_zeros_object_name(push_line.remote_object_name)
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
class ParsedPushStdin(NamedTuple):
|
|
200
|
-
"""Every well-formed push line from one stdin payload, and whether text arrived."""
|
|
201
|
-
|
|
202
|
-
all_push_lines: tuple[PushLine, ...]
|
|
203
|
-
has_stdin_content: bool
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
def _parse_push_stdin(stdin_text: str) -> ParsedPushStdin:
|
|
207
|
-
"""Parse the whole stdin payload once, warning on each malformed line.
|
|
208
|
-
|
|
209
|
-
Args:
|
|
210
|
-
stdin_text: The pre-push stdin payload.
|
|
211
|
-
|
|
212
|
-
Returns:
|
|
213
|
-
Every well-formed line in stdin order, paired with whether stdin
|
|
214
|
-
carried any non-blank text at all.
|
|
215
|
-
"""
|
|
216
|
-
all_push_lines: list[PushLine] = []
|
|
217
|
-
has_stdin_content = False
|
|
26
|
+
def find_protected_branch_push_violation(
|
|
27
|
+
stdin_text: str,
|
|
28
|
+
) -> tuple[str, str] | None:
|
|
218
29
|
for each_line in stdin_text.splitlines():
|
|
219
|
-
|
|
220
|
-
if
|
|
221
|
-
continue
|
|
222
|
-
has_stdin_content = True
|
|
223
|
-
push_line = parse_push_line(stripped_line)
|
|
224
|
-
if push_line is None:
|
|
225
|
-
print(
|
|
226
|
-
MALFORMED_STDIN_LINE_MESSAGE.format(line=stripped_line),
|
|
227
|
-
file=sys.stderr,
|
|
228
|
-
)
|
|
229
|
-
continue
|
|
230
|
-
all_push_lines.append(push_line)
|
|
231
|
-
return ParsedPushStdin(tuple(all_push_lines), has_stdin_content)
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
def _resolve_base_reference_from_lines(parsed_stdin: ParsedPushStdin) -> str | None:
|
|
235
|
-
"""Return the stdin-derived base reference for already-parsed lines.
|
|
236
|
-
|
|
237
|
-
Args:
|
|
238
|
-
parsed_stdin: The parsed stdin payload.
|
|
239
|
-
|
|
240
|
-
Returns:
|
|
241
|
-
The first branch update's remote object name, the no-parseable-lines
|
|
242
|
-
sentinel, None for a deletion-only push, or the remote default branch
|
|
243
|
-
reference.
|
|
244
|
-
"""
|
|
245
|
-
is_all_valid_lines_deletions = True
|
|
246
|
-
for each_push_line in parsed_stdin.all_push_lines:
|
|
247
|
-
if is_all_zeros_object_name(each_push_line.local_object_name):
|
|
30
|
+
all_fields = each_line.split()
|
|
31
|
+
if len(all_fields) < STDIN_LINE_FIELD_COUNT:
|
|
248
32
|
continue
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
if parsed_stdin.has_stdin_content and not parsed_stdin.all_push_lines:
|
|
253
|
-
return NO_PARSEABLE_STDIN_LINES_SENTINEL
|
|
254
|
-
if parsed_stdin.all_push_lines and is_all_valid_lines_deletions:
|
|
255
|
-
return None
|
|
256
|
-
return DEFAULT_REMOTE_BASE_REFERENCE
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
def resolve_base_reference_from_stdin(stdin_text: str) -> str | None:
|
|
260
|
-
return _resolve_base_reference_from_lines(_parse_push_stdin(stdin_text))
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
def run_git_reference_query(all_git_arguments: tuple[str, ...]) -> str | None:
|
|
264
|
-
"""Return the stripped stdout of a read-only git query.
|
|
265
|
-
|
|
266
|
-
Output decodes with the same replacing policy as ``run_git_text_command``,
|
|
267
|
-
so locale-invalid bytes become a marked string instead of crashing the hook.
|
|
268
|
-
|
|
269
|
-
Args:
|
|
270
|
-
all_git_arguments: The full git argv, including the ``git`` word.
|
|
271
|
-
|
|
272
|
-
Returns:
|
|
273
|
-
The stripped stdout, or None when git exits non-zero, prints nothing,
|
|
274
|
-
or cannot run.
|
|
275
|
-
"""
|
|
276
|
-
try:
|
|
277
|
-
completion = subprocess.run(
|
|
278
|
-
list(all_git_arguments),
|
|
279
|
-
capture_output=True,
|
|
280
|
-
check=False,
|
|
281
|
-
timeout=GIT_REFERENCE_QUERY_TIMEOUT_SECONDS,
|
|
282
|
-
)
|
|
283
|
-
except (OSError, subprocess.SubprocessError):
|
|
284
|
-
return None
|
|
285
|
-
if completion.returncode != 0:
|
|
286
|
-
return None
|
|
287
|
-
decoded_output = completion.stdout.decode(
|
|
288
|
-
GIT_OUTPUT_ENCODING_NAME, errors=GIT_OUTPUT_DECODE_ERRORS_POLICY
|
|
289
|
-
)
|
|
290
|
-
return decoded_output.strip() or None
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
def resolve_default_branch_reference(remote_name: str = DEFAULT_REMOTE_NAME) -> str | None:
|
|
294
|
-
"""Return the remote-tracking ref name of the remote's default branch.
|
|
295
|
-
|
|
296
|
-
The remote's HEAD symbolic ref answers first. When it is unset, the
|
|
297
|
-
supported default-branch tracking refs answer in turn.
|
|
298
|
-
|
|
299
|
-
Args:
|
|
300
|
-
remote_name: Remote whose default branch provides the gate base.
|
|
301
|
-
|
|
302
|
-
Returns:
|
|
303
|
-
A remote-tracking ref name, or None when no default-branch ref
|
|
304
|
-
resolves.
|
|
305
|
-
"""
|
|
306
|
-
remote_head_reference = REMOTE_BRANCH_REFERENCE_TEMPLATE.format(
|
|
307
|
-
remote=remote_name, branch="HEAD"
|
|
308
|
-
)
|
|
309
|
-
symbolic_reference = run_git_reference_query(
|
|
310
|
-
(*ALL_GIT_SYMBOLIC_REFERENCE_COMMAND_PREFIX, remote_head_reference)
|
|
311
|
-
)
|
|
312
|
-
if symbolic_reference is not None:
|
|
313
|
-
verified_symbolic_target = run_git_reference_query(
|
|
314
|
-
(*ALL_GIT_VERIFY_REFERENCE_COMMAND_PREFIX, symbolic_reference)
|
|
315
|
-
)
|
|
316
|
-
if verified_symbolic_target is not None:
|
|
317
|
-
return symbolic_reference
|
|
318
|
-
for each_branch_name in ALL_FALLBACK_REMOTE_DEFAULT_BRANCH_NAMES:
|
|
319
|
-
each_candidate_reference = REMOTE_BRANCH_REFERENCE_TEMPLATE.format(
|
|
320
|
-
remote=remote_name, branch=each_branch_name
|
|
321
|
-
)
|
|
322
|
-
verified_reference = run_git_reference_query(
|
|
323
|
-
(*ALL_GIT_VERIFY_REFERENCE_COMMAND_PREFIX, each_candidate_reference)
|
|
33
|
+
local_object_name = all_fields[LOCAL_SHA_FIELD_INDEX]
|
|
34
|
+
local_branch_name = all_fields[LOCAL_REFERENCE_FIELD_INDEX].removeprefix(
|
|
35
|
+
LOCAL_BRANCH_REFERENCE_PREFIX
|
|
324
36
|
)
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
return None
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
def _find_branch_update_in_lines(
|
|
331
|
-
all_push_lines: tuple[PushLine, ...],
|
|
332
|
-
) -> PushLine | None:
|
|
333
|
-
"""Return the first already-parsed line that moves an existing remote branch.
|
|
334
|
-
|
|
335
|
-
Args:
|
|
336
|
-
all_push_lines: Well-formed push lines in stdin order.
|
|
337
|
-
|
|
338
|
-
Returns:
|
|
339
|
-
The first branch-update line, or None when no line updates a branch
|
|
340
|
-
the remote already holds.
|
|
341
|
-
"""
|
|
342
|
-
for each_push_line in all_push_lines:
|
|
343
|
-
if is_branch_update(each_push_line):
|
|
344
|
-
return each_push_line
|
|
345
|
-
return None
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
def find_branch_update_push(stdin_text: str) -> PushLine | None:
|
|
349
|
-
"""Return the first stdin line that moves a branch the remote already holds.
|
|
350
|
-
|
|
351
|
-
Args:
|
|
352
|
-
stdin_text: The pre-push stdin payload.
|
|
353
|
-
|
|
354
|
-
Returns:
|
|
355
|
-
The parsed branch-update line, or None when no line updates a branch
|
|
356
|
-
the remote already holds.
|
|
357
|
-
"""
|
|
358
|
-
return _find_branch_update_in_lines(_parse_push_stdin(stdin_text).all_push_lines)
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
def resolve_default_branch_merge_base(
|
|
362
|
-
remote_branch_name: str,
|
|
363
|
-
pushed_object_name: str,
|
|
364
|
-
remote_name: str = DEFAULT_REMOTE_NAME,
|
|
365
|
-
) -> str | None:
|
|
366
|
-
"""Return the merge base of the pushed object and the default branch.
|
|
367
|
-
|
|
368
|
-
The remote branch the push updates decides whether a merge base applies,
|
|
369
|
-
so ``git push origin release-candidate:develop`` reads as an update of
|
|
370
|
-
``develop`` whatever the local branch is called.
|
|
371
|
-
|
|
372
|
-
Args:
|
|
373
|
-
remote_branch_name: The remote branch the push updates.
|
|
374
|
-
pushed_object_name: The object name the push carries.
|
|
375
|
-
remote_name: The remote whose default branch provides the gate base.
|
|
376
|
-
|
|
377
|
-
Returns:
|
|
378
|
-
The merge-base object name; None when no default-branch ref resolves or
|
|
379
|
-
when the push updates the default branch, which are the two cases where
|
|
380
|
-
no merge base applies; or the unresolvable-merge-base sentinel when a
|
|
381
|
-
default-branch ref resolves and git still reports no merge base.
|
|
382
|
-
"""
|
|
383
|
-
default_branch_reference = resolve_default_branch_reference(remote_name)
|
|
384
|
-
if default_branch_reference is None:
|
|
385
|
-
return None
|
|
386
|
-
default_branch_prefix = REMOTE_BRANCH_REFERENCE_TEMPLATE.format(remote=remote_name, branch="")
|
|
387
|
-
default_branch_name = default_branch_reference.removeprefix(default_branch_prefix)
|
|
388
|
-
if remote_branch_name == default_branch_name:
|
|
389
|
-
return None
|
|
390
|
-
# Anchors to the pushed object; deferring to the gate's HEAD-based merge-base would change behavior on non-HEAD pushes.
|
|
391
|
-
merge_base_object_name = run_git_reference_query(
|
|
392
|
-
(
|
|
393
|
-
*ALL_GIT_MERGE_BASE_COMMAND_PREFIX,
|
|
394
|
-
pushed_object_name,
|
|
395
|
-
default_branch_reference,
|
|
37
|
+
remote_branch_name = all_fields[REMOTE_REFERENCE_FIELD_INDEX].removeprefix(
|
|
38
|
+
LOCAL_BRANCH_REFERENCE_PREFIX
|
|
396
39
|
)
|
|
397
|
-
|
|
398
|
-
if merge_base_object_name is None:
|
|
399
|
-
return UNRESOLVABLE_MERGE_BASE_SENTINEL
|
|
400
|
-
return merge_base_object_name
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
def resolve_gate_base_reference(
|
|
404
|
-
stdin_text: str, remote_name: str = DEFAULT_REMOTE_NAME
|
|
405
|
-
) -> str | None:
|
|
406
|
-
"""Return the reference the gate scopes its changed lines to.
|
|
407
|
-
|
|
408
|
-
A branch update reads from the merge base with the remote default branch,
|
|
409
|
-
and every other push reads from the stdin remote object name::
|
|
410
|
-
|
|
411
|
-
branch update, default branch resolved -> merge-base(pushed, default)
|
|
412
|
-
branch update, no merge base found -> unresolvable-merge-base sentinel
|
|
413
|
-
branch update, no default branch ref -> stdin remote object name
|
|
414
|
-
default-branch push -> stdin remote object name
|
|
415
|
-
new branch or empty stdin -> remote default branch ref
|
|
416
|
-
deletion -> None
|
|
417
|
-
|
|
418
|
-
Args:
|
|
419
|
-
stdin_text: The pre-push stdin payload.
|
|
420
|
-
remote_name: The remote whose default branch provides the gate base.
|
|
421
|
-
|
|
422
|
-
Returns:
|
|
423
|
-
The gate's base reference, the no-parseable-lines sentinel, the
|
|
424
|
-
unresolvable-merge-base sentinel, or None when the push only deletes
|
|
425
|
-
remote branches.
|
|
426
|
-
"""
|
|
427
|
-
return _resolve_gate_base_from_parsed(_parse_push_stdin(stdin_text), remote_name)
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
def _resolve_gate_base_from_parsed(
|
|
431
|
-
parsed_stdin: ParsedPushStdin, remote_name: str = DEFAULT_REMOTE_NAME
|
|
432
|
-
) -> str | None:
|
|
433
|
-
"""Return the gate's base reference for an already-parsed stdin payload.
|
|
434
|
-
|
|
435
|
-
Args:
|
|
436
|
-
parsed_stdin: The parsed stdin payload.
|
|
437
|
-
remote_name: The remote whose default branch provides the gate base.
|
|
438
|
-
|
|
439
|
-
Returns:
|
|
440
|
-
The merge base with the remote default branch for a branch update, the
|
|
441
|
-
branch update's remote object name when no merge base applies, the
|
|
442
|
-
unresolvable-merge-base sentinel when a default-branch ref resolves and
|
|
443
|
-
git reports no merge base anyway, and otherwise whatever the
|
|
444
|
-
stdin-derived base reference reports.
|
|
445
|
-
"""
|
|
446
|
-
branch_update = _find_branch_update_in_lines(parsed_stdin.all_push_lines)
|
|
447
|
-
if branch_update is None:
|
|
448
|
-
return _resolve_base_reference_from_lines(parsed_stdin)
|
|
449
|
-
merge_base_object_name = resolve_default_branch_merge_base(
|
|
450
|
-
branch_update.remote_branch_name,
|
|
451
|
-
branch_update.local_object_name,
|
|
452
|
-
remote_name,
|
|
453
|
-
)
|
|
454
|
-
if merge_base_object_name is None:
|
|
455
|
-
return branch_update.remote_object_name
|
|
456
|
-
return merge_base_object_name
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
def _find_protected_violation_in_lines(
|
|
460
|
-
all_push_lines: tuple[PushLine, ...],
|
|
461
|
-
) -> tuple[str, str] | None:
|
|
462
|
-
"""Return the first already-parsed line that pushes onto a protected branch.
|
|
463
|
-
|
|
464
|
-
Args:
|
|
465
|
-
all_push_lines: Well-formed push lines in stdin order.
|
|
466
|
-
|
|
467
|
-
Returns:
|
|
468
|
-
The local and protected remote branch names, or None when no line
|
|
469
|
-
lands a differently named branch on a protected one.
|
|
470
|
-
"""
|
|
471
|
-
for each_push_line in all_push_lines:
|
|
472
|
-
if is_all_zeros_object_name(each_push_line.local_object_name):
|
|
40
|
+
if not local_object_name.strip(ALL_ZEROS_OBJECT_NAME_CHARACTER):
|
|
473
41
|
continue
|
|
474
42
|
if (
|
|
475
|
-
|
|
476
|
-
and
|
|
43
|
+
remote_branch_name in ALL_PROTECTED_BRANCH_PUSH_NAMES
|
|
44
|
+
and local_branch_name != remote_branch_name
|
|
477
45
|
):
|
|
478
|
-
return
|
|
479
|
-
each_push_line.local_branch_name,
|
|
480
|
-
each_push_line.remote_branch_name,
|
|
481
|
-
)
|
|
46
|
+
return local_branch_name, remote_branch_name
|
|
482
47
|
return None
|
|
483
48
|
|
|
484
49
|
|
|
485
|
-
def find_protected_branch_push_violation(stdin_text: str) -> tuple[str, str] | None:
|
|
486
|
-
return _find_protected_violation_in_lines(_parse_push_stdin(stdin_text).all_push_lines)
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
def invoke_gate(gate_script_path: Path, base_reference: str) -> int:
|
|
490
|
-
base_reference_argument = BASE_REFERENCE_ARGUMENT
|
|
491
|
-
invoke_gate_failure_message = INVOKE_GATE_FAILURE_MESSAGE
|
|
492
|
-
gate_infrastructure_failure_exit_code = GATE_INFRASTRUCTURE_FAILURE_EXIT_CODE
|
|
493
|
-
try:
|
|
494
|
-
resolved_gate_path = gate_script_path.resolve(strict=True)
|
|
495
|
-
completion = subprocess.run(
|
|
496
|
-
[
|
|
497
|
-
sys.executable,
|
|
498
|
-
str(resolved_gate_path),
|
|
499
|
-
base_reference_argument,
|
|
500
|
-
base_reference,
|
|
501
|
-
],
|
|
502
|
-
check=False,
|
|
503
|
-
)
|
|
504
|
-
except OSError as launch_error:
|
|
505
|
-
print(
|
|
506
|
-
invoke_gate_failure_message.format(error=launch_error),
|
|
507
|
-
file=sys.stderr,
|
|
508
|
-
)
|
|
509
|
-
return gate_infrastructure_failure_exit_code
|
|
510
|
-
return completion.returncode
|
|
511
|
-
|
|
512
|
-
|
|
513
50
|
def main() -> int:
|
|
514
|
-
stdin_read_failure_message = STDIN_READ_FAILURE_MESSAGE
|
|
515
|
-
gate_infrastructure_failure_exit_code = GATE_INFRASTRUCTURE_FAILURE_EXIT_CODE
|
|
516
|
-
pre_push_gate_script_not_found_message = PRE_PUSH_GATE_SCRIPT_NOT_FOUND_MESSAGE
|
|
517
|
-
no_parseable_stdin_lines_message = NO_PARSEABLE_STDIN_LINES_MESSAGE
|
|
518
|
-
no_parseable_stdin_lines_sentinel = NO_PARSEABLE_STDIN_LINES_SENTINEL
|
|
519
|
-
unresolvable_merge_base_message = UNRESOLVABLE_MERGE_BASE_MESSAGE
|
|
520
|
-
unresolvable_merge_base_sentinel = UNRESOLVABLE_MERGE_BASE_SENTINEL
|
|
521
|
-
protected_branch_push_block_message = PROTECTED_BRANCH_PUSH_BLOCK_MESSAGE
|
|
522
|
-
protected_branch_push_block_exit_code = PROTECTED_BRANCH_PUSH_BLOCK_EXIT_CODE
|
|
523
51
|
try:
|
|
524
52
|
stdin_text = sys.stdin.read()
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
file=sys.stderr,
|
|
543
|
-
)
|
|
544
|
-
return protected_branch_push_block_exit_code
|
|
545
|
-
gate_script_path, exact_allowed_path = resolve_gate_script_path()
|
|
546
|
-
if not is_safe_regular_file(gate_script_path, exact_allowed_path):
|
|
547
|
-
print(
|
|
548
|
-
pre_push_gate_script_not_found_message.format(path=gate_script_path),
|
|
549
|
-
file=sys.stderr,
|
|
53
|
+
branch_violation = find_protected_branch_push_violation(stdin_text)
|
|
54
|
+
if branch_violation is not None:
|
|
55
|
+
local_branch_name, remote_branch_name = branch_violation
|
|
56
|
+
_write_advisory(
|
|
57
|
+
PROTECTED_BRANCH_PUSH_ADVISORY_MESSAGE.format(
|
|
58
|
+
local_branch=local_branch_name,
|
|
59
|
+
remote_branch=remote_branch_name,
|
|
60
|
+
)
|
|
61
|
+
)
|
|
62
|
+
verification_notice_main(
|
|
63
|
+
[
|
|
64
|
+
"--event",
|
|
65
|
+
NOTICE_EVENT_PUSH,
|
|
66
|
+
REPOSITORY_ARGUMENT,
|
|
67
|
+
str(Path.cwd()),
|
|
68
|
+
],
|
|
69
|
+
stdout=sys.stdout,
|
|
550
70
|
)
|
|
551
71
|
return 0
|
|
552
|
-
|
|
553
|
-
base_reference = _resolve_gate_base_from_parsed(parsed_stdin, remote_name)
|
|
554
|
-
if base_reference is None:
|
|
72
|
+
except (OSError, UnicodeError, ValueError):
|
|
555
73
|
return 0
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
if base_reference == unresolvable_merge_base_sentinel:
|
|
560
|
-
print(unresolvable_merge_base_message, file=sys.stderr)
|
|
561
|
-
return gate_infrastructure_failure_exit_code
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _write_advisory(advisory_text: str) -> None:
|
|
562
77
|
try:
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
except GitCommandUnavailable as unavailable_error:
|
|
567
|
-
return _report_unavailable_git(unavailable_error)
|
|
568
|
-
if usable_base_reference is None:
|
|
569
|
-
return gate_infrastructure_failure_exit_code
|
|
570
|
-
code_rules_exit_code = invoke_gate(gate_script_path, usable_base_reference)
|
|
571
|
-
if code_rules_exit_code != 0:
|
|
572
|
-
return code_rules_exit_code
|
|
573
|
-
pull_request_url = get_pull_request_url(Path.cwd())
|
|
574
|
-
if pull_request_url:
|
|
575
|
-
print(build_pull_request_reminder(pull_request_url))
|
|
576
|
-
return 0
|
|
78
|
+
sys.stderr.write(advisory_text + "\n")
|
|
79
|
+
except UnicodeError:
|
|
80
|
+
return
|
|
577
81
|
|
|
578
82
|
|
|
579
83
|
if __name__ == "__main__":
|
|
580
|
-
|
|
84
|
+
raise SystemExit(main())
|