claude-dev-env 2.13.0 → 2.14.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.md +61 -0
- package/CLAUDE.md +1 -57
- package/_shared/AGENTS.md +15 -0
- package/_shared/CLAUDE.md +1 -15
- package/_shared/advisor/AGENTS.md +22 -0
- package/_shared/advisor/CLAUDE.md +1 -22
- package/_shared/pr-loop/AGENTS.md +27 -0
- package/_shared/pr-loop/CLAUDE.md +1 -27
- package/_shared/pr-loop/scripts/AGENTS.md +35 -0
- package/_shared/pr-loop/scripts/CLAUDE.md +1 -35
- package/_shared/pr-loop/scripts/code_rules_gate_parts/AGENTS.md +42 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/CLAUDE.md +1 -42
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/AGENTS.md +26 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/CLAUDE.md +1 -26
- package/_shared/pr-loop/scripts/tests/AGENTS.md +44 -0
- package/_shared/pr-loop/scripts/tests/CLAUDE.md +1 -44
- package/_shared/process-tree/AGENTS.md +41 -0
- package/_shared/process-tree/CLAUDE.md +1 -41
- package/agents/AGENTS.md +31 -0
- package/agents/CLAUDE.md +1 -31
- package/agents/clean-coder.md +6 -6
- package/agents/test_agent_frontmatter.py +4 -4
- package/audit-rubrics/AGENTS.md +43 -0
- package/audit-rubrics/CLAUDE.md +1 -43
- package/audit-rubrics/category_rubrics/AGENTS.md +37 -0
- package/audit-rubrics/category_rubrics/CLAUDE.md +1 -37
- package/audit-rubrics/prompts/AGENTS.md +37 -0
- package/audit-rubrics/prompts/CLAUDE.md +1 -37
- package/bin/AGENTS.md +112 -0
- package/bin/CLAUDE.md +1 -112
- package/bin/ever-shipped-skills.mjs +1 -0
- package/bin/install-constants.mjs +6 -7
- package/bin/install.mjs +24 -5
- package/bin/install.test.mjs +34 -0
- package/commands/AGENTS.md +23 -0
- package/commands/CLAUDE.md +1 -23
- package/commands/hook-log-extract.md +3 -3
- package/docs/AGENTS.md +32 -0
- package/docs/CLAUDE.md +1 -32
- package/docs/CODE_RULES.md +1 -1
- package/docs/imagegen.md +22 -0
- package/docs/references/AGENTS.md +17 -0
- package/docs/references/CLAUDE.md +1 -17
- package/hooks/AGENTS.md +31 -0
- package/hooks/CLAUDE.md +1 -31
- package/hooks/_gh_pr_author_swap_utils.py +14 -0
- package/hooks/advisory/AGENTS.md +16 -0
- package/hooks/advisory/CLAUDE.md +1 -16
- package/hooks/blocking/AGENTS.md +123 -0
- package/hooks/blocking/CLAUDE.md +1 -123
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/AGENTS.md +28 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/CLAUDE.md +1 -28
- package/hooks/blocking/code_rules_banned_identifiers.py +1 -1
- package/hooks/blocking/code_rules_dead_module_constant.py +149 -37
- package/hooks/blocking/code_rules_probe_chains.py +6 -2
- package/hooks/blocking/config/AGENTS.md +10 -0
- package/hooks/blocking/config/CLAUDE.md +1 -10
- package/hooks/blocking/gh_pr_author_restore.py +7 -2
- package/hooks/blocking/intent_only_ending_blocker.py +11 -16
- package/hooks/blocking/inventory_intent_records/AGENTS.md +26 -0
- package/hooks/blocking/inventory_intent_records/CLAUDE.md +1 -26
- package/hooks/blocking/package_inventory_stale_blocker_parts/AGENTS.md +26 -0
- package/hooks/blocking/package_inventory_stale_blocker_parts/CLAUDE.md +1 -26
- package/hooks/blocking/pii_prevention_blocker_parts/AGENTS.md +24 -0
- package/hooks/blocking/pii_prevention_blocker_parts/CLAUDE.md +1 -24
- package/hooks/blocking/tdd_enforcer_parts/AGENTS.md +30 -0
- package/hooks/blocking/tdd_enforcer_parts/CLAUDE.md +1 -30
- package/hooks/blocking/test_code_rules_enforcer_banned_prefixes.py +1 -1
- package/hooks/blocking/test_code_rules_enforcer_dead_module_constant.py +171 -0
- package/hooks/blocking/test_code_rules_enforcer_magic_allowlist.py +1 -1
- package/hooks/blocking/test_gh_pr_author_restore.py +2 -1
- package/hooks/blocking/test_intent_only_ending_blocker.py +4 -0
- package/hooks/diagnostic/AGENTS.md +43 -0
- package/hooks/diagnostic/CLAUDE.md +1 -43
- package/hooks/diagnostic/hook_log_extractor.py +2 -2
- package/hooks/diagnostic/migrations/AGENTS.md +16 -0
- package/hooks/diagnostic/migrations/CLAUDE.md +1 -16
- package/hooks/diagnostic/queries/AGENTS.md +19 -0
- package/hooks/diagnostic/queries/CLAUDE.md +1 -19
- package/hooks/diagnostic/test_hook_log_extractor.py +2 -2
- package/hooks/git-hooks/AGENTS.md +31 -0
- package/hooks/git-hooks/CLAUDE.md +1 -31
- package/hooks/git-hooks/git_hooks_constants/AGENTS.md +21 -0
- package/hooks/git-hooks/git_hooks_constants/CLAUDE.md +1 -21
- package/hooks/git-hooks/git_hooks_constants/__init__.py +4 -6
- package/hooks/git-hooks/pre_push.py +47 -34
- package/hooks/git-hooks/test_pre_push.py +111 -7
- package/hooks/hooks_constants/AGENTS.md +104 -0
- package/hooks/hooks_constants/CLAUDE.md +1 -104
- package/hooks/hooks_constants/hook_log_extractor_constants.py +1 -2
- package/hooks/hooks_constants/task_list_loop_starter_constants.py +4 -3
- package/hooks/hooks_constants/working_style_prompt_constants.py +29 -20
- package/hooks/lifecycle/AGENTS.md +18 -0
- package/hooks/lifecycle/CLAUDE.md +1 -18
- package/hooks/observability/AGENTS.md +20 -0
- package/hooks/observability/CLAUDE.md +1 -20
- package/hooks/session/AGENTS.md +33 -0
- package/hooks/session/CLAUDE.md +1 -33
- package/hooks/session/gh_pr_author_session_cleanup.py +7 -2
- package/hooks/session/task_list_loop_starter.py +3 -3
- package/hooks/session/test_gh_pr_author_session_cleanup.py +2 -1
- package/hooks/session/test_task_list_loop_starter.py +10 -2
- package/hooks/session/test_working_style_prompt.py +9 -4
- package/hooks/validation/AGENTS.md +20 -0
- package/hooks/validation/CLAUDE.md +1 -20
- package/hooks/validators/AGENTS.md +51 -0
- package/hooks/validators/CLAUDE.md +1 -51
- package/hooks/validators/health_check.py +52 -19
- package/hooks/validators/python_style_checks.py +1 -1
- package/hooks/validators/test_health_check.py +112 -27
- package/hooks/workflow/AGENTS.md +16 -0
- package/hooks/workflow/CLAUDE.md +1 -16
- package/installable-surfaces.manifest.json +1 -0
- package/output-styles/AGENTS.md +15 -0
- package/output-styles/CLAUDE.md +1 -15
- package/package.json +2 -1
- package/rules/AGENTS.md +61 -0
- package/rules/CLAUDE.md +1 -61
- package/rules/code-standards.md +5 -9
- package/rules/doc-prose-cuts.md +1 -1
- package/scripts/AGENTS.md +62 -0
- package/scripts/CLAUDE.md +1 -62
- package/scripts/codex_compat_materializer.py +4 -0
- package/scripts/dev_env_scripts_constants/AGENTS.md +21 -0
- package/scripts/dev_env_scripts_constants/CLAUDE.md +1 -21
- package/scripts/sync_to_cursor/AGENTS.md +23 -0
- package/scripts/sync_to_cursor/CLAUDE.md +1 -23
- package/scripts/test_validate_instruction_pairs.py +90 -0
- package/scripts/test_verify_installable_package.py +5 -1
- package/scripts/tests/AGENTS.md +33 -0
- package/scripts/tests/CLAUDE.md +1 -33
- package/scripts/tests/test_codex_compat_materializer.py +36 -1
- package/scripts/validate_instruction_pairs.py +183 -0
- package/skills/AGENTS.md +72 -0
- package/skills/CLAUDE.md +1 -72
- package/skills/_shared/AGENTS.md +44 -0
- package/skills/_shared/CLAUDE.md +1 -44
- package/skills/_shared/advisor/AGENTS.md +9 -0
- package/skills/_shared/advisor/CLAUDE.md +1 -9
- package/skills/_shared/pr-loop/AGENTS.md +50 -0
- package/skills/_shared/pr-loop/CLAUDE.md +1 -50
- package/skills/_shared/pr-loop/prompts/AGENTS.md +9 -0
- package/skills/_shared/pr-loop/prompts/CLAUDE.md +1 -9
- package/skills/_shared/pr-loop/scripts/AGENTS.md +35 -0
- package/skills/_shared/pr-loop/scripts/CLAUDE.md +1 -35
- package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/AGENTS.md +24 -0
- package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/CLAUDE.md +1 -24
- package/skills/anthropic-plan/AGENTS.md +34 -0
- package/skills/anthropic-plan/CLAUDE.md +1 -34
- package/skills/anthropic-plan/scripts/AGENTS.md +11 -0
- package/skills/anthropic-plan/scripts/CLAUDE.md +1 -11
- package/skills/anthropic-plan/scripts/anthropic_plan_scripts_constants/AGENTS.md +16 -0
- package/skills/anthropic-plan/scripts/anthropic_plan_scripts_constants/CLAUDE.md +1 -16
- package/skills/anthropic-plan/templates/AGENTS.md +13 -0
- package/skills/anthropic-plan/templates/CLAUDE.md +1 -13
- package/skills/anthropic-plan/workflow/AGENTS.md +14 -0
- package/skills/anthropic-plan/workflow/CLAUDE.md +1 -14
- package/skills/auditing-claude-config/AGENTS.md +21 -0
- package/skills/auditing-claude-config/CLAUDE.md +1 -21
- package/skills/autoconverge/AGENTS.md +36 -0
- package/skills/autoconverge/CLAUDE.md +1 -36
- package/skills/autoconverge/reference/AGENTS.md +16 -0
- package/skills/autoconverge/reference/CLAUDE.md +1 -16
- package/skills/autoconverge/workflow/AGENTS.md +26 -0
- package/skills/autoconverge/workflow/CLAUDE.md +1 -26
- package/skills/autoconverge/workflow/autoconverge_report_constants/AGENTS.md +16 -0
- package/skills/autoconverge/workflow/autoconverge_report_constants/CLAUDE.md +1 -16
- package/skills/bugteam/AGENTS.md +30 -0
- package/skills/bugteam/CLAUDE.md +1 -30
- package/skills/bugteam/reference/AGENTS.md +20 -0
- package/skills/bugteam/reference/CLAUDE.md +1 -20
- package/skills/bugteam/reference/obstacles/AGENTS.md +24 -0
- package/skills/bugteam/reference/obstacles/CLAUDE.md +1 -24
- package/skills/bugteam/scripts/AGENTS.md +30 -0
- package/skills/bugteam/scripts/CLAUDE.md +1 -30
- package/skills/bugteam/scripts/bugteam_scripts_constants/AGENTS.md +18 -0
- package/skills/bugteam/scripts/bugteam_scripts_constants/CLAUDE.md +1 -18
- package/skills/closeout/reference/handoff-prompt-template.md +1 -1
- package/skills/codex-review/AGENTS.md +46 -0
- package/skills/codex-review/CLAUDE.md +1 -46
- package/skills/codex-review/reference/AGENTS.md +15 -0
- package/skills/codex-review/reference/CLAUDE.md +1 -15
- package/skills/codex-review/scripts/codex_review_scripts_constants/AGENTS.md +18 -0
- package/skills/codex-review/scripts/codex_review_scripts_constants/CLAUDE.md +1 -18
- package/skills/codex-review/test_skill_scaffold.py +3 -3
- package/skills/copilot-review/AGENTS.md +18 -0
- package/skills/copilot-review/CLAUDE.md +1 -18
- package/skills/everything-search/AGENTS.md +17 -0
- package/skills/everything-search/CLAUDE.md +1 -17
- package/skills/fresh-branch/AGENTS.md +14 -0
- package/skills/fresh-branch/CLAUDE.md +1 -14
- package/skills/grok-spawn/AGENTS.md +28 -0
- package/skills/grok-spawn/CLAUDE.md +1 -28
- package/skills/pr-converge/AGENTS.md +32 -0
- package/skills/pr-converge/CLAUDE.md +1 -32
- package/skills/pr-converge/pr_converge_skill_constants/AGENTS.md +26 -0
- package/skills/pr-converge/pr_converge_skill_constants/CLAUDE.md +1 -26
- package/skills/pr-converge/reference/AGENTS.md +28 -0
- package/skills/pr-converge/reference/CLAUDE.md +1 -28
- package/skills/pr-converge/reference/obstacles/AGENTS.md +23 -0
- package/skills/pr-converge/reference/obstacles/CLAUDE.md +1 -23
- package/skills/pr-converge/scripts/AGENTS.md +46 -0
- package/skills/pr-converge/scripts/CLAUDE.md +1 -46
- package/skills/pr-converge/scripts/pr_converge_scripts_constants/AGENTS.md +18 -0
- package/skills/pr-converge/scripts/pr_converge_scripts_constants/CLAUDE.md +1 -18
- package/skills/pr-converge/workflows/AGENTS.md +16 -0
- package/skills/pr-converge/workflows/CLAUDE.md +1 -16
- package/skills/prototype/reference/honest-limitations.md +1 -1
- package/skills/rebase/AGENTS.md +32 -0
- package/skills/rebase/CLAUDE.md +1 -32
- package/skills/recall/AGENTS.md +30 -0
- package/skills/recall/CLAUDE.md +1 -30
- package/skills/remember/AGENTS.md +31 -0
- package/skills/remember/CLAUDE.md +1 -31
- package/skills/reviews/SKILL.md +1 -1
- package/skills/session-log/AGENTS.md +32 -0
- package/skills/session-log/CLAUDE.md +1 -32
- package/skills/session-tidy/AGENTS.md +36 -0
- package/skills/session-tidy/CLAUDE.md +1 -36
- package/skills/skill-builder/AGENTS.md +49 -0
- package/skills/skill-builder/CLAUDE.md +1 -49
- package/skills/skill-builder/references/AGENTS.md +25 -0
- package/skills/skill-builder/references/CLAUDE.md +1 -25
- package/skills/skill-builder/references/deterministic-elements.md +1 -1
- package/skills/skill-builder/templates/AGENTS.md +14 -0
- package/skills/skill-builder/templates/CLAUDE.md +1 -14
- package/skills/skill-builder/workflows/AGENTS.md +19 -0
- package/skills/skill-builder/workflows/CLAUDE.md +1 -19
- package/skills/task-build/AGENTS.md +29 -0
- package/skills/task-build/CLAUDE.md +1 -29
- package/skills/update/AGENTS.md +38 -0
- package/skills/update/CLAUDE.md +1 -38
- package/system-prompts/AGENTS.md +17 -0
- package/system-prompts/CLAUDE.md +1 -17
- package/system-prompts/software-engineer.xml +3 -3
- package/skills/imagegen/SKILL.md +0 -21
- package/skills/imagegen/scripts/config/__init__.py +0 -1
- package/skills/imagegen/scripts/config/constants.py +0 -28
- package/skills/imagegen/scripts/imagegen.py +0 -45
- package/skills/imagegen/scripts/imagegen_core.py +0 -528
- package/skills/imagegen/scripts/test_imagegen.py +0 -204
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# hooks/diagnostic/queries
|
|
2
|
+
|
|
3
|
+
Parameterized SQL queries for inspecting the `hook_events` Neon table. Run these directly against the Neon database to analyze hook-firing patterns.
|
|
4
|
+
|
|
5
|
+
## Files
|
|
6
|
+
|
|
7
|
+
| File | What it returns |
|
|
8
|
+
|---|---|
|
|
9
|
+
| `block_details_for_hook.sql` | Full details for all blocked events matching a given hook name |
|
|
10
|
+
| `blocks_by_category.sql` | Count of blocks grouped by hook category |
|
|
11
|
+
| `blocks_by_tool.sql` | Count of blocks grouped by tool name |
|
|
12
|
+
| `blocks_last_7_days.sql` | All blocked events from the last 7 days |
|
|
13
|
+
| `top_blockers_last_24_hours.sql` | Hook names with the most blocks in the last 24 hours |
|
|
14
|
+
| `top_blockers_overall.sql` | Hook names with the most blocks across all time |
|
|
15
|
+
|
|
16
|
+
## Conventions
|
|
17
|
+
|
|
18
|
+
- Queries target the `hook_events` table and `blocked_commands` view defined in `diagnostic/schema.sql`.
|
|
19
|
+
- Run with `psql $DATABASE_URL -f <query>.sql` or paste into the Neon console.
|
|
@@ -1,19 +1 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
Parameterized SQL queries for inspecting the `hook_events` Neon table. Run these directly against the Neon database to analyze hook-firing patterns.
|
|
4
|
-
|
|
5
|
-
## Files
|
|
6
|
-
|
|
7
|
-
| File | What it returns |
|
|
8
|
-
|---|---|
|
|
9
|
-
| `block_details_for_hook.sql` | Full details for all blocked events matching a given hook name |
|
|
10
|
-
| `blocks_by_category.sql` | Count of blocks grouped by hook category |
|
|
11
|
-
| `blocks_by_tool.sql` | Count of blocks grouped by tool name |
|
|
12
|
-
| `blocks_last_7_days.sql` | All blocked events from the last 7 days |
|
|
13
|
-
| `top_blockers_last_24_hours.sql` | Hook names with the most blocks in the last 24 hours |
|
|
14
|
-
| `top_blockers_overall.sql` | Hook names with the most blocks across all time |
|
|
15
|
-
|
|
16
|
-
## Conventions
|
|
17
|
-
|
|
18
|
-
- Queries target the `hook_events` table and `blocked_commands` view defined in `diagnostic/schema.sql`.
|
|
19
|
-
- Run with `psql $DATABASE_URL -f <query>.sql` or paste into the Neon console.
|
|
1
|
+
@AGENTS.md
|
|
@@ -617,7 +617,7 @@ def test_tool_name_extracted_from_hook_name_prefix() -> None:
|
|
|
617
617
|
assert hook_log_extractor.extract_tool_name("UserPromptSubmit") is None
|
|
618
618
|
|
|
619
619
|
|
|
620
|
-
def
|
|
620
|
+
def test_run_summary_prints_zero_blocked_events_when_cursor_empty(
|
|
621
621
|
capsys: pytest.CaptureFixture[str],
|
|
622
622
|
) -> None:
|
|
623
623
|
fake_cursor = MagicMock()
|
|
@@ -632,7 +632,7 @@ def test_run_summary_prints_no_new_blocks_when_cursor_empty(
|
|
|
632
632
|
|
|
633
633
|
captured = capsys.readouterr()
|
|
634
634
|
assert exit_code == 0
|
|
635
|
-
assert "
|
|
635
|
+
assert "Blocked events recorded in the last 24 hours: 0." in captured.out
|
|
636
636
|
|
|
637
637
|
|
|
638
638
|
def test_run_summary_prints_table_when_rows_returned(
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# hooks/git-hooks
|
|
2
|
+
|
|
3
|
+
Native git hooks that run outside the Claude Code lifecycle — invoked directly by git at commit and push time. The installer copies these scripts into the user's shared git-hooks directory (`core.hooksPath`).
|
|
4
|
+
|
|
5
|
+
## Key files
|
|
6
|
+
|
|
7
|
+
| File | Git hook | What it does |
|
|
8
|
+
|---|---|---|
|
|
9
|
+
| `pre_commit.py` | `pre-commit` | Runs the CODE_RULES gate (`precommit_code_rules_gate.py`) over staged changes; exits 1 when any staged file has a blocking violation |
|
|
10
|
+
| `pre_push.py` | `pre-push` | Blocks a push that would land a non-`main` local branch onto remote `main` (or `master`), then runs the CODE_RULES gate. An existing branch's gate base is the merge base with the remote default branch; the gate process still diffs that base against checkout HEAD, so the surface matches the pushed tip only when HEAD is that tip. |
|
|
11
|
+
| `pre_push_base_reference.py` | — | Resolves a usable gate base for `pre_push.py`: reads the pushed remote name from git's arguments, then turns a symbolic default-branch head into a reference that git can resolve |
|
|
12
|
+
| `post_commit.py` | `post-commit` | Runs after a commit lands; performs any post-commit bookkeeping |
|
|
13
|
+
| `gate_utils.py` | — | Shared helpers: resolves the gate script path, checks that the path is a safe regular file |
|
|
14
|
+
| `test_config.py` | — | Test configuration helpers |
|
|
15
|
+
| `test_gate_utils.py` | — | Tests for `gate_utils.py` |
|
|
16
|
+
| `test_pre_commit.py` | — | Tests for `pre_commit.py` |
|
|
17
|
+
| `test_pre_push.py` | — | Tests for `pre_push.py` |
|
|
18
|
+
| `test_pre_push_base_reference.py` | — | Tests for the gate base resolution in `pre_push.py` |
|
|
19
|
+
| `conftest.py` | — | Puts this directory on `sys.path` and clears cached `config` modules, so each test module imports the hooks at the top of the file |
|
|
20
|
+
|
|
21
|
+
## Subdirectory
|
|
22
|
+
|
|
23
|
+
| Directory | Role |
|
|
24
|
+
|---|---|
|
|
25
|
+
| `git_hooks_constants/` | Shared constants imported by the git-hook scripts |
|
|
26
|
+
|
|
27
|
+
## Conventions
|
|
28
|
+
|
|
29
|
+
- The installer strips the `_` and `.py` suffix when copying into the live git-hooks path (e.g. `pre_commit.py` becomes `pre-commit`).
|
|
30
|
+
- Constants (exit codes, argument names, error messages) live in `git_hooks_constants/` and are imported at the top of each script.
|
|
31
|
+
- Run tests with `python -m pytest git-hooks/test_<name>.py`.
|
|
@@ -1,31 +1 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
Native git hooks that run outside the Claude Code lifecycle — invoked directly by git at commit and push time. The installer copies these scripts into the user's shared git-hooks directory (`core.hooksPath`).
|
|
4
|
-
|
|
5
|
-
## Key files
|
|
6
|
-
|
|
7
|
-
| File | Git hook | What it does |
|
|
8
|
-
|---|---|---|
|
|
9
|
-
| `pre_commit.py` | `pre-commit` | Runs the CODE_RULES gate (`precommit_code_rules_gate.py`) over staged changes; exits 1 when any staged file has a blocking violation |
|
|
10
|
-
| `pre_push.py` | `pre-push` | Blocks a push that would land a non-`main` local branch onto remote `main` (or `master`), then runs the CODE_RULES gate. An existing branch's gate base is the merge base with the remote default branch; the gate process still diffs that base against checkout HEAD, so the surface matches the pushed tip only when HEAD is that tip. |
|
|
11
|
-
| `pre_push_base_reference.py` | — | Resolves a usable gate base for `pre_push.py`: reads the pushed remote name from git's arguments, then turns a symbolic default-branch head into a reference that git can resolve |
|
|
12
|
-
| `post_commit.py` | `post-commit` | Runs after a commit lands; performs any post-commit bookkeeping |
|
|
13
|
-
| `gate_utils.py` | — | Shared helpers: resolves the gate script path, checks that the path is a safe regular file |
|
|
14
|
-
| `test_config.py` | — | Test configuration helpers |
|
|
15
|
-
| `test_gate_utils.py` | — | Tests for `gate_utils.py` |
|
|
16
|
-
| `test_pre_commit.py` | — | Tests for `pre_commit.py` |
|
|
17
|
-
| `test_pre_push.py` | — | Tests for `pre_push.py` |
|
|
18
|
-
| `test_pre_push_base_reference.py` | — | Tests for the gate base resolution in `pre_push.py` |
|
|
19
|
-
| `conftest.py` | — | Puts this directory on `sys.path` and clears cached `config` modules, so each test module imports the hooks at the top of the file |
|
|
20
|
-
|
|
21
|
-
## Subdirectory
|
|
22
|
-
|
|
23
|
-
| Directory | Role |
|
|
24
|
-
|---|---|
|
|
25
|
-
| `git_hooks_constants/` | Shared constants imported by the git-hook scripts |
|
|
26
|
-
|
|
27
|
-
## Conventions
|
|
28
|
-
|
|
29
|
-
- The installer strips the `_` and `.py` suffix when copying into the live git-hooks path (e.g. `pre_commit.py` becomes `pre-commit`).
|
|
30
|
-
- Constants (exit codes, argument names, error messages) live in `git_hooks_constants/` and are imported at the top of each script.
|
|
31
|
-
- Run tests with `python -m pytest git-hooks/test_<name>.py`.
|
|
1
|
+
@AGENTS.md
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# hooks/git-hooks/git_hooks_constants
|
|
2
|
+
|
|
3
|
+
Shared constants imported by the git-hook scripts in `git-hooks/`. Centralizes exit codes, argument names, and error messages so every tunable lives in one place.
|
|
4
|
+
|
|
5
|
+
## Files
|
|
6
|
+
|
|
7
|
+
| File | Contents |
|
|
8
|
+
|---|---|
|
|
9
|
+
| `__init__.py` | Exports all constants; marks this as a package so `from git_hooks_constants import ...` resolves |
|
|
10
|
+
|
|
11
|
+
## Key constants (defined in `__init__.py`)
|
|
12
|
+
|
|
13
|
+
- `GATE_INFRASTRUCTURE_FAILURE_EXIT_CODE` — exit code when the gate script cannot be found or launched
|
|
14
|
+
- `GATE_SCRIPT_NOT_FOUND_MESSAGE` — error message when the gate script path does not exist
|
|
15
|
+
- `INVOKE_GATE_FAILURE_MESSAGE` — error message when the gate subprocess fails to start
|
|
16
|
+
- `STAGED_SCOPE_ARGUMENT` — CLI argument passed to the gate script to scope it to staged changes
|
|
17
|
+
|
|
18
|
+
## Conventions
|
|
19
|
+
|
|
20
|
+
- Import with `from git_hooks_constants import <CONSTANT>` from within the `git-hooks/` directory.
|
|
21
|
+
- Add new constants here rather than inline in the hook scripts.
|
|
@@ -1,21 +1 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
Shared constants imported by the git-hook scripts in `git-hooks/`. Centralizes exit codes, argument names, and error messages so every tunable lives in one place.
|
|
4
|
-
|
|
5
|
-
## Files
|
|
6
|
-
|
|
7
|
-
| File | Contents |
|
|
8
|
-
|---|---|
|
|
9
|
-
| `__init__.py` | Exports all constants; marks this as a package so `from git_hooks_constants import ...` resolves |
|
|
10
|
-
|
|
11
|
-
## Key constants (defined in `__init__.py`)
|
|
12
|
-
|
|
13
|
-
- `GATE_INFRASTRUCTURE_FAILURE_EXIT_CODE` — exit code when the gate script cannot be found or launched
|
|
14
|
-
- `GATE_SCRIPT_NOT_FOUND_MESSAGE` — error message when the gate script path does not exist
|
|
15
|
-
- `INVOKE_GATE_FAILURE_MESSAGE` — error message when the gate subprocess fails to start
|
|
16
|
-
- `STAGED_SCOPE_ARGUMENT` — CLI argument passed to the gate script to scope it to staged changes
|
|
17
|
-
|
|
18
|
-
## Conventions
|
|
19
|
-
|
|
20
|
-
- Import with `from git_hooks_constants import <CONSTANT>` from within the `git-hooks/` directory.
|
|
21
|
-
- Add new constants here rather than inline in the hook scripts.
|
|
1
|
+
@AGENTS.md
|
|
@@ -49,12 +49,9 @@ NO_PARSEABLE_STDIN_LINES_MESSAGE: str = (
|
|
|
49
49
|
NO_PARSEABLE_STDIN_LINES_SENTINEL: str = "__no_parseable_stdin_lines__"
|
|
50
50
|
UNRESOLVABLE_MERGE_BASE_SENTINEL: str = "__unresolvable_merge_base__"
|
|
51
51
|
UNRESOLVABLE_MERGE_BASE_MESSAGE: str = (
|
|
52
|
-
"claude-dev-env pre-push:
|
|
53
|
-
"and
|
|
54
|
-
"
|
|
55
|
-
"default branch hold unrelated histories, or when a default-branch ref "
|
|
56
|
-
"resolved and merge-base still could not name a shared commit "
|
|
57
|
-
"(for example a shallow clone missing the connecting history)."
|
|
52
|
+
"claude-dev-env pre-push: CODE_RULES validation is pending because the pushed "
|
|
53
|
+
"object and default branch have no shared merge base. Restore shared history "
|
|
54
|
+
"or provide a reachable default branch, then run the push again."
|
|
58
55
|
)
|
|
59
56
|
LOCAL_BRANCH_REFERENCE_PREFIX: str = "refs/heads/"
|
|
60
57
|
ORIGIN_HEAD_SYMBOLIC_REFERENCE: str = "refs/remotes/origin/HEAD"
|
|
@@ -63,6 +60,7 @@ ALL_DEFAULT_BRANCH_FALLBACK_REFERENCES: tuple[str, ...] = (
|
|
|
63
60
|
"refs/remotes/origin/main",
|
|
64
61
|
"refs/remotes/origin/master",
|
|
65
62
|
)
|
|
63
|
+
REMOTE_BRANCH_REFERENCE_TEMPLATE: str = "refs/remotes/{remote}/{branch}"
|
|
66
64
|
ALL_GIT_SYMBOLIC_REFERENCE_COMMAND_PREFIX: tuple[str, ...] = (
|
|
67
65
|
"git",
|
|
68
66
|
"symbolic-ref",
|
|
@@ -22,9 +22,8 @@ object name is the base when no default-branch ref resolves and when the
|
|
|
22
22
|
pushed branch is the default branch. When every remote object name is zero
|
|
23
23
|
(new branch) or stdin is empty, the gate falls back to the remote's default
|
|
24
24
|
branch symbolic ref. When a default-branch ref resolves and git still reports
|
|
25
|
-
no merge base, the base
|
|
26
|
-
a printed reason
|
|
27
|
-
a rebased tip would re-flag replayed commits.
|
|
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.
|
|
28
27
|
|
|
29
28
|
Gate scope: the gate process this hook launches diffs its `--base` against the
|
|
30
29
|
current checkout's HEAD, so the surface is the commits between that base and
|
|
@@ -32,14 +31,13 @@ HEAD. A push whose pushed object is HEAD reads as the branch's own work; a
|
|
|
32
31
|
push of any other object is still scoped to HEAD.
|
|
33
32
|
|
|
34
33
|
Exit codes:
|
|
35
|
-
0 - the push destination is allowed and its commits pass the gate
|
|
36
|
-
|
|
37
|
-
default-branch ref resolved and git still reported no merge base).
|
|
34
|
+
0 - the push destination is allowed and its commits pass the gate, or the
|
|
35
|
+
gate is not installed.
|
|
38
36
|
1 - the push would land a non-protected local branch onto a protected
|
|
39
37
|
remote branch, or a commit introduces a blocking violation.
|
|
40
|
-
2 -
|
|
41
|
-
stdin carried no parseable line,
|
|
42
|
-
|
|
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.
|
|
43
41
|
"""
|
|
44
42
|
|
|
45
43
|
from __future__ import annotations
|
|
@@ -49,8 +47,9 @@ import sys
|
|
|
49
47
|
from pathlib import Path
|
|
50
48
|
from typing import NamedTuple
|
|
51
49
|
|
|
50
|
+
from gate_utils import is_safe_regular_file, resolve_gate_script_path
|
|
52
51
|
from git_hooks_constants import (
|
|
53
|
-
|
|
52
|
+
ALL_FALLBACK_REMOTE_DEFAULT_BRANCH_NAMES,
|
|
54
53
|
ALL_GIT_MERGE_BASE_COMMAND_PREFIX,
|
|
55
54
|
ALL_GIT_SYMBOLIC_REFERENCE_COMMAND_PREFIX,
|
|
56
55
|
ALL_GIT_VERIFY_REFERENCE_COMMAND_PREFIX,
|
|
@@ -58,6 +57,7 @@ from git_hooks_constants import (
|
|
|
58
57
|
ALL_ZEROS_OBJECT_NAME_CHARACTER,
|
|
59
58
|
BASE_REFERENCE_ARGUMENT,
|
|
60
59
|
DEFAULT_REMOTE_BASE_REFERENCE,
|
|
60
|
+
DEFAULT_REMOTE_NAME,
|
|
61
61
|
GATE_INFRASTRUCTURE_FAILURE_EXIT_CODE,
|
|
62
62
|
GIT_COMMAND_TIMEOUT_SECONDS,
|
|
63
63
|
GIT_COMMAND_UNAVAILABLE_MESSAGE,
|
|
@@ -72,11 +72,10 @@ from git_hooks_constants import (
|
|
|
72
72
|
MALFORMED_STDIN_LINE_MESSAGE,
|
|
73
73
|
NO_PARSEABLE_STDIN_LINES_MESSAGE,
|
|
74
74
|
NO_PARSEABLE_STDIN_LINES_SENTINEL,
|
|
75
|
-
ORIGIN_HEAD_SYMBOLIC_REFERENCE,
|
|
76
|
-
ORIGIN_REMOTE_TRACKING_REFERENCE_PREFIX,
|
|
77
75
|
PRE_PUSH_GATE_SCRIPT_NOT_FOUND_MESSAGE,
|
|
78
76
|
PROTECTED_BRANCH_PUSH_BLOCK_EXIT_CODE,
|
|
79
77
|
PROTECTED_BRANCH_PUSH_BLOCK_MESSAGE,
|
|
78
|
+
REMOTE_BRANCH_REFERENCE_TEMPLATE,
|
|
80
79
|
REMOTE_REFERENCE_FIELD_INDEX,
|
|
81
80
|
STDIN_LINE_FIELD_COUNT,
|
|
82
81
|
STDIN_READ_FAILURE_MESSAGE,
|
|
@@ -84,7 +83,6 @@ from git_hooks_constants import (
|
|
|
84
83
|
UNRESOLVABLE_MERGE_BASE_MESSAGE,
|
|
85
84
|
UNRESOLVABLE_MERGE_BASE_SENTINEL,
|
|
86
85
|
)
|
|
87
|
-
from gate_utils import is_safe_regular_file, resolve_gate_script_path
|
|
88
86
|
from pre_push_base_reference import (
|
|
89
87
|
resolve_remote_name_from_arguments,
|
|
90
88
|
resolve_usable_base_reference,
|
|
@@ -294,24 +292,24 @@ def run_git_reference_query(all_git_arguments: tuple[str, ...]) -> str | None:
|
|
|
294
292
|
return decoded_output.strip() or None
|
|
295
293
|
|
|
296
294
|
|
|
297
|
-
def resolve_default_branch_reference() -> str | None:
|
|
295
|
+
def resolve_default_branch_reference(remote_name: str = DEFAULT_REMOTE_NAME) -> str | None:
|
|
298
296
|
"""Return the remote-tracking ref name of the remote's default branch.
|
|
299
297
|
|
|
300
|
-
The remote's HEAD symbolic ref answers first. When it is unset,
|
|
301
|
-
|
|
298
|
+
The remote's HEAD symbolic ref answers first. When it is unset, the
|
|
299
|
+
supported default-branch tracking refs answer in turn.
|
|
302
300
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
over ``origin/master``, so a repo whose default is another branch beside a
|
|
306
|
-
legacy ``main`` resolves the wrong base ΓÇö the assumption set tracked at
|
|
307
|
-
~/.claude/orchestrator-runs/falsify-first/parked-items.md row 3.
|
|
301
|
+
Args:
|
|
302
|
+
remote_name: Remote whose default branch provides the gate base.
|
|
308
303
|
|
|
309
304
|
Returns:
|
|
310
305
|
A remote-tracking ref name, or None when no default-branch ref
|
|
311
306
|
resolves.
|
|
312
307
|
"""
|
|
308
|
+
remote_head_reference = REMOTE_BRANCH_REFERENCE_TEMPLATE.format(
|
|
309
|
+
remote=remote_name, branch="HEAD"
|
|
310
|
+
)
|
|
313
311
|
symbolic_reference = run_git_reference_query(
|
|
314
|
-
(*ALL_GIT_SYMBOLIC_REFERENCE_COMMAND_PREFIX,
|
|
312
|
+
(*ALL_GIT_SYMBOLIC_REFERENCE_COMMAND_PREFIX, remote_head_reference)
|
|
315
313
|
)
|
|
316
314
|
if symbolic_reference is not None:
|
|
317
315
|
verified_symbolic_target = run_git_reference_query(
|
|
@@ -319,7 +317,10 @@ def resolve_default_branch_reference() -> str | None:
|
|
|
319
317
|
)
|
|
320
318
|
if verified_symbolic_target is not None:
|
|
321
319
|
return symbolic_reference
|
|
322
|
-
for
|
|
320
|
+
for each_branch_name in ALL_FALLBACK_REMOTE_DEFAULT_BRANCH_NAMES:
|
|
321
|
+
each_candidate_reference = REMOTE_BRANCH_REFERENCE_TEMPLATE.format(
|
|
322
|
+
remote=remote_name, branch=each_branch_name
|
|
323
|
+
)
|
|
323
324
|
verified_reference = run_git_reference_query(
|
|
324
325
|
(*ALL_GIT_VERIFY_REFERENCE_COMMAND_PREFIX, each_candidate_reference)
|
|
325
326
|
)
|
|
@@ -360,7 +361,9 @@ def find_branch_update_push(stdin_text: str) -> PushLine | None:
|
|
|
360
361
|
|
|
361
362
|
|
|
362
363
|
def resolve_default_branch_merge_base(
|
|
363
|
-
remote_branch_name: str,
|
|
364
|
+
remote_branch_name: str,
|
|
365
|
+
pushed_object_name: str,
|
|
366
|
+
remote_name: str = DEFAULT_REMOTE_NAME,
|
|
364
367
|
) -> str | None:
|
|
365
368
|
"""Return the merge base of the pushed object and the default branch.
|
|
366
369
|
|
|
@@ -371,6 +374,7 @@ def resolve_default_branch_merge_base(
|
|
|
371
374
|
Args:
|
|
372
375
|
remote_branch_name: The remote branch the push updates.
|
|
373
376
|
pushed_object_name: The object name the push carries.
|
|
377
|
+
remote_name: The remote whose default branch provides the gate base.
|
|
374
378
|
|
|
375
379
|
Returns:
|
|
376
380
|
The merge-base object name; None when no default-branch ref resolves or
|
|
@@ -378,12 +382,13 @@ def resolve_default_branch_merge_base(
|
|
|
378
382
|
no merge base applies; or the unresolvable-merge-base sentinel when a
|
|
379
383
|
default-branch ref resolves and git still reports no merge base.
|
|
380
384
|
"""
|
|
381
|
-
default_branch_reference = resolve_default_branch_reference()
|
|
385
|
+
default_branch_reference = resolve_default_branch_reference(remote_name)
|
|
382
386
|
if default_branch_reference is None:
|
|
383
387
|
return None
|
|
384
|
-
|
|
385
|
-
|
|
388
|
+
default_branch_prefix = REMOTE_BRANCH_REFERENCE_TEMPLATE.format(
|
|
389
|
+
remote=remote_name, branch=""
|
|
386
390
|
)
|
|
391
|
+
default_branch_name = default_branch_reference.removeprefix(default_branch_prefix)
|
|
387
392
|
if remote_branch_name == default_branch_name:
|
|
388
393
|
return None
|
|
389
394
|
# Anchors to the pushed object; deferring to the gate's HEAD-based merge-base would change behavior on non-HEAD pushes.
|
|
@@ -399,7 +404,9 @@ def resolve_default_branch_merge_base(
|
|
|
399
404
|
return merge_base_object_name
|
|
400
405
|
|
|
401
406
|
|
|
402
|
-
def resolve_gate_base_reference(
|
|
407
|
+
def resolve_gate_base_reference(
|
|
408
|
+
stdin_text: str, remote_name: str = DEFAULT_REMOTE_NAME
|
|
409
|
+
) -> str | None:
|
|
403
410
|
"""Return the reference the gate scopes its changed lines to.
|
|
404
411
|
|
|
405
412
|
A branch update reads from the merge base with the remote default branch,
|
|
@@ -414,20 +421,24 @@ def resolve_gate_base_reference(stdin_text: str) -> str | None:
|
|
|
414
421
|
|
|
415
422
|
Args:
|
|
416
423
|
stdin_text: The pre-push stdin payload.
|
|
424
|
+
remote_name: The remote whose default branch provides the gate base.
|
|
417
425
|
|
|
418
426
|
Returns:
|
|
419
427
|
The gate's base reference, the no-parseable-lines sentinel, the
|
|
420
428
|
unresolvable-merge-base sentinel, or None when the push only deletes
|
|
421
429
|
remote branches.
|
|
422
430
|
"""
|
|
423
|
-
return _resolve_gate_base_from_parsed(_parse_push_stdin(stdin_text))
|
|
431
|
+
return _resolve_gate_base_from_parsed(_parse_push_stdin(stdin_text), remote_name)
|
|
424
432
|
|
|
425
433
|
|
|
426
|
-
def _resolve_gate_base_from_parsed(
|
|
434
|
+
def _resolve_gate_base_from_parsed(
|
|
435
|
+
parsed_stdin: ParsedPushStdin, remote_name: str = DEFAULT_REMOTE_NAME
|
|
436
|
+
) -> str | None:
|
|
427
437
|
"""Return the gate's base reference for an already-parsed stdin payload.
|
|
428
438
|
|
|
429
439
|
Args:
|
|
430
440
|
parsed_stdin: The parsed stdin payload.
|
|
441
|
+
remote_name: The remote whose default branch provides the gate base.
|
|
431
442
|
|
|
432
443
|
Returns:
|
|
433
444
|
The merge base with the remote default branch for a branch update, the
|
|
@@ -440,7 +451,9 @@ def _resolve_gate_base_from_parsed(parsed_stdin: ParsedPushStdin) -> str | None:
|
|
|
440
451
|
if branch_update is None:
|
|
441
452
|
return _resolve_base_reference_from_lines(parsed_stdin)
|
|
442
453
|
merge_base_object_name = resolve_default_branch_merge_base(
|
|
443
|
-
branch_update.remote_branch_name,
|
|
454
|
+
branch_update.remote_branch_name,
|
|
455
|
+
branch_update.local_object_name,
|
|
456
|
+
remote_name,
|
|
444
457
|
)
|
|
445
458
|
if merge_base_object_name is None:
|
|
446
459
|
return branch_update.remote_object_name
|
|
@@ -542,7 +555,8 @@ def main() -> int:
|
|
|
542
555
|
file=sys.stderr,
|
|
543
556
|
)
|
|
544
557
|
return 0
|
|
545
|
-
|
|
558
|
+
remote_name = resolve_remote_name_from_arguments(sys.argv)
|
|
559
|
+
base_reference = _resolve_gate_base_from_parsed(parsed_stdin, remote_name)
|
|
546
560
|
if base_reference is None:
|
|
547
561
|
return 0
|
|
548
562
|
if base_reference == no_parseable_stdin_lines_sentinel:
|
|
@@ -550,8 +564,7 @@ def main() -> int:
|
|
|
550
564
|
return gate_infrastructure_failure_exit_code
|
|
551
565
|
if base_reference == unresolvable_merge_base_sentinel:
|
|
552
566
|
print(unresolvable_merge_base_message, file=sys.stderr)
|
|
553
|
-
return
|
|
554
|
-
remote_name = resolve_remote_name_from_arguments(sys.argv)
|
|
567
|
+
return gate_infrastructure_failure_exit_code
|
|
555
568
|
try:
|
|
556
569
|
usable_base_reference = resolve_usable_base_reference(
|
|
557
570
|
base_reference, remote_name, run_git_text_command
|
|
@@ -156,11 +156,10 @@ def test_run_git_reference_query_returns_replaced_text_on_locale_invalid_bytes(
|
|
|
156
156
|
assert resolved_output == expected_replaced_reference
|
|
157
157
|
|
|
158
158
|
|
|
159
|
-
def
|
|
159
|
+
def test_unresolvable_merge_base_message_describes_pending_validation() -> None:
|
|
160
160
|
skip_message = git_hooks_constants.UNRESOLVABLE_MERGE_BASE_MESSAGE
|
|
161
|
-
assert "
|
|
162
|
-
assert "
|
|
163
|
-
assert "merge-base still could not name a shared commit" in skip_message
|
|
161
|
+
assert "CODE_RULES validation is pending" in skip_message
|
|
162
|
+
assert "Restore shared history" in skip_message
|
|
164
163
|
|
|
165
164
|
|
|
166
165
|
def test_run_git_text_command_raises_when_git_cannot_launch(
|
|
@@ -239,6 +238,11 @@ def resolve_remote_head_reference(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
|
239
238
|
return GIT_RESOLVED_EXIT_CODE, RESOLVED_COMMIT_OBJECT_NAME
|
|
240
239
|
|
|
241
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
|
+
)
|
|
242
246
|
|
|
243
247
|
|
|
244
248
|
|
|
@@ -994,6 +998,46 @@ def test_resolve_default_branch_reference_reads_the_origin_head_symbolic_referen
|
|
|
994
998
|
assert pre_push.resolve_default_branch_reference() == ORIGIN_DEFAULT_BRANCH_REFERENCE
|
|
995
999
|
|
|
996
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
|
+
|
|
997
1041
|
def test_resolve_default_branch_reference_returns_none_without_remote_tracking_refs(
|
|
998
1042
|
tmp_path: Path,
|
|
999
1043
|
monkeypatch: pytest.MonkeyPatch,
|
|
@@ -1198,7 +1242,7 @@ def test_main_allows_the_push_when_origin_head_names_an_absent_reference(
|
|
|
1198
1242
|
assert recorded_arguments == ["--base", remote_object_name]
|
|
1199
1243
|
|
|
1200
1244
|
|
|
1201
|
-
def
|
|
1245
|
+
def test_main_keeps_the_gate_pending_when_no_merge_base_resolves(
|
|
1202
1246
|
tmp_path: Path,
|
|
1203
1247
|
monkeypatch: pytest.MonkeyPatch,
|
|
1204
1248
|
capsys: pytest.CaptureFixture[str],
|
|
@@ -1230,13 +1274,73 @@ def test_main_skips_the_gate_when_no_merge_base_resolves(
|
|
|
1230
1274
|
|
|
1231
1275
|
exit_code = pre_push.main()
|
|
1232
1276
|
|
|
1233
|
-
assert exit_code ==
|
|
1277
|
+
assert exit_code == git_hooks_constants.GATE_INFRASTRUCTURE_FAILURE_EXIT_CODE
|
|
1234
1278
|
assert not recorded_arguments_path.exists(), (
|
|
1235
|
-
"the gate
|
|
1279
|
+
"the gate remains pending until the hook verifies a usable base"
|
|
1236
1280
|
)
|
|
1237
1281
|
assert git_hooks_constants.UNRESOLVABLE_MERGE_BASE_MESSAGE in capsys.readouterr().err
|
|
1238
1282
|
|
|
1239
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
|
+
|
|
1240
1344
|
def test_main_allows_default_branch_file_the_rebase_replayed_onto(
|
|
1241
1345
|
tmp_path: Path,
|
|
1242
1346
|
monkeypatch: pytest.MonkeyPatch,
|