claude-dev-env 1.42.0 → 1.44.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/_shared/pr-loop/scripts/_claude_permissions_common.py +1 -5
- package/_shared/pr-loop/scripts/code_rules_gate.py +293 -8
- package/_shared/pr-loop/scripts/fix_hookspath.py +96 -5
- package/_shared/pr-loop/scripts/grant_project_claude_permissions.py +3 -16
- package/_shared/pr-loop/scripts/post_audit_thread.py +4 -4
- package/_shared/pr-loop/scripts/{config → pr_loop_shared_constants}/claude_permissions_constants.py +1 -1
- package/_shared/pr-loop/scripts/preflight.py +13 -31
- package/_shared/pr-loop/scripts/reviews_disabled.py +2 -16
- package/_shared/pr-loop/scripts/revoke_project_claude_permissions.py +3 -16
- package/_shared/pr-loop/scripts/tests/conftest.py +1 -51
- package/_shared/pr-loop/scripts/tests/test_agent_config_carveout.py +4 -4
- package/_shared/pr-loop/scripts/tests/test_claude_permissions_common.py +4 -2
- package/_shared/pr-loop/scripts/tests/test_claude_permissions_constants.py +4 -2
- package/_shared/pr-loop/scripts/tests/test_claude_settings_keys_constants.py +4 -2
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate_constants.py +4 -2
- package/_shared/pr-loop/scripts/tests/test_fix_hookspath_constants.py +6 -2
- package/_shared/pr-loop/scripts/tests/test_grant_project_claude_permissions.py +2 -2
- package/_shared/pr-loop/scripts/tests/test_post_audit_thread.py +1 -2
- package/_shared/pr-loop/scripts/tests/test_post_audit_thread_constants.py +4 -2
- package/_shared/pr-loop/scripts/tests/test_preflight.py +17 -52
- package/_shared/pr-loop/scripts/tests/test_preflight_constants.py +6 -2
- package/_shared/pr-loop/scripts/tests/test_revoke_project_claude_permissions.py +2 -2
- package/agents/pr-description-writer.md +50 -140
- package/docs/PR_DESCRIPTION_GUIDE.md +101 -102
- package/hooks/_gh_pr_author_swap_utils.py +1 -1
- package/hooks/blocking/bot_mention_comment_blocker.py +4 -10
- package/hooks/blocking/code_rules_enforcer.py +217 -99
- package/hooks/blocking/code_rules_path_utils.py +8 -1
- package/hooks/blocking/destructive_command_blocker.py +1 -1
- package/hooks/blocking/es_exe_path_rewriter.py +7 -13
- package/hooks/blocking/gh_body_arg_blocker.py +6 -1
- package/hooks/blocking/gh_pr_author_enforcer.py +5 -5
- package/hooks/blocking/gh_pr_author_restore.py +5 -5
- package/hooks/blocking/hedging_language_blocker.py +4 -10
- package/hooks/blocking/md_path_exemptions.py +205 -0
- package/hooks/blocking/md_to_html_blocker.py +48 -20
- package/hooks/blocking/pr_converge_bugteam_enforcer.py +5 -11
- package/hooks/blocking/pr_description_enforcer.py +626 -41
- package/hooks/blocking/question_to_user_enforcer.py +4 -10
- package/hooks/blocking/state_description_blocker.py +6 -12
- package/hooks/blocking/tdd_enforcer.py +1 -1
- package/hooks/blocking/test_bot_mention_comment_blocker.py +1 -1
- package/hooks/blocking/test_code_rules_enforcer.py +3 -3
- package/hooks/blocking/test_code_rules_enforcer_any_exempt_files.py +1 -1
- package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +0 -2
- package/hooks/blocking/test_code_rules_enforcer_comment_string_awareness.py +184 -0
- package/hooks/blocking/test_code_rules_enforcer_type_checking_scope.py +82 -0
- package/hooks/blocking/test_code_rules_enforcer_unused_imports.py +29 -29
- package/hooks/blocking/test_gh_body_arg_blocker.py +7 -8
- package/hooks/blocking/test_gh_pr_author_enforcer.py +1 -1
- package/hooks/blocking/test_gh_pr_author_restore.py +1 -1
- package/hooks/blocking/test_hedging_language_blocker.py +2 -2
- package/hooks/blocking/test_md_to_html_blocker.py +463 -8
- package/hooks/blocking/test_pr_converge_bugteam_enforcer.py +1 -1
- package/hooks/blocking/test_pr_description_enforcer.py +1210 -13
- package/hooks/blocking/test_question_to_user_enforcer.py +1 -1
- package/hooks/blocking/windows_rmtree_blocker.py +5 -11
- package/hooks/diagnostic/hook_log_extractor.py +1 -1
- package/hooks/diagnostic/hook_log_init.py +1 -1
- package/hooks/diagnostic/hook_log_stop_wrapper.py +1 -1
- package/hooks/diagnostic/test_hook_log_extractor.py +1 -1
- package/hooks/diagnostic/test_hook_log_init.py +2 -2
- package/hooks/diagnostic/test_hook_log_stop_wrapper.py +1 -1
- package/hooks/git-hooks/gate_utils.py +1 -1
- package/hooks/git-hooks/pre_commit.py +1 -1
- package/hooks/git-hooks/pre_push.py +1 -1
- package/hooks/git-hooks/test_config.py +5 -5
- package/hooks/git-hooks/test_pre_push.py +6 -6
- package/hooks/{config → hooks_constants}/code_rules_enforcer_constants.py +37 -0
- package/hooks/hooks_constants/code_rules_path_utils_constants.py +28 -0
- package/hooks/hooks_constants/md_to_html_blocker_constants.py +82 -0
- package/hooks/{config → hooks_constants}/pr_converge_bugteam_enforcer_state.py +1 -1
- package/hooks/hooks_constants/pr_description_enforcer_constants.py +154 -0
- package/hooks/{config → hooks_constants}/pre_tool_use_stdin.py +1 -1
- package/hooks/{config → hooks_constants}/project_paths_reader.py +2 -2
- package/hooks/{config → hooks_constants}/test_banned_identifiers_constants.py +1 -1
- package/hooks/{config → hooks_constants}/test_dynamic_stderr_handler.py +1 -1
- package/hooks/{config → hooks_constants}/test_hardcoded_user_path_constants.py +1 -1
- package/hooks/{config → hooks_constants}/test_hook_log_extractor_constants.py +2 -2
- package/hooks/hooks_constants/test_md_to_html_blocker_constants.py +110 -0
- package/hooks/{config → hooks_constants}/test_messages.py +2 -6
- package/hooks/{config → hooks_constants}/test_path_rewriter_constants.py +1 -1
- package/hooks/hooks_constants/test_pr_description_enforcer_constants.py +292 -0
- package/hooks/{config → hooks_constants}/test_pre_tool_use_stdin.py +2 -2
- package/hooks/{config → hooks_constants}/test_project_paths_reader.py +3 -3
- package/hooks/{config → hooks_constants}/test_session_env_cleanup_constants.py +1 -1
- package/hooks/{config → hooks_constants}/test_setup_project_paths_constants.py +2 -2
- package/hooks/{config → hooks_constants}/test_unused_module_import_constants.py +1 -1
- package/hooks/lifecycle/pr_converge_bugteam_skill_tracker.py +5 -11
- package/hooks/lifecycle/test_pr_converge_bugteam_skill_tracker.py +1 -1
- package/hooks/session/gh_pr_author_session_cleanup.py +5 -6
- package/hooks/session/session_env_cleanup.py +4 -10
- package/hooks/session/test_gh_pr_author_session_cleanup.py +1 -1
- package/hooks/session/test_untracked_repo_detector.py +2 -2
- package/hooks/session/untracked_repo_detector.py +6 -12
- package/hooks/test__gh_pr_author_swap_utils.py +1 -1
- package/hooks/validators/run_all_validators.py +16 -5
- package/hooks/validators/test_output_formatter.py +46 -0
- package/hooks/workflow/doc_gist_auto_publish.py +1 -1
- package/hooks/workflow/md_to_html_companion.py +8 -15
- package/hooks/workflow/test_md_to_html_companion.py +184 -23
- package/package.json +1 -1
- package/rules/ask-user-question-required.md +1 -1
- package/rules/vault-context.md +1 -1
- package/scripts/{config → dev_env_scripts_constants}/timing.py +1 -1
- package/scripts/setup_project_paths.py +49 -11
- package/scripts/sweep_empty_dirs.py +10 -1
- package/scripts/test_setup_project_paths.py +2 -2
- package/scripts/test_sweep_empty_dirs.py +2 -6
- package/skills/_shared/pr-loop/scripts/_path_resolver.py +1 -1
- package/skills/_shared/pr-loop/scripts/build_audit_prompt.py +1 -1
- package/skills/_shared/pr-loop/scripts/build_fix_prompt.py +1 -1
- package/skills/_shared/pr-loop/scripts/init_loop_state.py +1 -1
- package/skills/_shared/pr-loop/scripts/teardown_worktrees.py +1 -1
- package/skills/_shared/pr-loop/scripts/write_audit_outcomes.py +2 -2
- package/skills/_shared/pr-loop/scripts/write_fix_outcomes.py +2 -2
- package/skills/bugteam/PROMPTS.md +1 -1
- package/skills/bugteam/SKILL.md +1 -1
- package/skills/bugteam/reference/github-pr-reviews.md +1 -1
- package/skills/bugteam/scripts/{_claude_permissions_common.py → _bugteam_permissions_common.py} +1 -13
- package/skills/bugteam/scripts/bugteam_code_rules_gate.py +1 -13
- package/skills/bugteam/scripts/bugteam_fix_hookspath.py +1 -16
- package/skills/bugteam/scripts/bugteam_preflight.py +1 -13
- package/skills/bugteam/scripts/grant_project_claude_permissions.py +2 -8
- package/skills/bugteam/scripts/probe_code_rules_enforcer_check.py +1 -1
- package/skills/bugteam/scripts/reflow_skill_md.py +1 -1
- package/skills/bugteam/scripts/revoke_project_claude_permissions.py +2 -8
- package/skills/bugteam/scripts/{test__claude_permissions_common.py → test__bugteam_permissions_common.py} +4 -4
- package/skills/bugteam/scripts/test_agent_config_carveout.py +2 -2
- package/skills/bugteam/scripts/test_bugteam_fix_hookspath.py +0 -26
- package/skills/bugteam/scripts/{test_claude_permissions_common.py → test_bugteam_permissions_common.py} +3 -66
- package/skills/bugteam/scripts/test_bugteam_preflight.py +2 -27
- package/skills/bugteam/scripts/windows_safe_rmtree.py +1 -1
- package/skills/doc-gist/SKILL.md +1 -1
- package/skills/doc-gist/scripts/gist_upload.py +1 -1
- package/skills/implement/SKILL.md +2 -2
- package/skills/implement/scripts/append_note.py +1 -1
- package/skills/pr-converge/pr_converge_skill_constants/__init__.py +0 -0
- package/skills/pr-converge/{config → pr_converge_skill_constants}/constants.py +1 -1
- package/skills/pr-converge/scripts/check_bugbot_ci.py +1 -1
- package/skills/pr-converge/scripts/check_convergence.py +11 -4
- package/skills/pr-converge/scripts/check_pending_reviews.py +1 -1
- package/skills/pr-converge/scripts/fetch_copilot_reviews.py +1 -1
- package/skills/pr-converge/scripts/post_fix_reply.py +1 -1
- package/skills/pr-converge/scripts/pr_converge_scripts_constants/__init__.py +0 -0
- package/skills/pr-converge/scripts/{config → pr_converge_scripts_constants}/pr_converge_constants.py +1 -1
- package/skills/pr-converge/scripts/reflow_skill_md.py +90 -16
- package/skills/pr-converge/scripts/test_check_convergence.py +18 -0
- package/skills/pr-converge/scripts/test_reflow_skill_md.py +0 -31
- package/skills/pre-compact/SKILL.md +114 -0
- package/skills/session-log/SKILL.md +98 -233
- package/hooks/config/pr_description_enforcer_constants.py +0 -19
- package/hooks/config/test_pr_description_enforcer_constants.py +0 -82
- package/skills/bugteam/scripts/test_grant_project_claude_permissions.py +0 -55
- package/skills/bugteam/scripts/test_revoke_project_claude_permissions.py +0 -55
- package/skills/pr-converge/scripts/conftest.py +0 -60
- package/skills/pr-converge/scripts/evict_cached_config_modules.py +0 -20
- package/skills/pr-converge/scripts/test_evict_cached_config_modules.py +0 -22
- /package/_shared/pr-loop/scripts/{config → pr_loop_shared_constants}/__init__.py +0 -0
- /package/_shared/pr-loop/scripts/{config → pr_loop_shared_constants}/claude_settings_keys_constants.py +0 -0
- /package/_shared/pr-loop/scripts/{config → pr_loop_shared_constants}/code_rules_gate_constants.py +0 -0
- /package/_shared/pr-loop/scripts/{config → pr_loop_shared_constants}/fix_hookspath_constants.py +0 -0
- /package/_shared/pr-loop/scripts/{config → pr_loop_shared_constants}/post_audit_thread_constants.py +0 -0
- /package/_shared/pr-loop/scripts/{config → pr_loop_shared_constants}/preflight_constants.py +0 -0
- /package/_shared/pr-loop/scripts/{config → pr_loop_shared_constants}/reviews_disabled_constants.py +0 -0
- /package/hooks/git-hooks/{config.py → git_hooks_constants/__init__.py} +0 -0
- /package/hooks/{config → hooks_constants}/__init__.py +0 -0
- /package/hooks/{config → hooks_constants}/any_type_config.py +0 -0
- /package/hooks/{config → hooks_constants}/banned_identifiers_constants.py +0 -0
- /package/hooks/{config → hooks_constants}/blocking_check_limits.py +0 -0
- /package/hooks/{config → hooks_constants}/bot_mention_comment_blocker_constants.py +0 -0
- /package/hooks/{config → hooks_constants}/convergence_branch_constants.py +0 -0
- /package/hooks/{config → hooks_constants}/doc_gist_auto_publish_constants.py +0 -0
- /package/hooks/{config → hooks_constants}/dynamic_stderr_handler.py +0 -0
- /package/hooks/{config → hooks_constants}/gh_pr_author_swap_constants.py +0 -0
- /package/hooks/{config → hooks_constants}/hardcoded_user_path_constants.py +0 -0
- /package/hooks/{config → hooks_constants}/hook_log_extractor_constants.py +0 -0
- /package/hooks/{config → hooks_constants}/html_companion_constants.py +0 -0
- /package/hooks/{config → hooks_constants}/inline_tuple_string_magic_constants.py +0 -0
- /package/hooks/{config → hooks_constants}/messages.py +0 -0
- /package/hooks/{config → hooks_constants}/path_rewriter_constants.py +0 -0
- /package/hooks/{config → hooks_constants}/pr_converge_bugteam_enforcer_constants.py +0 -0
- /package/hooks/{config → hooks_constants}/session_env_cleanup_constants.py +0 -0
- /package/hooks/{config → hooks_constants}/setup_project_paths_constants.py +0 -0
- /package/hooks/{config → hooks_constants}/state_description_blocker_constants.py +0 -0
- /package/hooks/{config → hooks_constants}/stuttering_check_config.py +0 -0
- /package/hooks/{config → hooks_constants}/stuttering_import_binding_constants.py +0 -0
- /package/hooks/{config → hooks_constants}/sys_path_insert_constants.py +0 -0
- /package/hooks/{config → hooks_constants}/unused_module_import_constants.py +0 -0
- /package/hooks/{config → hooks_constants}/windows_rmtree_blocker_constants.py +0 -0
- /package/{skills/_shared/pr-loop/scripts/config → hooks/lifecycle}/__init__.py +0 -0
- /package/{skills/bugteam/scripts/config → hooks/session}/__init__.py +0 -0
- /package/scripts/{config → dev_env_scripts_constants}/__init__.py +0 -0
- /package/skills/{doc-gist/scripts/config → _shared/pr-loop/scripts/skills_pr_loop_constants}/__init__.py +0 -0
- /package/skills/_shared/pr-loop/scripts/{config → skills_pr_loop_constants}/path_resolver_constants.py +0 -0
- /package/skills/{implement/scripts/config → bugteam/scripts/bugteam_scripts_constants}/__init__.py +0 -0
- /package/skills/bugteam/scripts/{config → bugteam_scripts_constants}/bugteam_code_rules_gate_constants.py +0 -0
- /package/skills/bugteam/scripts/{config → bugteam_scripts_constants}/bugteam_fix_hookspath_constants.py +0 -0
- /package/skills/bugteam/scripts/{config → bugteam_scripts_constants}/bugteam_preflight_constants.py +0 -0
- /package/skills/bugteam/scripts/{config → bugteam_scripts_constants}/claude_permissions_common_constants.py +0 -0
- /package/skills/bugteam/scripts/{config → bugteam_scripts_constants}/probe_code_rules_enforcer_check_constants.py +0 -0
- /package/skills/bugteam/scripts/{config → bugteam_scripts_constants}/reflow_skill_md_constants.py +0 -0
- /package/skills/bugteam/scripts/{config → bugteam_scripts_constants}/windows_safe_rmtree_constants.py +0 -0
- /package/skills/{pr-converge/config → doc-gist/scripts/doc_gist_scripts_constants}/__init__.py +0 -0
- /package/skills/doc-gist/scripts/{config → doc_gist_scripts_constants}/gist_upload_constants.py +0 -0
- /package/skills/{pr-converge/scripts/config → implement/scripts/implement_scripts_constants}/__init__.py +0 -0
- /package/skills/implement/scripts/{config → implement_scripts_constants}/notes_constants.py +0 -0
- /package/skills/pr-converge/scripts/{config → pr_converge_scripts_constants}/reflow_skill_md_constants.py +0 -0
|
@@ -17,11 +17,7 @@ from collections.abc import Callable
|
|
|
17
17
|
from pathlib import Path
|
|
18
18
|
from typing import NoReturn
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
if str(Path(__file__).resolve().parent) not in sys.path:
|
|
22
|
-
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
23
|
-
|
|
24
|
-
from config.claude_permissions_constants import (
|
|
20
|
+
from pr_loop_shared_constants.claude_permissions_constants import (
|
|
25
21
|
ALL_TRUST_ENTRY_PROJECT_PATH_BOUNDARY_QUOTE_CHARACTERS,
|
|
26
22
|
CLAUDE_SETTINGS_DIRECTORY_NAME,
|
|
27
23
|
GIT_DIRECTORY_NAME,
|
|
@@ -7,11 +7,11 @@ import sys
|
|
|
7
7
|
from collections.abc import Callable, Iterator
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
if
|
|
12
|
-
sys.path.insert(0,
|
|
10
|
+
parent_directory = str(Path(__file__).resolve().parent)
|
|
11
|
+
if parent_directory not in sys.path:
|
|
12
|
+
sys.path.insert(0, parent_directory)
|
|
13
13
|
|
|
14
|
-
from
|
|
14
|
+
from pr_loop_shared_constants.code_rules_gate_constants import ( # noqa: E402
|
|
15
15
|
ALL_CODE_FILE_EXTENSIONS,
|
|
16
16
|
ALL_GIT_DIFF_CACHED_NAME_ONLY_NULL_TERMINATED_COMMAND,
|
|
17
17
|
ALL_GIT_DIFF_NAME_ONLY_NULL_TERMINATED_COMMAND_PREFIX,
|
|
@@ -41,6 +41,18 @@ def violation_line_pattern() -> re.Pattern[str]:
|
|
|
41
41
|
|
|
42
42
|
|
|
43
43
|
def resolve_claude_dev_env_root(starting_path: Path) -> Path:
|
|
44
|
+
"""Walk up from *starting_path* to the claude-dev-env package root.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
starting_path: A path inside the worktree; the function climbs to
|
|
48
|
+
find the ancestor containing ``hooks/blocking/code_rules_enforcer.py``.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
The resolved package root that contains the enforcer file.
|
|
52
|
+
|
|
53
|
+
Raises:
|
|
54
|
+
SystemExit: When no ancestor contains the enforcer.
|
|
55
|
+
"""
|
|
44
56
|
starting = Path(starting_path).resolve()
|
|
45
57
|
enforcer_relative = Path("hooks") / "blocking" / "code_rules_enforcer.py"
|
|
46
58
|
for each_candidate in [starting, *starting.parents]:
|
|
@@ -66,6 +78,15 @@ def _resolve_package_root_absolute(starting_path: Path) -> Path:
|
|
|
66
78
|
|
|
67
79
|
|
|
68
80
|
def load_validate_content() -> ValidateContentCallable:
|
|
81
|
+
"""Load ``code_rules_enforcer.validate_content`` for in-process use.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
The ``validate_content`` callable from the enforcer module.
|
|
85
|
+
|
|
86
|
+
Raises:
|
|
87
|
+
SystemExit: When the package root cannot be located or the
|
|
88
|
+
enforcer module cannot be loaded from disk.
|
|
89
|
+
"""
|
|
69
90
|
package_root = resolve_claude_dev_env_root(Path(__file__).resolve())
|
|
70
91
|
enforcer_path = package_root / "hooks" / "blocking" / "code_rules_enforcer.py"
|
|
71
92
|
if not enforcer_path.is_file():
|
|
@@ -85,11 +106,11 @@ def load_validate_content() -> ValidateContentCallable:
|
|
|
85
106
|
while hooks_root_path in sys.path:
|
|
86
107
|
sys.path.remove(hooks_root_path)
|
|
87
108
|
sys.path.insert(0, hooks_root_path)
|
|
88
|
-
|
|
109
|
+
saved_hooks_constants_modules = {
|
|
89
110
|
each_module_name: sys.modules.pop(each_module_name)
|
|
90
111
|
for each_module_name in [
|
|
91
112
|
each_key for each_key in list(sys.modules)
|
|
92
|
-
if each_key == "
|
|
113
|
+
if each_key == "hooks_constants" or each_key.startswith("hooks_constants.")
|
|
93
114
|
]
|
|
94
115
|
}
|
|
95
116
|
try:
|
|
@@ -99,14 +120,26 @@ def load_validate_content() -> ValidateContentCallable:
|
|
|
99
120
|
sys.path.remove(hooks_root_path)
|
|
100
121
|
for each_module_name in [
|
|
101
122
|
each_key for each_key in list(sys.modules)
|
|
102
|
-
if each_key == "
|
|
123
|
+
if each_key == "hooks_constants" or each_key.startswith("hooks_constants.")
|
|
103
124
|
]:
|
|
104
125
|
sys.modules.pop(each_module_name, None)
|
|
105
|
-
sys.modules.update(
|
|
126
|
+
sys.modules.update(saved_hooks_constants_modules)
|
|
106
127
|
return module.validate_content
|
|
107
128
|
|
|
108
129
|
|
|
109
130
|
def resolve_merge_base(repository_root: Path, base_reference: str) -> str:
|
|
131
|
+
"""Return the merge-base SHA between HEAD and *base_reference*.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
repository_root: Repository root used as the ``git -C`` target.
|
|
135
|
+
base_reference: The git reference to merge-base against.
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
The stripped merge-base SHA.
|
|
139
|
+
|
|
140
|
+
Raises:
|
|
141
|
+
SystemExit: When ``git merge-base`` returns non-zero.
|
|
142
|
+
"""
|
|
110
143
|
merge_result = subprocess.run(
|
|
111
144
|
["git", "merge-base", "HEAD", base_reference],
|
|
112
145
|
cwd=str(repository_root),
|
|
@@ -131,6 +164,19 @@ def filter_paths_under_prefixes(
|
|
|
131
164
|
repository_root: Path,
|
|
132
165
|
all_prefixes: list[str],
|
|
133
166
|
) -> list[Path]:
|
|
167
|
+
"""Filter *all_file_paths* to entries falling under the supplied prefixes.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
all_file_paths: Resolved file paths to filter.
|
|
171
|
+
repository_root: Repository root used to compute relative paths.
|
|
172
|
+
all_prefixes: Repository-relative POSIX prefixes; each path must
|
|
173
|
+
equal one prefix or be nested beneath it to pass through.
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
The subset of *all_file_paths* whose relative POSIX path matches one
|
|
177
|
+
of the prefixes. When *all_prefixes* is empty, returns the input
|
|
178
|
+
list unchanged.
|
|
179
|
+
"""
|
|
134
180
|
if not all_prefixes:
|
|
135
181
|
return all_file_paths
|
|
136
182
|
normalized_prefixes = [
|
|
@@ -157,6 +203,19 @@ def filter_paths_under_prefixes(
|
|
|
157
203
|
|
|
158
204
|
|
|
159
205
|
def paths_from_git_staged(repository_root: Path) -> list[Path]:
|
|
206
|
+
"""Return absolute paths for every file in the staged index.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
repository_root: Repository root used as the ``git -C`` target.
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
List of absolute paths for staged files. Names whose bytes cannot
|
|
213
|
+
be decoded as Unicode are logged and skipped.
|
|
214
|
+
|
|
215
|
+
Raises:
|
|
216
|
+
SystemExit: When ``git diff --cached --name-only -z`` returns
|
|
217
|
+
non-zero.
|
|
218
|
+
"""
|
|
160
219
|
name_result = subprocess.run(
|
|
161
220
|
list(ALL_GIT_DIFF_CACHED_NAME_ONLY_NULL_TERMINATED_COMMAND),
|
|
162
221
|
cwd=str(repository_root),
|
|
@@ -191,6 +250,19 @@ def staged_file_line_count(
|
|
|
191
250
|
repository_root: Path,
|
|
192
251
|
relative_path_posix: str,
|
|
193
252
|
) -> int:
|
|
253
|
+
"""Return the staged-blob line count for *relative_path_posix*.
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
repository_root: Repository root used as the ``git -C`` target.
|
|
257
|
+
relative_path_posix: Repository-relative POSIX path of the staged
|
|
258
|
+
file.
|
|
259
|
+
|
|
260
|
+
Returns:
|
|
261
|
+
The staged content line count, or zero when the blob is empty.
|
|
262
|
+
|
|
263
|
+
Raises:
|
|
264
|
+
SystemExit: When ``git show :<path>`` returns non-zero.
|
|
265
|
+
"""
|
|
194
266
|
show_result = subprocess.run(
|
|
195
267
|
["git", "show", f":{relative_path_posix}"],
|
|
196
268
|
cwd=str(repository_root),
|
|
@@ -217,6 +289,20 @@ def is_staged_file_newly_added(
|
|
|
217
289
|
repository_root: Path,
|
|
218
290
|
relative_path_posix: str,
|
|
219
291
|
) -> bool:
|
|
292
|
+
"""Check whether *relative_path_posix* is newly added in the staged diff.
|
|
293
|
+
|
|
294
|
+
Args:
|
|
295
|
+
repository_root: Repository root used as the ``git -C`` target.
|
|
296
|
+
relative_path_posix: Repository-relative POSIX path to inspect.
|
|
297
|
+
|
|
298
|
+
Returns:
|
|
299
|
+
True when the first non-empty name-status line begins with the git
|
|
300
|
+
added-prefix; False otherwise.
|
|
301
|
+
|
|
302
|
+
Raises:
|
|
303
|
+
SystemExit: When ``git diff --cached --name-status`` returns
|
|
304
|
+
non-zero.
|
|
305
|
+
"""
|
|
220
306
|
status_result = subprocess.run(
|
|
221
307
|
["git", "diff", "--cached", "--name-status", "--", relative_path_posix],
|
|
222
308
|
cwd=str(repository_root),
|
|
@@ -244,6 +330,19 @@ def added_lines_for_staged_file(
|
|
|
244
330
|
repository_root: Path,
|
|
245
331
|
relative_path_posix: str,
|
|
246
332
|
) -> set[int]:
|
|
333
|
+
"""Return added line numbers within the staged diff for one file.
|
|
334
|
+
|
|
335
|
+
Args:
|
|
336
|
+
repository_root: Repository root used as the ``git -C`` target.
|
|
337
|
+
relative_path_posix: Repository-relative POSIX path to inspect.
|
|
338
|
+
|
|
339
|
+
Returns:
|
|
340
|
+
Set of line numbers (1-indexed) added in the staged diff. When the
|
|
341
|
+
file is newly added, returns every line in the staged blob.
|
|
342
|
+
|
|
343
|
+
Raises:
|
|
344
|
+
SystemExit: When the staged diff command returns non-zero.
|
|
345
|
+
"""
|
|
247
346
|
diff_result = subprocess.run(
|
|
248
347
|
["git", "diff", "--cached", "--unified=0", "--", relative_path_posix],
|
|
249
348
|
cwd=str(repository_root),
|
|
@@ -273,6 +372,16 @@ def added_lines_by_file_staged(
|
|
|
273
372
|
repository_root: Path,
|
|
274
373
|
all_file_paths: list[Path],
|
|
275
374
|
) -> dict[Path, set[int]]:
|
|
375
|
+
"""Build a per-file map of staged-added line numbers.
|
|
376
|
+
|
|
377
|
+
Args:
|
|
378
|
+
repository_root: Repository root for diff invocations.
|
|
379
|
+
all_file_paths: File paths whose added lines should be collected.
|
|
380
|
+
|
|
381
|
+
Returns:
|
|
382
|
+
Mapping from resolved file path to the set of staged-added line
|
|
383
|
+
numbers.
|
|
384
|
+
"""
|
|
276
385
|
resolved_root = repository_root.resolve()
|
|
277
386
|
added_by_path: dict[Path, set[int]] = {}
|
|
278
387
|
for each_path in all_file_paths:
|
|
@@ -291,6 +400,20 @@ def added_lines_by_file_staged(
|
|
|
291
400
|
|
|
292
401
|
|
|
293
402
|
def paths_from_git_diff(repository_root: Path, base_reference: str) -> list[Path]:
|
|
403
|
+
"""Return absolute paths for every file changed since *base_reference*.
|
|
404
|
+
|
|
405
|
+
Args:
|
|
406
|
+
repository_root: Repository root used as the ``git -C`` target.
|
|
407
|
+
base_reference: The git reference to merge-base against.
|
|
408
|
+
|
|
409
|
+
Returns:
|
|
410
|
+
List of absolute paths changed since the merge-base of HEAD and
|
|
411
|
+
*base_reference*.
|
|
412
|
+
|
|
413
|
+
Raises:
|
|
414
|
+
SystemExit: When the ``git diff --name-only`` command returns
|
|
415
|
+
non-zero.
|
|
416
|
+
"""
|
|
294
417
|
merge_base = resolve_merge_base(repository_root, base_reference)
|
|
295
418
|
diff_command = list(ALL_GIT_DIFF_NAME_ONLY_NULL_TERMINATED_COMMAND_PREFIX) + [
|
|
296
419
|
f"{merge_base}..HEAD"
|
|
@@ -336,6 +459,13 @@ def is_test_path(file_path: str) -> bool:
|
|
|
336
459
|
Mirrors the test-file detection rule documented in CODE_RULES.md:
|
|
337
460
|
filename matches test_*.py OR *_test.py OR *.test.* OR *.spec.* OR
|
|
338
461
|
conftest.py, OR path contains the segment /tests/.
|
|
462
|
+
|
|
463
|
+
Args:
|
|
464
|
+
file_path: Path string to classify; backslashes are normalized to
|
|
465
|
+
forward slashes before pattern matching.
|
|
466
|
+
|
|
467
|
+
Returns:
|
|
468
|
+
True when the path matches any test-file pattern; False otherwise.
|
|
339
469
|
"""
|
|
340
470
|
normalized_posix = file_path.replace("\\", "/")
|
|
341
471
|
filename_only = normalized_posix.rsplit("/", maxsplit=1)[-1]
|
|
@@ -395,6 +525,15 @@ def check_wrapper_plumb_through(content: str, file_path: str) -> list[str]:
|
|
|
395
525
|
separate call sites; only the enclosing Call is inspected. This avoids
|
|
396
526
|
false positives where a callee nested as an argument is confused with a
|
|
397
527
|
top-level delegate invocation (for example `delegate(helper(x))`).
|
|
528
|
+
|
|
529
|
+
Args:
|
|
530
|
+
content: File content as a single string for AST parsing.
|
|
531
|
+
file_path: Repository-relative POSIX path of the file (used to
|
|
532
|
+
skip non-Python code extensions early).
|
|
533
|
+
|
|
534
|
+
Returns:
|
|
535
|
+
List of violation strings, one per dropped optional kwarg. Returns
|
|
536
|
+
an empty list when the file is not Python or has a syntax error.
|
|
398
537
|
"""
|
|
399
538
|
non_python_code_extensions = ALL_CODE_FILE_EXTENSIONS - {PYTHON_FILE_EXTENSION}
|
|
400
539
|
lowercase_file_path = file_path.lower()
|
|
@@ -464,6 +603,15 @@ def check_wrapper_plumb_through(content: str, file_path: str) -> list[str]:
|
|
|
464
603
|
|
|
465
604
|
|
|
466
605
|
def parse_added_line_numbers(unified_diff_text: str) -> set[int]:
|
|
606
|
+
"""Extract added line numbers from unified-diff text.
|
|
607
|
+
|
|
608
|
+
Args:
|
|
609
|
+
unified_diff_text: Output from ``git diff --unified=0``.
|
|
610
|
+
|
|
611
|
+
Returns:
|
|
612
|
+
Set of newly-added line numbers (1-indexed) extracted from the
|
|
613
|
+
hunk headers.
|
|
614
|
+
"""
|
|
467
615
|
header_regex = hunk_header_pattern()
|
|
468
616
|
added_line_numbers: set[int] = set()
|
|
469
617
|
for each_line in unified_diff_text.splitlines():
|
|
@@ -485,6 +633,17 @@ def is_file_new_at_base(
|
|
|
485
633
|
merge_base: str,
|
|
486
634
|
relative_path_posix: str,
|
|
487
635
|
) -> bool:
|
|
636
|
+
"""Check whether *relative_path_posix* did not exist at *merge_base*.
|
|
637
|
+
|
|
638
|
+
Args:
|
|
639
|
+
repository_root: Repository root used as the ``git -C`` target.
|
|
640
|
+
merge_base: The merge-base SHA against which to check existence.
|
|
641
|
+
relative_path_posix: Repository-relative POSIX path to inspect.
|
|
642
|
+
|
|
643
|
+
Returns:
|
|
644
|
+
True when ``git cat-file -e`` fails to find the blob at the merge
|
|
645
|
+
base (i.e. the file was added on the HEAD side); False otherwise.
|
|
646
|
+
"""
|
|
488
647
|
cat_result = subprocess.run(
|
|
489
648
|
["git", "cat-file", "-e", f"{merge_base}:{relative_path_posix}"],
|
|
490
649
|
cwd=str(repository_root),
|
|
@@ -502,6 +661,19 @@ def added_lines_for_file(
|
|
|
502
661
|
merge_base: str,
|
|
503
662
|
relative_path_posix: str,
|
|
504
663
|
) -> set[int]:
|
|
664
|
+
"""Return added line numbers for *relative_path_posix* since *merge_base*.
|
|
665
|
+
|
|
666
|
+
Args:
|
|
667
|
+
repository_root: Repository root used as the ``git -C`` target.
|
|
668
|
+
merge_base: The merge-base SHA against which to diff.
|
|
669
|
+
relative_path_posix: Repository-relative POSIX path to inspect.
|
|
670
|
+
|
|
671
|
+
Returns:
|
|
672
|
+
Set of line numbers (1-indexed) added on the HEAD side of the diff.
|
|
673
|
+
|
|
674
|
+
Raises:
|
|
675
|
+
SystemExit: When the diff command returns non-zero.
|
|
676
|
+
"""
|
|
505
677
|
diff_result = subprocess.run(
|
|
506
678
|
[
|
|
507
679
|
"git",
|
|
@@ -531,6 +703,15 @@ def added_lines_for_file(
|
|
|
531
703
|
|
|
532
704
|
|
|
533
705
|
def whole_file_line_set(file_path: Path) -> set[int]:
|
|
706
|
+
"""Return the set of line numbers covering an entire file.
|
|
707
|
+
|
|
708
|
+
Args:
|
|
709
|
+
file_path: Path to the file whose line span should be summarized.
|
|
710
|
+
|
|
711
|
+
Returns:
|
|
712
|
+
Set of line numbers (1-indexed) covering every line in *file_path*,
|
|
713
|
+
or an empty set when the file is unreadable or empty.
|
|
714
|
+
"""
|
|
534
715
|
try:
|
|
535
716
|
total_lines = len(file_path.read_text(encoding="utf-8").splitlines())
|
|
536
717
|
except (OSError, UnicodeDecodeError) as read_error:
|
|
@@ -558,6 +739,17 @@ def renamed_file_source_map_since(
|
|
|
558
739
|
splitting; rename records emit three null-terminated tokens in
|
|
559
740
|
sequence (status, source, destination), other status records emit
|
|
560
741
|
two (status, path).
|
|
742
|
+
|
|
743
|
+
Args:
|
|
744
|
+
repository_root: Repository root used as the ``git -C`` target.
|
|
745
|
+
merge_base: The merge-base SHA against which to diff.
|
|
746
|
+
|
|
747
|
+
Returns:
|
|
748
|
+
Mapping from rename-destination POSIX path to rename-source POSIX
|
|
749
|
+
path. Empty when no rename records are present.
|
|
750
|
+
|
|
751
|
+
Raises:
|
|
752
|
+
SystemExit: When ``git diff --name-status`` returns non-zero.
|
|
561
753
|
"""
|
|
562
754
|
name_status_result = subprocess.run(
|
|
563
755
|
["git", "diff", "--name-status", "-M", "-z", f"{merge_base}..HEAD"],
|
|
@@ -613,6 +805,16 @@ def added_lines_for_renamed_file(
|
|
|
613
805
|
in the source file before the rename. Falls back to whole-file coverage
|
|
614
806
|
when the source blob is absent at the merge base (i.e. the source was
|
|
615
807
|
itself a new or renamed file that landed earlier in the branch).
|
|
808
|
+
|
|
809
|
+
Args:
|
|
810
|
+
repository_root: Repository root used as the ``git -C`` target.
|
|
811
|
+
merge_base: The merge-base SHA against which to compare blobs.
|
|
812
|
+
source_posix: Rename-source POSIX path at the merge base.
|
|
813
|
+
destination_posix: Rename-destination POSIX path at HEAD.
|
|
814
|
+
|
|
815
|
+
Returns:
|
|
816
|
+
Set of line numbers (1-indexed) added on the HEAD side of the
|
|
817
|
+
comparison; empty on diff failure.
|
|
616
818
|
"""
|
|
617
819
|
diff_result = subprocess.run(
|
|
618
820
|
[
|
|
@@ -647,6 +849,18 @@ def added_lines_by_file(
|
|
|
647
849
|
base_reference: str,
|
|
648
850
|
all_file_paths: list[Path],
|
|
649
851
|
) -> dict[Path, set[int]]:
|
|
852
|
+
"""Build a per-file map of added line numbers across the branch.
|
|
853
|
+
|
|
854
|
+
Args:
|
|
855
|
+
repository_root: Repository root for diff invocations.
|
|
856
|
+
base_reference: The git reference to merge-base against.
|
|
857
|
+
all_file_paths: File paths whose added lines should be collected.
|
|
858
|
+
|
|
859
|
+
Returns:
|
|
860
|
+
Mapping from resolved file path to the set of line numbers added
|
|
861
|
+
on the HEAD side, with renames resolved to compare against the
|
|
862
|
+
original source path.
|
|
863
|
+
"""
|
|
650
864
|
merge_base = resolve_merge_base(repository_root, base_reference)
|
|
651
865
|
resolved_root = repository_root.resolve()
|
|
652
866
|
rename_source_map = renamed_file_source_map_since(resolved_root, merge_base)
|
|
@@ -680,6 +894,15 @@ def added_lines_by_file(
|
|
|
680
894
|
|
|
681
895
|
|
|
682
896
|
def extract_violation_line_number(violation_text: str) -> int | None:
|
|
897
|
+
"""Return the line number captured by the gate's violation-line regex.
|
|
898
|
+
|
|
899
|
+
Args:
|
|
900
|
+
violation_text: A single violation string of the form ``Line N: ...``.
|
|
901
|
+
|
|
902
|
+
Returns:
|
|
903
|
+
The integer line number captured in the prefix, or None when the
|
|
904
|
+
text does not match the violation-line pattern.
|
|
905
|
+
"""
|
|
683
906
|
match_result = violation_line_pattern().match(violation_text)
|
|
684
907
|
if match_result is None:
|
|
685
908
|
return None
|
|
@@ -690,6 +913,19 @@ def split_violations_by_scope(
|
|
|
690
913
|
all_issues: list[str],
|
|
691
914
|
all_added_line_numbers: set[int] | None,
|
|
692
915
|
) -> tuple[list[str], list[str]]:
|
|
916
|
+
"""Partition issues into blocking vs advisory based on touched lines.
|
|
917
|
+
|
|
918
|
+
Args:
|
|
919
|
+
all_issues: Violation strings emitted by the enforcer.
|
|
920
|
+
all_added_line_numbers: Lines added in the current diff, or None
|
|
921
|
+
to treat every violation as blocking.
|
|
922
|
+
|
|
923
|
+
Returns:
|
|
924
|
+
Tuple ``(blocking, advisory)``. When *all_added_line_numbers* is
|
|
925
|
+
None, every issue is blocking; otherwise issues whose ``Line N:``
|
|
926
|
+
prefix matches an added line are blocking and the rest are
|
|
927
|
+
advisory.
|
|
928
|
+
"""
|
|
693
929
|
if all_added_line_numbers is None:
|
|
694
930
|
return list(all_issues), []
|
|
695
931
|
blocking: list[str] = []
|
|
@@ -711,6 +947,14 @@ def print_violation_section(
|
|
|
711
947
|
violations_by_file: dict[Path, list[str]],
|
|
712
948
|
repository_root: Path,
|
|
713
949
|
) -> None:
|
|
950
|
+
"""Print a labeled block of violations grouped by relative path.
|
|
951
|
+
|
|
952
|
+
Args:
|
|
953
|
+
header_message: Section header to write to stderr.
|
|
954
|
+
violations_by_file: Mapping from absolute file path to the list of
|
|
955
|
+
violation strings to render under that path.
|
|
956
|
+
repository_root: Repository root used to compute relative paths.
|
|
957
|
+
"""
|
|
714
958
|
print(header_message, file=sys.stderr)
|
|
715
959
|
resolved_root = repository_root.resolve()
|
|
716
960
|
for each_path in sorted(violations_by_file.keys()):
|
|
@@ -723,6 +967,16 @@ def print_violation_section(
|
|
|
723
967
|
def read_prior_committed_content(
|
|
724
968
|
repository_root: Path, relative_path_posix: str
|
|
725
969
|
) -> str:
|
|
970
|
+
"""Return the HEAD-committed content for *relative_path_posix*.
|
|
971
|
+
|
|
972
|
+
Args:
|
|
973
|
+
repository_root: Repository root used as the ``git -C`` target.
|
|
974
|
+
relative_path_posix: Repository-relative POSIX path to read.
|
|
975
|
+
|
|
976
|
+
Returns:
|
|
977
|
+
The committed file content at HEAD, or an empty string when the
|
|
978
|
+
path is not tracked or ``git show`` returns non-zero.
|
|
979
|
+
"""
|
|
726
980
|
show_result = subprocess.run(
|
|
727
981
|
["git", "show", f"HEAD:{relative_path_posix}"],
|
|
728
982
|
cwd=str(repository_root),
|
|
@@ -743,6 +997,19 @@ def run_gate(
|
|
|
743
997
|
repository_root: Path,
|
|
744
998
|
all_added_lines_by_path: dict[Path, set[int]] | None = None,
|
|
745
999
|
) -> int:
|
|
1000
|
+
"""Run the gate over *all_file_paths* and emit a partitioned report.
|
|
1001
|
+
|
|
1002
|
+
Args:
|
|
1003
|
+
validate_content: The enforcer ``validate_content`` callable.
|
|
1004
|
+
all_file_paths: File paths to inspect.
|
|
1005
|
+
repository_root: Repository root used to resolve relative paths.
|
|
1006
|
+
all_added_lines_by_path: Optional per-file added-line maps used to
|
|
1007
|
+
partition issues into blocking vs advisory.
|
|
1008
|
+
|
|
1009
|
+
Returns:
|
|
1010
|
+
``1`` when at least one blocking violation is reported, ``0``
|
|
1011
|
+
otherwise.
|
|
1012
|
+
"""
|
|
746
1013
|
blocking_by_file: dict[Path, list[str]] = {}
|
|
747
1014
|
advisory_by_file: dict[Path, list[str]] = {}
|
|
748
1015
|
for each_path in sorted(set(all_file_paths)):
|
|
@@ -814,6 +1081,15 @@ def run_gate(
|
|
|
814
1081
|
|
|
815
1082
|
|
|
816
1083
|
def parse_arguments(all_arguments: list[str]) -> argparse.Namespace:
|
|
1084
|
+
"""Parse the command-line arguments for the code-rules gate.
|
|
1085
|
+
|
|
1086
|
+
Args:
|
|
1087
|
+
all_arguments: Command-line argument list forwarded to argparse.
|
|
1088
|
+
|
|
1089
|
+
Returns:
|
|
1090
|
+
The parsed argparse namespace with ``repo_root``, ``base``,
|
|
1091
|
+
``staged``, ``only_under``, and ``paths`` attributes.
|
|
1092
|
+
"""
|
|
817
1093
|
parser = argparse.ArgumentParser(
|
|
818
1094
|
description=(
|
|
819
1095
|
"Run CODE_RULES validators (validate_content) on files in the working tree. "
|
|
@@ -862,6 +1138,15 @@ def parse_arguments(all_arguments: list[str]) -> argparse.Namespace:
|
|
|
862
1138
|
|
|
863
1139
|
|
|
864
1140
|
def main(all_arguments: list[str]) -> int:
|
|
1141
|
+
"""Run the gate using the parsed CLI arguments.
|
|
1142
|
+
|
|
1143
|
+
Args:
|
|
1144
|
+
all_arguments: Command-line argument list forwarded to argparse.
|
|
1145
|
+
|
|
1146
|
+
Returns:
|
|
1147
|
+
The exit code from ``run_gate`` (``0`` clean, ``1`` blocking
|
|
1148
|
+
violations).
|
|
1149
|
+
"""
|
|
865
1150
|
arguments = parse_arguments(all_arguments)
|
|
866
1151
|
repository_root = (
|
|
867
1152
|
arguments.repo_root.resolve()
|
|
@@ -3,11 +3,11 @@ import subprocess
|
|
|
3
3
|
import sys
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
if
|
|
8
|
-
sys.path.insert(0,
|
|
6
|
+
parent_directory = str(Path(__file__).resolve().parent)
|
|
7
|
+
if parent_directory not in sys.path:
|
|
8
|
+
sys.path.insert(0, parent_directory)
|
|
9
9
|
|
|
10
|
-
from
|
|
10
|
+
from pr_loop_shared_constants.fix_hookspath_constants import ( # noqa: E402
|
|
11
11
|
ALL_CANONICAL_HOOKS_DIRECTORY_COMPONENTS,
|
|
12
12
|
ALL_GIT_GLOBAL_GET_CORE_HOOKS_PATH_COMMAND,
|
|
13
13
|
ALL_HOME_ENV_VAR_NAMES,
|
|
@@ -15,12 +15,22 @@ from config.fix_hookspath_constants import (
|
|
|
15
15
|
PREFLIGHT_NO_PYTEST_FLAG,
|
|
16
16
|
PREFLIGHT_REPO_ROOT_FLAG,
|
|
17
17
|
)
|
|
18
|
-
from
|
|
18
|
+
from pr_loop_shared_constants.preflight_constants import GIT_DIRECTORY_NAME # noqa: E402
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
def resolve_canonical_hooks_directory(
|
|
22
22
|
all_environment_overrides: dict[str, str] | None,
|
|
23
23
|
) -> Path:
|
|
24
|
+
"""Return the canonical claude-dev-env git hooks directory path.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
all_environment_overrides: Optional environment variable mapping used
|
|
28
|
+
to discover the user's home directory (HOME / USERPROFILE).
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
The absolute path to the canonical hooks directory beneath the
|
|
32
|
+
resolved home location.
|
|
33
|
+
"""
|
|
24
34
|
if all_environment_overrides is not None:
|
|
25
35
|
for each_env_var_name in ALL_HOME_ENV_VAR_NAMES:
|
|
26
36
|
home_value = all_environment_overrides.get(each_env_var_name)
|
|
@@ -33,6 +43,17 @@ def list_local_core_hooks_path_values(
|
|
|
33
43
|
repository_root: Path,
|
|
34
44
|
all_environment_overrides: dict[str, str] | None,
|
|
35
45
|
) -> list[str]:
|
|
46
|
+
"""Return all repo-local ``core.hooksPath`` values configured on the repo.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
repository_root: Repository root used as the ``git -C`` target.
|
|
50
|
+
all_environment_overrides: Optional environment variable mapping
|
|
51
|
+
forwarded to ``subprocess.run``.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
Non-empty stripped values from ``git config --local --get-all``, or
|
|
55
|
+
an empty list when no values are configured.
|
|
56
|
+
"""
|
|
36
57
|
git_command = [
|
|
37
58
|
"git",
|
|
38
59
|
"-C",
|
|
@@ -71,6 +92,16 @@ def list_local_core_hooks_path_values(
|
|
|
71
92
|
def read_global_core_hooks_path(
|
|
72
93
|
all_environment_overrides: dict[str, str] | None,
|
|
73
94
|
) -> str:
|
|
95
|
+
"""Return the global-scope ``core.hooksPath`` value from git config.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
all_environment_overrides: Optional environment variable mapping
|
|
99
|
+
forwarded to ``subprocess.run``.
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
The stripped global value, or an empty string when unset or when git
|
|
103
|
+
returns non-zero.
|
|
104
|
+
"""
|
|
74
105
|
git_command = list(ALL_GIT_GLOBAL_GET_CORE_HOOKS_PATH_COMMAND)
|
|
75
106
|
completed_process = subprocess.run(
|
|
76
107
|
git_command,
|
|
@@ -97,6 +128,16 @@ def unset_local_core_hooks_path(
|
|
|
97
128
|
repository_root: Path,
|
|
98
129
|
all_environment_overrides: dict[str, str] | None,
|
|
99
130
|
) -> int:
|
|
131
|
+
"""Remove every repo-local ``core.hooksPath`` entry from the repo config.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
repository_root: Repository root used as the ``git -C`` target.
|
|
135
|
+
all_environment_overrides: Optional environment variable mapping
|
|
136
|
+
forwarded to ``subprocess.run``.
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
The ``git config --unset-all`` exit code (zero on success).
|
|
140
|
+
"""
|
|
100
141
|
git_command = [
|
|
101
142
|
"git",
|
|
102
143
|
"-C",
|
|
@@ -120,6 +161,16 @@ def set_global_core_hooks_path(
|
|
|
120
161
|
target_value: str,
|
|
121
162
|
all_environment_overrides: dict[str, str] | None,
|
|
122
163
|
) -> int:
|
|
164
|
+
"""Write the global-scope ``core.hooksPath`` value into git config.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
target_value: Path value to install at global scope.
|
|
168
|
+
all_environment_overrides: Optional environment variable mapping
|
|
169
|
+
forwarded to ``subprocess.run``.
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
The ``git config --global`` exit code (zero on success).
|
|
173
|
+
"""
|
|
123
174
|
git_command = ["git", "config", "--global", "core.hooksPath", target_value]
|
|
124
175
|
completed_process = subprocess.run(
|
|
125
176
|
git_command,
|
|
@@ -142,6 +193,15 @@ def is_canonical_hooks_path(raw_value: str) -> bool:
|
|
|
142
193
|
|
|
143
194
|
|
|
144
195
|
def find_repository_root(start: Path) -> Path:
|
|
196
|
+
"""Walk up from *start* to the nearest directory containing a git marker.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
start: The directory to start the upward search from.
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
The resolved ancestor that contains a ``.git`` directory or file, or
|
|
203
|
+
the resolved *start* path when no git marker is found.
|
|
204
|
+
"""
|
|
145
205
|
resolved_start = start.resolve()
|
|
146
206
|
candidate_paths = [resolved_start, *resolved_start.parents]
|
|
147
207
|
for each_candidate in candidate_paths:
|
|
@@ -155,6 +215,17 @@ def rerun_preflight(
|
|
|
155
215
|
repository_root: Path,
|
|
156
216
|
all_environment_overrides: dict[str, str] | None,
|
|
157
217
|
) -> int:
|
|
218
|
+
"""Re-invoke ``preflight.py`` after the hooks path has been repaired.
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
repository_root: Repository root passed through to preflight as
|
|
222
|
+
``--repo-root``.
|
|
223
|
+
all_environment_overrides: Optional environment variable mapping
|
|
224
|
+
forwarded to ``subprocess.run``.
|
|
225
|
+
|
|
226
|
+
Returns:
|
|
227
|
+
The preflight subprocess exit code.
|
|
228
|
+
"""
|
|
158
229
|
preflight_script_path = Path(__file__).resolve().parent / "preflight.py"
|
|
159
230
|
rerun_command = [
|
|
160
231
|
sys.executable,
|
|
@@ -172,6 +243,15 @@ def rerun_preflight(
|
|
|
172
243
|
|
|
173
244
|
|
|
174
245
|
def parse_arguments(all_arguments: list[str] | None) -> argparse.Namespace:
|
|
246
|
+
"""Parse the command-line arguments for the fix_hookspath script.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
all_arguments: Command-line argument list, or None to read from
|
|
250
|
+
``sys.argv``.
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
The parsed argparse namespace with a ``repo_root`` attribute.
|
|
254
|
+
"""
|
|
175
255
|
parser = argparse.ArgumentParser(
|
|
176
256
|
description=(
|
|
177
257
|
"Auto-fix core.hooksPath when bugteam preflight detects a stale override. "
|
|
@@ -192,6 +272,17 @@ def main(
|
|
|
192
272
|
all_arguments: list[str],
|
|
193
273
|
all_environment_overrides: dict[str, str] | None,
|
|
194
274
|
) -> int:
|
|
275
|
+
"""Run the fix_hookspath repair routine and re-invoke preflight.
|
|
276
|
+
|
|
277
|
+
Args:
|
|
278
|
+
all_arguments: Command-line argument list forwarded to argparse.
|
|
279
|
+
all_environment_overrides: Optional environment variable mapping
|
|
280
|
+
forwarded to every git invocation and to the preflight rerun.
|
|
281
|
+
|
|
282
|
+
Returns:
|
|
283
|
+
Zero on success. Non-zero on the first failing git command or on a
|
|
284
|
+
non-zero preflight rerun exit code.
|
|
285
|
+
"""
|
|
195
286
|
arguments = parse_arguments(all_arguments)
|
|
196
287
|
start_directory = Path.cwd()
|
|
197
288
|
repository_root = (
|