py-wlcli 0.8.0__py3-none-any.whl
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.
- py_wlcli-0.8.0.dist-info/METADATA +139 -0
- py_wlcli-0.8.0.dist-info/RECORD +320 -0
- py_wlcli-0.8.0.dist-info/WHEEL +4 -0
- py_wlcli-0.8.0.dist-info/entry_points.txt +2 -0
- py_wlcli-0.8.0.dist-info/licenses/LICENSE +21 -0
- wl_cli/__init__.py +27 -0
- wl_cli/__main__.py +130 -0
- wl_cli/capabilities.py +79 -0
- wl_cli/cc/__init__.py +5 -0
- wl_cli/cc/checks.py +1019 -0
- wl_cli/cc/cli.py +370 -0
- wl_cli/cc/mutate.py +1350 -0
- wl_cli/cli_click.py +156 -0
- wl_cli/commands/__init__.py +84 -0
- wl_cli/commands/base.py +80 -0
- wl_cli/commands/buildcommands/__init__.py +1 -0
- wl_cli/commands/buildcommands/build.py +71 -0
- wl_cli/commands/buildcommands/build_utils.py +47 -0
- wl_cli/commands/buildcommands/builders/__init__.py +18 -0
- wl_cli/commands/buildcommands/builders/cleanup.py +35 -0
- wl_cli/commands/buildcommands/builders/coordinator.py +122 -0
- wl_cli/commands/buildcommands/builders/environment.py +81 -0
- wl_cli/commands/buildcommands/builders/rust_builder.py +39 -0
- wl_cli/commands/buildcommands/builders/stubs.py +58 -0
- wl_cli/commands/buildcommands/dist.py +326 -0
- wl_cli/commands/buildcommands/test.py +89 -0
- wl_cli/commands/clean/__init__.py +1 -0
- wl_cli/commands/clean/build.py +151 -0
- wl_cli/commands/clean/clean.py +294 -0
- wl_cli/commands/clean/clean_utils.py +77 -0
- wl_cli/commands/clean/core.py +236 -0
- wl_cli/commands/clean/dead_code/__init__.py +0 -0
- wl_cli/commands/clean/dead_code/dead_code_command.py +70 -0
- wl_cli/commands/clean/dead_code/detector.py +161 -0
- wl_cli/commands/clean/dead_code/fixer.py +176 -0
- wl_cli/commands/clean/dead_code/reporter.py +295 -0
- wl_cli/commands/clean/dead_code/utils.py +0 -0
- wl_cli/commands/clean/lfs.py +215 -0
- wl_cli/commands/clean/python.py +81 -0
- wl_cli/commands/clean/rust.py +91 -0
- wl_cli/commands/clean/utils/__init__.py +34 -0
- wl_cli/commands/clean/venv.py +91 -0
- wl_cli/commands/cleanup.py +73 -0
- wl_cli/commands/click_base.py +79 -0
- wl_cli/commands/config/__init__.py +1 -0
- wl_cli/commands/config/config.py +291 -0
- wl_cli/commands/debug/__init__.py +6 -0
- wl_cli/commands/debug/debug.py +309 -0
- wl_cli/commands/debug/debug_cache.py +86 -0
- wl_cli/commands/debug/debug_subagent.py +156 -0
- wl_cli/commands/debug/output_formatter.py +165 -0
- wl_cli/commands/debug/source_hasher.py +92 -0
- wl_cli/commands/format/__init__.py +13 -0
- wl_cli/commands/format/format_cache_adapter.py +125 -0
- wl_cli/commands/format/format_command.py +197 -0
- wl_cli/commands/format/format_coordinator.py +191 -0
- wl_cli/commands/format/format_executor.py +470 -0
- wl_cli/commands/format/format_outcome.py +44 -0
- wl_cli/commands/format/format_path_handler.py +1016 -0
- wl_cli/commands/format/format_progress.py +62 -0
- wl_cli/commands/format/format_reporter.py +31 -0
- wl_cli/commands/format/python_formatter.py +506 -0
- wl_cli/commands/format/rust_formatter.py +106 -0
- wl_cli/commands/gate/__init__.py +1 -0
- wl_cli/commands/gate/gate_command.py +862 -0
- wl_cli/commands/gate/preflight.py +445 -0
- wl_cli/commands/git/__init__.py +1 -0
- wl_cli/commands/git/config.py +169 -0
- wl_cli/commands/git/git_facts.py +60 -0
- wl_cli/commands/git/git_flow.py +86 -0
- wl_cli/commands/git/hook.py +112 -0
- wl_cli/commands/git/policy.py +216 -0
- wl_cli/commands/git/push_update.py +82 -0
- wl_cli/commands/initenv/__init__.py +1 -0
- wl_cli/commands/initenv/common/__init__.py +23 -0
- wl_cli/commands/initenv/common/check_git_bash.py +78 -0
- wl_cli/commands/initenv/common/error_handler.py +68 -0
- wl_cli/commands/initenv/common/exceptions.py +52 -0
- wl_cli/commands/initenv/components/__init__.py +4 -0
- wl_cli/commands/initenv/components/cargo_sync.py +240 -0
- wl_cli/commands/initenv/components/config_manager.py +106 -0
- wl_cli/commands/initenv/components/directory_utils.py +71 -0
- wl_cli/commands/initenv/components/git/__init__.py +9 -0
- wl_cli/commands/initenv/components/git/branch_creator.py +143 -0
- wl_cli/commands/initenv/components/git/git_flow_interface.py +24 -0
- wl_cli/commands/initenv/components/git/git_flow_manager.py +159 -0
- wl_cli/commands/initenv/components/git/git_lfs_manager.py +272 -0
- wl_cli/commands/initenv/components/git/git_utility.py +254 -0
- wl_cli/commands/initenv/components/git/gitignore_manager.py +53 -0
- wl_cli/commands/initenv/components/git_initializer.py +212 -0
- wl_cli/commands/initenv/components/i18n_manager.py +104 -0
- wl_cli/commands/initenv/components/log_manager.py +241 -0
- wl_cli/commands/initenv/components/project_structure/__init__.py +23 -0
- wl_cli/commands/initenv/components/project_structure/config_generator.py +360 -0
- wl_cli/commands/initenv/components/project_structure/directory_creator.py +184 -0
- wl_cli/commands/initenv/components/project_structure/hook_source.py +110 -0
- wl_cli/commands/initenv/components/project_structure/hooks_manager.py +995 -0
- wl_cli/commands/initenv/components/project_structure/readme_handler.py +56 -0
- wl_cli/commands/initenv/components/project_structure/rust_files_handler.py +30 -0
- wl_cli/commands/initenv/components/project_structure/setup_handler.py +103 -0
- wl_cli/commands/initenv/components/project_structure/sop_generator.py +73 -0
- wl_cli/commands/initenv/components/project_structure/template_copier.py +104 -0
- wl_cli/commands/initenv/components/project_structure/template_manager.py +195 -0
- wl_cli/commands/initenv/components/pyproject_generator.py +194 -0
- wl_cli/commands/initenv/components/rust_initializer.py +145 -0
- wl_cli/commands/initenv/components/symlink_cleaner.py +99 -0
- wl_cli/commands/initenv/components/symlink_manager.py +52 -0
- wl_cli/commands/initenv/components/venv_manager.py +118 -0
- wl_cli/commands/initenv/initenv.py +169 -0
- wl_cli/commands/initenv/services/__init__.py +11 -0
- wl_cli/commands/initenv/services/environment_initializer.py +288 -0
- wl_cli/commands/initenv/services/git_lfs_initializer.py +26 -0
- wl_cli/commands/initenv/services/project_structure_initializer.py +52 -0
- wl_cli/commands/lfs/README.md +221 -0
- wl_cli/commands/lfs/__init__.py +5 -0
- wl_cli/commands/lfs/cli.py +106 -0
- wl_cli/commands/lfs/config.py +164 -0
- wl_cli/commands/lfs/hook.py +61 -0
- wl_cli/commands/lfs/initializer.py +531 -0
- wl_cli/commands/lfs/manager.py +95 -0
- wl_cli/commands/lfs/policy.py +165 -0
- wl_cli/commands/lfs/watcher.py +294 -0
- wl_cli/commands/lint/__init__.py +1 -0
- wl_cli/commands/lint/lint_command.py +297 -0
- wl_cli/commands/lint/lint_executor.py +409 -0
- wl_cli/commands/lint/lint_reporter.py +579 -0
- wl_cli/commands/lint/lint_run.py +258 -0
- wl_cli/commands/lint/mypy_config.py +90 -0
- wl_cli/commands/registry.py +103 -0
- wl_cli/commands/self/__init__.py +7 -0
- wl_cli/commands/self/common/__init__.py +8 -0
- wl_cli/commands/self/common/capabilities.py +30 -0
- wl_cli/commands/self/common/config.py +27 -0
- wl_cli/commands/self/common/version_cache.py +85 -0
- wl_cli/commands/self/handlers/__init__.py +19 -0
- wl_cli/commands/self/handlers/base_handler.py +28 -0
- wl_cli/commands/self/handlers/install_handler.py +85 -0
- wl_cli/commands/self/handlers/status_handler.py +66 -0
- wl_cli/commands/self/handlers/uninstall_handler.py +58 -0
- wl_cli/commands/self/handlers/update_handler.py +102 -0
- wl_cli/commands/self/handlers/version_handler.py +16 -0
- wl_cli/commands/self/self_command.py +237 -0
- wl_cli/commands/self/services/__init__.py +15 -0
- wl_cli/commands/self/services/installation_manager.py +187 -0
- wl_cli/commands/self/services/local_installer.py +165 -0
- wl_cli/commands/self/services/pypi_updater.py +278 -0
- wl_cli/commands/self/services/windows_update.py +174 -0
- wl_cli/commands/testcommands/__init__.py +8 -0
- wl_cli/commands/testcommands/base_dependency_analyzer.py +198 -0
- wl_cli/commands/testcommands/batch_processor.py +278 -0
- wl_cli/commands/testcommands/cache_manager.py +1156 -0
- wl_cli/commands/testcommands/coverage_analyzer.py +955 -0
- wl_cli/commands/testcommands/coverage_cache_manager.py +183 -0
- wl_cli/commands/testcommands/progress_manager.py +79 -0
- wl_cli/commands/testcommands/python_env_detector.py +207 -0
- wl_cli/commands/testcommands/report_generator.py +101 -0
- wl_cli/commands/testcommands/single_test_executor.py +250 -0
- wl_cli/commands/testcommands/speed_checker.py +118 -0
- wl_cli/commands/testcommands/test_command_builder.py +165 -0
- wl_cli/commands/testcommands/test_commands.py +786 -0
- wl_cli/commands/testcommands/test_reporter.py +181 -0
- wl_cli/commands/testcommands/test_result_handler.py +96 -0
- wl_cli/commands/testcommands/test_runner.py +1343 -0
- wl_cli/commands/testcommands/test_utils.py +197 -0
- wl_cli/commands/testcommands/utils/__init__.py +17 -0
- wl_cli/commands/testcommands/utils/coverage_parser.py +98 -0
- wl_cli/commands/testcommands/utils/env_info.py +36 -0
- wl_cli/commands/testcommands/utils/junit_parser.py +75 -0
- wl_cli/commands/testcommands/utils/markdown_utils.py +20 -0
- wl_cli/commands/testcommands/utils/project_root.py +61 -0
- wl_cli/commands/update/__init__.py +9 -0
- wl_cli/commands/update/services/__init__.py +21 -0
- wl_cli/commands/update/services/backup_manager.py +207 -0
- wl_cli/commands/update/services/component_manager.py +83 -0
- wl_cli/commands/update/services/conflict_resolver.py +189 -0
- wl_cli/commands/update/services/vendors_updater.py +85 -0
- wl_cli/commands/update/services/version_tracker.py +127 -0
- wl_cli/commands/update/update_command.py +270 -0
- wl_cli/commands/update/utils/__init__.py +3 -0
- wl_cli/commands/version/__init__.py +9 -0
- wl_cli/commands/version/version_command.py +155 -0
- wl_cli/commands/workflow.py +477 -0
- wl_cli/env/__init__.py +5 -0
- wl_cli/env/cli.py +167 -0
- wl_cli/env_status.py +157 -0
- wl_cli/envelope.py +91 -0
- wl_cli/exceptions.py +85 -0
- wl_cli/lib/__init__.py +3 -0
- wl_cli/lib/rust_utils.py +35 -0
- wl_cli/mcp/__init__.py +6 -0
- wl_cli/mcp/capabilities.py +19 -0
- wl_cli/mcp/cli.py +22 -0
- wl_cli/mcp/gitlab.py +553 -0
- wl_cli/mcp/k8s.py +344 -0
- wl_cli/mcp/protocol.py +159 -0
- wl_cli/platform.py +139 -0
- wl_cli/quality/__init__.py +18 -0
- wl_cli/utils/__init__.py +7 -0
- wl_cli/utils/cache.py +491 -0
- wl_cli/utils/cache_ledger.py +355 -0
- wl_cli/utils/cache_store.py +546 -0
- wl_cli/utils/config/__init__.py +27 -0
- wl_cli/utils/config/config_loader.py +163 -0
- wl_cli/utils/config/core.py +862 -0
- wl_cli/utils/config/defaults.py +142 -0
- wl_cli/utils/config/env_exporter.py +86 -0
- wl_cli/utils/config/env_parser.py +108 -0
- wl_cli/utils/config/validation.py +280 -0
- wl_cli/utils/error_handler.py +165 -0
- wl_cli/utils/file_copier.py +56 -0
- wl_cli/utils/file_operations.py +506 -0
- wl_cli/utils/language_utils.py +122 -0
- wl_cli/utils/lazy_import.py +240 -0
- wl_cli/utils/log_levels.py +35 -0
- wl_cli/utils/log_rotators.py +74 -0
- wl_cli/utils/logging.py +458 -0
- wl_cli/utils/output_context.py +142 -0
- wl_cli/utils/performance/__init__.py +1 -0
- wl_cli/utils/performance/decorators.py +111 -0
- wl_cli/utils/performance/exporter.py +38 -0
- wl_cli/utils/performance/models.py +16 -0
- wl_cli/utils/performance/monitor.py +161 -0
- wl_cli/utils/performance_monitor.py +32 -0
- wl_cli/utils/platform_adapter.py +131 -0
- wl_cli/utils/pre_commit.py +266 -0
- wl_cli/utils/pre_commit_config.py +72 -0
- wl_cli/utils/root.py +521 -0
- wl_cli/utils/root_fallback.py +256 -0
- wl_cli/utils/run_evidence.py +236 -0
- wl_cli/utils/structured_logger.py +18 -0
- wl_cli/utils/structured_logging/__init__.py +19 -0
- wl_cli/utils/structured_logging/config.py +337 -0
- wl_cli/utils/structured_logging/core.py +177 -0
- wl_cli/utils/structured_logging/env.py +56 -0
- wl_cli/utils/structured_logging/filtering.py +75 -0
- wl_cli/utils/structured_logging/filters.py +55 -0
- wl_cli/utils/structured_logging/formatters.py +251 -0
- wl_cli/utils/structured_logging/handlers.py +308 -0
- wl_cli/utils/structured_logging/logger.py +354 -0
- wl_cli/utils/subprocess.py +71 -0
- wl_cli/utils/subprocess_utils.py +260 -0
- wl_cli/utils/test_utils/__init__.py +5 -0
- wl_cli/utils/test_utils/test_filter.py +36 -0
- wl_cli/utils/uv_run.py +79 -0
- wl_cli/utils/uv_tool.py +117 -0
- wl_cli/utils/uv_tree.py +71 -0
- wl_cli/utils/uv_workspace.py +174 -0
- wl_cli/utils/version/__init__.py +15 -0
- wl_cli/utils/version/pypi_version_checker.py +95 -0
- wl_cli/utils/version/version_comparator.py +133 -0
- wl_cli/utils/version/version_detectors.py +312 -0
- wl_cli/utils/version/version_service.py +131 -0
- wl_cli/utils/version/version_updater.py +198 -0
- wl_cli/utils/wl_dir_updater.py +1440 -0
- wl_cli/utils/workspace_detector/__init__.py +28 -0
- wl_cli/utils/workspace_detector/core.py +161 -0
- wl_cli/utils/workspace_detector/detection_strategies.py +107 -0
- wl_cli/utils/workspace_detector/exceptions.py +5 -0
- wl_cli/utils/workspace_detector/types.py +17 -0
- wl_cli/utils/workspace_detector/venv_resolver.py +84 -0
- wl_cli/vendors/config/.codespellrc +2 -0
- wl_cli/vendors/config/dependencies.toml +10 -0
- wl_cli/vendors/config/mypy.ini +62 -0
- wl_cli/vendors/config/pyproject_defaults.toml +28 -0
- wl_cli/vendors/config/template_config.toml +24 -0
- wl_cli/vendors/config/tool_configs.toml +76 -0
- wl_cli/vendors/git/.gitignore +122 -0
- wl_cli/vendors/git/.pre-commit-config.yaml +37 -0
- wl_cli/vendors/hooks/applypatch-msg.sample +15 -0
- wl_cli/vendors/hooks/commit-msg.sample +24 -0
- wl_cli/vendors/hooks/fsmonitor-watchman.sample +174 -0
- wl_cli/vendors/hooks/post-update.sample +8 -0
- wl_cli/vendors/hooks/pre-applypatch.sample +14 -0
- wl_cli/vendors/hooks/pre-commit +45 -0
- wl_cli/vendors/hooks/pre-commit.legacy +31 -0
- wl_cli/vendors/hooks/pre-commit.sample +49 -0
- wl_cli/vendors/hooks/pre-merge-commit.sample +13 -0
- wl_cli/vendors/hooks/pre-push.sample +60 -0
- wl_cli/vendors/hooks/pre-rebase.sample +169 -0
- wl_cli/vendors/hooks/pre-receive.sample +24 -0
- wl_cli/vendors/hooks/prepare-commit-msg.sample +42 -0
- wl_cli/vendors/hooks/push-to-checkout.sample +78 -0
- wl_cli/vendors/hooks/sendemail-validate.sample +77 -0
- wl_cli/vendors/hooks/unix/applypatch-msg.sample +15 -0
- wl_cli/vendors/hooks/unix/check_lockfile.py +58 -0
- wl_cli/vendors/hooks/unix/check_lockfile.sh +36 -0
- wl_cli/vendors/hooks/unix/commit-msg.sample +24 -0
- wl_cli/vendors/hooks/unix/fsmonitor-watchman.sample +174 -0
- wl_cli/vendors/hooks/unix/post-update.sample +8 -0
- wl_cli/vendors/hooks/unix/pre-applypatch.sample +14 -0
- wl_cli/vendors/hooks/unix/pre-commit +190 -0
- wl_cli/vendors/hooks/unix/pre-commit.legacy +31 -0
- wl_cli/vendors/hooks/unix/pre-commit.py +81 -0
- wl_cli/vendors/hooks/unix/pre-commit.sample +49 -0
- wl_cli/vendors/hooks/unix/pre-merge-commit.sample +13 -0
- wl_cli/vendors/hooks/unix/pre-push +417 -0
- wl_cli/vendors/hooks/unix/pre-push.sample +53 -0
- wl_cli/vendors/hooks/unix/pre-rebase.sample +169 -0
- wl_cli/vendors/hooks/unix/pre-receive.sample +24 -0
- wl_cli/vendors/hooks/unix/prepare-commit-msg.sample +42 -0
- wl_cli/vendors/hooks/update.sample +128 -0
- wl_cli/vendors/hooks/win/work.bat +63 -0
- wl_cli/vendors/project/.gitignore +90 -0
- wl_cli/vendors/project/.python-version +1 -0
- wl_cli/vendors/project/LICENSE +21 -0
- wl_cli/vendors/project/README.md +0 -0
- wl_cli/vendors/project/pyproject.toml +27 -0
- wl_cli/vendors/project/pyproject_no_workspace.toml +16 -0
- wl_cli/vendors/project/pyproject_workspace.toml +14 -0
- wl_cli/vendors/readme/README.md +120 -0
- wl_cli/vendors/rust/Cargo.toml +14 -0
- wl_cli/vendors/rust/lib/__init__.py +3 -0
- wl_cli/vendors/rust/lib/rust_utils.py +31 -0
- wl_cli/vendors/rust/src/lib.rs +13 -0
- wl_cli/vendors/rust/src/main.rs +3 -0
- wl_cli/vendors/rust/tests/test_rust_fallback.py +40 -0
- wl_cli/vendors/sop_doc/git-dev-sop.md +220 -0
- wl_cli/vendors/sop_doc/industrial-standard-sop.md +145 -0
- wl_cli/vendors/sop_doc/python-sop.md +342 -0
- wl_cli/vendors/sop_doc/rust-readme.md +219 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Git Flow CLI commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from .config import GitFlowConfig
|
|
8
|
+
from .git_facts import get_changed_files, get_current_branch
|
|
9
|
+
from .hook import format_agent_hint
|
|
10
|
+
from .policy import GitFlowPolicyValidator, PushContext
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def run_git_flow_check_text() -> str:
|
|
14
|
+
"""Run the Git Flow check core and return the exact banner text.
|
|
15
|
+
|
|
16
|
+
Returns the multi-line banner text that ``wl git-flow check`` prints: the
|
|
17
|
+
``Git-Flow-Agent-Hint:`` block followed by the status lines. This is the
|
|
18
|
+
pure-hint entry point used by the pre-commit hook: it never raises, so the
|
|
19
|
+
caller can display the banner without gating on the result.
|
|
20
|
+
"""
|
|
21
|
+
text, _ok = _run_git_flow_check()
|
|
22
|
+
return text
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@click.group(name="git-flow")
|
|
26
|
+
def git_flow_command() -> None:
|
|
27
|
+
"""Check Git Flow branch policy."""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@git_flow_command.command(name="check")
|
|
31
|
+
def check_command() -> None:
|
|
32
|
+
"""Check the current branch against Git Flow policy."""
|
|
33
|
+
text, ok = _run_git_flow_check()
|
|
34
|
+
click.echo(text)
|
|
35
|
+
if not ok:
|
|
36
|
+
raise click.ClickException("Git Flow policy check failed")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _run_git_flow_check() -> tuple[str, bool]:
|
|
40
|
+
"""Shared core: run the policy and return (banner_text, ok)."""
|
|
41
|
+
config = GitFlowConfig()
|
|
42
|
+
validator = GitFlowPolicyValidator(config)
|
|
43
|
+
branch = get_current_branch()
|
|
44
|
+
branch_type = validator.classify_branch(branch)
|
|
45
|
+
source = _source_for_branch_type(config, branch_type)
|
|
46
|
+
changed_files = (
|
|
47
|
+
get_changed_files(source, file_filters=["VERSION", "CHANGELOG.md"])
|
|
48
|
+
if source
|
|
49
|
+
else frozenset()
|
|
50
|
+
)
|
|
51
|
+
result = validator.validate_push(
|
|
52
|
+
PushContext(local_branch=branch, changed_files=changed_files)
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
lines = [
|
|
56
|
+
format_agent_hint(result),
|
|
57
|
+
f"Git Flow check: {result.status}",
|
|
58
|
+
f"Branch: {result.branch}",
|
|
59
|
+
f"Type: {branch_type}",
|
|
60
|
+
]
|
|
61
|
+
if source:
|
|
62
|
+
lines.append(f"Source: {source}")
|
|
63
|
+
targets = _targets_for_branch_type(config, branch_type)
|
|
64
|
+
if targets:
|
|
65
|
+
lines.append(f"Target: {', '.join(targets)}")
|
|
66
|
+
lines.extend(
|
|
67
|
+
[
|
|
68
|
+
f"Rule: {result.rule}",
|
|
69
|
+
f"Fix: {result.fix}",
|
|
70
|
+
f"Config: {result.config_source}",
|
|
71
|
+
]
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
return "\n".join(lines), result.ok
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _source_for_branch_type(config: GitFlowConfig, branch_type: str) -> str | None:
|
|
78
|
+
if branch_type in {"feature", "release", "hotfix"}:
|
|
79
|
+
return config.source_branch(branch_type)
|
|
80
|
+
return None
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _targets_for_branch_type(config: GitFlowConfig, branch_type: str) -> list[str]:
|
|
84
|
+
if branch_type in {"feature", "release", "hotfix"}:
|
|
85
|
+
return config.merge_targets(branch_type)
|
|
86
|
+
return []
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""Pre-push hook helpers for Git Flow policy validation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
from .policy import PolicyResult, PushContext
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from .push_update import PushUpdate
|
|
13
|
+
|
|
14
|
+
BRANCH_NAME_PATTERN = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._/-]*$")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def build_push_context(
|
|
18
|
+
local_branch: str,
|
|
19
|
+
updates: tuple[PushUpdate, ...],
|
|
20
|
+
changed_files: frozenset[str],
|
|
21
|
+
) -> PushContext:
|
|
22
|
+
"""Build the policy context from pre-push updates parsed once by the caller."""
|
|
23
|
+
return PushContext(
|
|
24
|
+
local_branch=local_branch,
|
|
25
|
+
remote_branch=_remote_branch_from_updates(updates),
|
|
26
|
+
changed_files=changed_files,
|
|
27
|
+
bypass_token=os.environ.get("WL_GIT_FLOW_BYPASS"),
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def format_agent_hint(result: PolicyResult) -> str:
|
|
32
|
+
hint = result.agent_hint
|
|
33
|
+
lines = ["Git-Flow-Agent-Hint:"]
|
|
34
|
+
for key in (
|
|
35
|
+
"branch",
|
|
36
|
+
"branch_type",
|
|
37
|
+
"status",
|
|
38
|
+
"recommended_action",
|
|
39
|
+
"source_branch",
|
|
40
|
+
"merge_targets",
|
|
41
|
+
"allowed_push",
|
|
42
|
+
"direct_push_allowed",
|
|
43
|
+
"protected_target_push_allowed",
|
|
44
|
+
"required_changes",
|
|
45
|
+
"missing_changes",
|
|
46
|
+
"rename_required",
|
|
47
|
+
"suggested_branch_patterns",
|
|
48
|
+
"next_step",
|
|
49
|
+
"agent_policy",
|
|
50
|
+
):
|
|
51
|
+
lines.extend(_format_yaml_field(key, getattr(hint, key)))
|
|
52
|
+
return "\n".join(lines)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def format_hook_failure(result: PolicyResult) -> str:
|
|
56
|
+
return "\n".join(
|
|
57
|
+
[
|
|
58
|
+
f"Git Flow check: {result.status}",
|
|
59
|
+
f"Branch: {result.branch}",
|
|
60
|
+
f"Rule: {result.rule}",
|
|
61
|
+
f"Fix: {result.fix}",
|
|
62
|
+
f"Config: {result.config_source}",
|
|
63
|
+
"Run `wl git-flow check` for details.",
|
|
64
|
+
]
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _format_yaml_field(key: str, value: object) -> list[str]:
|
|
69
|
+
if isinstance(value, bool):
|
|
70
|
+
return [f" {key}: {str(value).lower()}"]
|
|
71
|
+
if isinstance(value, str):
|
|
72
|
+
return [f" {key}: {_quote_yaml_string(value)}"]
|
|
73
|
+
if isinstance(value, tuple):
|
|
74
|
+
if not value:
|
|
75
|
+
return [f" {key}: []"]
|
|
76
|
+
return [f" {key}:"] + [f" - {_quote_yaml_string(item)}" for item in value]
|
|
77
|
+
raise TypeError(f"unsupported agent hint value for {key}: {type(value).__name__}")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _quote_yaml_string(value: str) -> str:
|
|
81
|
+
escaped = (
|
|
82
|
+
value.replace("\\", "\\\\")
|
|
83
|
+
.replace('"', '\\"')
|
|
84
|
+
.replace("\n", "\\n")
|
|
85
|
+
.replace("\r", "\\r")
|
|
86
|
+
.replace("\t", "\\t")
|
|
87
|
+
)
|
|
88
|
+
return '"' + escaped + '"'
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _remote_branch_from_updates(updates: tuple[PushUpdate, ...]) -> str | None:
|
|
92
|
+
"""First safe branch target among the updates, else ``None``.
|
|
93
|
+
|
|
94
|
+
An unsafe name does not end the scan: a push may carry several heads, and
|
|
95
|
+
skipping the bad line keeps the check on the remaining ones (#163, keeping
|
|
96
|
+
the pre-split behaviour).
|
|
97
|
+
"""
|
|
98
|
+
for update in updates:
|
|
99
|
+
branch_name = update.branch_name
|
|
100
|
+
if branch_name is None:
|
|
101
|
+
continue
|
|
102
|
+
if _is_safe_branch_name(branch_name):
|
|
103
|
+
return branch_name
|
|
104
|
+
return None
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _is_safe_branch_name(branch_name: str) -> bool:
|
|
108
|
+
return (
|
|
109
|
+
bool(BRANCH_NAME_PATTERN.fullmatch(branch_name))
|
|
110
|
+
and ".." not in branch_name
|
|
111
|
+
and not branch_name.endswith("/")
|
|
112
|
+
)
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"""Pure Git Flow policy validation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hmac
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from typing import TYPE_CHECKING, Literal
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from .config import GitFlowConfig
|
|
11
|
+
|
|
12
|
+
PolicyStatus = Literal["OK", "WARN", "BLOCKED"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class PushContext:
|
|
17
|
+
local_branch: str
|
|
18
|
+
remote_branch: str | None = None
|
|
19
|
+
changed_files: frozenset[str] = field(default_factory=frozenset)
|
|
20
|
+
bypass_token: str | None = None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True)
|
|
24
|
+
class AgentHint:
|
|
25
|
+
branch: str
|
|
26
|
+
branch_type: str
|
|
27
|
+
status: PolicyStatus
|
|
28
|
+
recommended_action: str
|
|
29
|
+
source_branch: str
|
|
30
|
+
merge_targets: tuple[str, ...]
|
|
31
|
+
allowed_push: bool
|
|
32
|
+
direct_push_allowed: bool
|
|
33
|
+
protected_target_push_allowed: bool
|
|
34
|
+
required_changes: tuple[str, ...]
|
|
35
|
+
missing_changes: tuple[str, ...]
|
|
36
|
+
rename_required: bool
|
|
37
|
+
suggested_branch_patterns: tuple[str, ...]
|
|
38
|
+
next_step: str
|
|
39
|
+
agent_policy: str
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(frozen=True)
|
|
43
|
+
class PolicyResult:
|
|
44
|
+
ok: bool
|
|
45
|
+
status: PolicyStatus
|
|
46
|
+
branch: str
|
|
47
|
+
rule: str
|
|
48
|
+
fix: str
|
|
49
|
+
config_source: str
|
|
50
|
+
agent_hint: AgentHint
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class GitFlowPolicyValidator:
|
|
54
|
+
BYPASS_TOKEN = "release-automation"
|
|
55
|
+
|
|
56
|
+
def __init__(self, config: GitFlowConfig) -> None:
|
|
57
|
+
self.config = config
|
|
58
|
+
|
|
59
|
+
def classify_branch(self, branch_name: str) -> str:
|
|
60
|
+
if branch_name == self.config.main_branch:
|
|
61
|
+
return "main"
|
|
62
|
+
if branch_name == self.config.develop_branch:
|
|
63
|
+
return "develop"
|
|
64
|
+
for branch_type in ("feature", "release", "hotfix"):
|
|
65
|
+
if branch_name.startswith(self.config.branch_prefix(branch_type)):
|
|
66
|
+
return branch_type
|
|
67
|
+
return "unknown"
|
|
68
|
+
|
|
69
|
+
def validate_push(self, context: PushContext) -> PolicyResult:
|
|
70
|
+
if not context.local_branch:
|
|
71
|
+
return self._blocked(
|
|
72
|
+
"unknown",
|
|
73
|
+
"could not determine the current branch for Git Flow validation.",
|
|
74
|
+
"run `git branch --show-current`, then rerun wl git-flow check.",
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
target_branch = context.remote_branch or context.local_branch
|
|
78
|
+
target_type = self.classify_branch(target_branch)
|
|
79
|
+
|
|
80
|
+
if target_type in {"main", "develop"} and not self._has_bypass(context):
|
|
81
|
+
return self._blocked(
|
|
82
|
+
target_branch,
|
|
83
|
+
f"direct human pushes to protected branch {target_branch} are not allowed.",
|
|
84
|
+
f"push a feature/*, release/*, or hotfix/* branch and open a PR into {target_branch}.",
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
local_type = self.classify_branch(context.local_branch)
|
|
88
|
+
if context.local_branch == "features":
|
|
89
|
+
return PolicyResult(
|
|
90
|
+
ok=True,
|
|
91
|
+
status="WARN",
|
|
92
|
+
branch="features",
|
|
93
|
+
rule="features is a legacy working branch; canonical feature branches use feature/*.",
|
|
94
|
+
fix="continue for now, but prefer feature/<name> for new work.",
|
|
95
|
+
config_source=self.config.config_source,
|
|
96
|
+
agent_hint=self._agent_hint("features", "legacy_features", "WARN"),
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
if local_type in {"release", "hotfix"}:
|
|
100
|
+
evidence_result = self._validate_release_evidence(context, local_type)
|
|
101
|
+
if evidence_result is not None:
|
|
102
|
+
return evidence_result
|
|
103
|
+
|
|
104
|
+
return PolicyResult(
|
|
105
|
+
ok=True,
|
|
106
|
+
status="OK",
|
|
107
|
+
branch=context.local_branch,
|
|
108
|
+
rule="branch satisfies Git Flow policy.",
|
|
109
|
+
fix="push this branch and open the configured PR when ready.",
|
|
110
|
+
config_source=self.config.config_source,
|
|
111
|
+
agent_hint=self._agent_hint(context.local_branch, local_type, "OK"),
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
def _validate_release_evidence(
|
|
115
|
+
self, context: PushContext, branch_type: str
|
|
116
|
+
) -> PolicyResult | None:
|
|
117
|
+
required_files = frozenset({"VERSION", "CHANGELOG.md"})
|
|
118
|
+
missing_files = required_files - context.changed_files
|
|
119
|
+
if not missing_files:
|
|
120
|
+
return None
|
|
121
|
+
|
|
122
|
+
source_branch = self.config.source_branch(branch_type)
|
|
123
|
+
missing_list = " and ".join(sorted(missing_files))
|
|
124
|
+
return self._blocked(
|
|
125
|
+
context.local_branch,
|
|
126
|
+
f"{branch_type} branches must change VERSION and CHANGELOG.md compared with {source_branch}; missing {missing_list}.",
|
|
127
|
+
"update VERSION and CHANGELOG.md, then rerun wl git-flow check.",
|
|
128
|
+
self._agent_hint(
|
|
129
|
+
context.local_branch,
|
|
130
|
+
branch_type,
|
|
131
|
+
"BLOCKED",
|
|
132
|
+
missing_changes=tuple(sorted(missing_files)),
|
|
133
|
+
),
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
def _has_bypass(self, context: PushContext) -> bool:
|
|
137
|
+
if context.bypass_token is None:
|
|
138
|
+
return False
|
|
139
|
+
return hmac.compare_digest(context.bypass_token, self.BYPASS_TOKEN)
|
|
140
|
+
|
|
141
|
+
def _blocked(
|
|
142
|
+
self, branch: str, rule: str, fix: str, agent_hint: AgentHint | None = None
|
|
143
|
+
) -> PolicyResult:
|
|
144
|
+
branch_type = self.classify_branch(branch) if branch != "unknown" else "unknown"
|
|
145
|
+
return PolicyResult(
|
|
146
|
+
ok=False,
|
|
147
|
+
status="BLOCKED",
|
|
148
|
+
branch=branch,
|
|
149
|
+
rule=rule,
|
|
150
|
+
fix=fix,
|
|
151
|
+
config_source=self.config.config_source,
|
|
152
|
+
agent_hint=agent_hint
|
|
153
|
+
or self._agent_hint(branch, branch_type, "BLOCKED", allowed_push=False),
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
def _agent_hint(
|
|
157
|
+
self,
|
|
158
|
+
branch: str,
|
|
159
|
+
branch_type: str,
|
|
160
|
+
status: PolicyStatus,
|
|
161
|
+
missing_changes: tuple[str, ...] = (),
|
|
162
|
+
allowed_push: bool = True,
|
|
163
|
+
) -> AgentHint:
|
|
164
|
+
required_changes: tuple[str, ...] = ()
|
|
165
|
+
source_branch = ""
|
|
166
|
+
merge_targets: tuple[str, ...] = ()
|
|
167
|
+
recommended_action = "rename_or_create_git_flow_branch"
|
|
168
|
+
rename_required = False
|
|
169
|
+
suggested_branch_patterns: tuple[str, ...] = ()
|
|
170
|
+
next_step = "Create a branch that matches feature/<name>, release/<version>, or hotfix/<name> depending on the work intent."
|
|
171
|
+
|
|
172
|
+
if branch_type == "feature":
|
|
173
|
+
source_branch = self.config.source_branch("feature")
|
|
174
|
+
merge_targets = tuple(self.config.merge_targets("feature"))
|
|
175
|
+
recommended_action = "create_pull_request"
|
|
176
|
+
next_step = "Push the current feature branch and open a PR into develop."
|
|
177
|
+
elif branch_type == "legacy_features":
|
|
178
|
+
source_branch = self.config.develop_branch
|
|
179
|
+
merge_targets = (self.config.develop_branch,)
|
|
180
|
+
recommended_action = "rename_branch_by_change_intent"
|
|
181
|
+
rename_required = True
|
|
182
|
+
suggested_branch_patterns = (
|
|
183
|
+
"feature/<short-topic>",
|
|
184
|
+
"release/<version>",
|
|
185
|
+
"hotfix/<short-topic>",
|
|
186
|
+
)
|
|
187
|
+
next_step = "Inspect the current code changes, infer the work intent, suggest a standard Git Flow branch name, and ask the user before renaming or pushing."
|
|
188
|
+
elif branch_type in {"release", "hotfix"}:
|
|
189
|
+
source_branch = self.config.source_branch(branch_type)
|
|
190
|
+
merge_targets = tuple(self.config.merge_targets(branch_type))
|
|
191
|
+
required_changes = ("VERSION", "CHANGELOG.md")
|
|
192
|
+
recommended_action = (
|
|
193
|
+
f"verify_{branch_type}_evidence_then_create_pull_requests"
|
|
194
|
+
)
|
|
195
|
+
next_step = "Confirm VERSION and CHANGELOG.md, then open PRs to main and back to develop."
|
|
196
|
+
elif branch_type in {"main", "develop"}:
|
|
197
|
+
recommended_action = "create_topic_branch"
|
|
198
|
+
next_step = "Do not direct-push; create a feature/*, release/*, or hotfix/* branch and open a PR."
|
|
199
|
+
|
|
200
|
+
return AgentHint(
|
|
201
|
+
branch=branch,
|
|
202
|
+
branch_type=branch_type,
|
|
203
|
+
status=status,
|
|
204
|
+
recommended_action=recommended_action,
|
|
205
|
+
source_branch=source_branch,
|
|
206
|
+
merge_targets=merge_targets,
|
|
207
|
+
allowed_push=allowed_push,
|
|
208
|
+
direct_push_allowed=allowed_push,
|
|
209
|
+
protected_target_push_allowed=branch_type not in {"main", "develop"},
|
|
210
|
+
required_changes=required_changes,
|
|
211
|
+
missing_changes=missing_changes,
|
|
212
|
+
rename_required=rename_required,
|
|
213
|
+
suggested_branch_patterns=suggested_branch_patterns,
|
|
214
|
+
next_step=next_step,
|
|
215
|
+
agent_policy="Suggest only; ask the user before executing git branch -m, git checkout -b, git push, gh pr create, merge, or branch deletion.",
|
|
216
|
+
)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Single parse of git pre-push stdin (#163).
|
|
2
|
+
|
|
3
|
+
Git hands a pre-push hook one line per ref being pushed:
|
|
4
|
+
|
|
5
|
+
<local-ref> <local-sha1> <remote-ref> <remote-sha1>
|
|
6
|
+
|
|
7
|
+
Three call sites used to split that text on their own: the vendored hook's
|
|
8
|
+
``parse_remote_refs`` (remote refs only), its ``_has_live_head_update`` (local
|
|
9
|
+
sha only) and ``git/hook.py``'s remote-branch lookup (``parts[2]`` only). The
|
|
10
|
+
shape of "what counts as a push" therefore had to be changed in three places
|
|
11
|
+
at once. This module parses it once, keeping both sides of each ref, so the
|
|
12
|
+
hook and its Python helpers read fields instead of re-splitting stdin.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
|
|
19
|
+
HEADS_PREFIX = "refs/heads/"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True)
|
|
23
|
+
class PushUpdate:
|
|
24
|
+
"""One ``<local-ref> <local-sha> <remote-ref> <remote-sha>`` pre-push line."""
|
|
25
|
+
|
|
26
|
+
local_ref: str
|
|
27
|
+
local_sha: str
|
|
28
|
+
remote_ref: str
|
|
29
|
+
remote_sha: str
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def is_branch(self) -> bool:
|
|
33
|
+
"""Whether this update targets a branch (tags and other refs excluded)."""
|
|
34
|
+
return self.remote_ref.startswith(HEADS_PREFIX)
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def is_delete(self) -> bool:
|
|
38
|
+
"""Whether this update only deletes the remote ref.
|
|
39
|
+
|
|
40
|
+
Deletions carry ``(delete)`` or an all-zero local sha and no content
|
|
41
|
+
for the Git Flow evidence rules to judge.
|
|
42
|
+
"""
|
|
43
|
+
return self.local_sha == "(delete)" or (
|
|
44
|
+
self.local_sha != "" and self.local_sha.strip("0") == ""
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def branch_name(self) -> str | None:
|
|
49
|
+
"""The remote branch name, or ``None`` for non-branch refs.
|
|
50
|
+
|
|
51
|
+
A deletion line still yields its branch name: branch protection needs
|
|
52
|
+
to know which branch would be deleted.
|
|
53
|
+
"""
|
|
54
|
+
if not self.is_branch:
|
|
55
|
+
return None
|
|
56
|
+
return self.remote_ref.removeprefix(HEADS_PREFIX)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def parse_push_updates(stdin_text: str) -> tuple[PushUpdate, ...]:
|
|
60
|
+
"""Parse pre-push stdin into updates, one ``splitlines``/``split`` pass.
|
|
61
|
+
|
|
62
|
+
Blank lines and lines with fewer than four fields are skipped: git never
|
|
63
|
+
emits them, but a truncated or hand-fed stream must not raise in a hook
|
|
64
|
+
that is supposed to fail open.
|
|
65
|
+
"""
|
|
66
|
+
updates: list[PushUpdate] = []
|
|
67
|
+
for line in stdin_text.splitlines():
|
|
68
|
+
line = line.strip()
|
|
69
|
+
if not line:
|
|
70
|
+
continue
|
|
71
|
+
parts = line.split()
|
|
72
|
+
if len(parts) < 4:
|
|
73
|
+
continue
|
|
74
|
+
updates.append(
|
|
75
|
+
PushUpdate(
|
|
76
|
+
local_ref=parts[0],
|
|
77
|
+
local_sha=parts[1],
|
|
78
|
+
remote_ref=parts[2],
|
|
79
|
+
remote_sha=parts[3],
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
return tuple(updates)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Init command is registered via Click group in initenv.py
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Common infrastructure for initenv module."""
|
|
2
|
+
|
|
3
|
+
from .error_handler import error_handler
|
|
4
|
+
from .exceptions import (
|
|
5
|
+
CargoSyncError,
|
|
6
|
+
GitInitError,
|
|
7
|
+
GitLfsInitError,
|
|
8
|
+
InitEnvError,
|
|
9
|
+
ProjectStructureError,
|
|
10
|
+
VenvCreationError,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
# Exceptions
|
|
15
|
+
"InitEnvError",
|
|
16
|
+
"ProjectStructureError",
|
|
17
|
+
"VenvCreationError",
|
|
18
|
+
"GitInitError",
|
|
19
|
+
"GitLfsInitError",
|
|
20
|
+
"CargoSyncError",
|
|
21
|
+
# Decorators
|
|
22
|
+
"error_handler",
|
|
23
|
+
]
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Shared Git Bash detection utility for initenv components."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from ....utils.logging import log_debug, log_info
|
|
7
|
+
from ....utils.platform_adapter import PlatformAdapter
|
|
8
|
+
|
|
9
|
+
_check_cache: bool | None = None
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _find_git_bash() -> str | None:
|
|
13
|
+
"""Find Git Bash executable path."""
|
|
14
|
+
common_paths = [
|
|
15
|
+
r"C:\Program Files\Git\bin\bash.exe",
|
|
16
|
+
r"C:\Program Files (x86)\Git\bin\bash.exe",
|
|
17
|
+
r"C:\Git\bin\bash.exe",
|
|
18
|
+
]
|
|
19
|
+
for path in common_paths:
|
|
20
|
+
if Path(path).exists():
|
|
21
|
+
return path
|
|
22
|
+
|
|
23
|
+
# Try to find git bash using where command
|
|
24
|
+
try:
|
|
25
|
+
import subprocess
|
|
26
|
+
|
|
27
|
+
result = subprocess.run(
|
|
28
|
+
["where", "bash"], capture_output=True, text=True, check=False
|
|
29
|
+
)
|
|
30
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
31
|
+
return result.stdout.strip().split("\n")[0]
|
|
32
|
+
except Exception:
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
return None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _handle_git_bash_result(git_bash_path: str | None) -> None:
|
|
39
|
+
"""Handle Git Bash path resolution."""
|
|
40
|
+
if git_bash_path:
|
|
41
|
+
bash_dir = str(Path(git_bash_path).parent)
|
|
42
|
+
os.environ.setdefault("GIT_BASH_PATH", git_bash_path)
|
|
43
|
+
os.environ.setdefault("GIT_BASH_DIR", bash_dir)
|
|
44
|
+
# Add Git Bash directory to PATH so hooks can find it during execution
|
|
45
|
+
path_env = os.environ.get("PATH", "")
|
|
46
|
+
if bash_dir not in path_env:
|
|
47
|
+
os.environ["PATH"] = f"{bash_dir}{os.pathsep}{path_env}"
|
|
48
|
+
log_debug(f"Git Bash found at: {git_bash_path}")
|
|
49
|
+
else:
|
|
50
|
+
log_info(
|
|
51
|
+
"Warning: Git Bash not found. Git hooks may not work properly on Windows."
|
|
52
|
+
)
|
|
53
|
+
log_info("警告: 未找到 Git Bash,Git 钩子在 Windows 上可能无法正常工作。")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def check_git_bash_installed() -> None:
|
|
57
|
+
"""Check if Git Bash is installed on Windows. Results cached after first call.
|
|
58
|
+
|
|
59
|
+
Git Bash is required for Git hooks to work properly on Windows.
|
|
60
|
+
If Git Bash is not found, display a warning message.
|
|
61
|
+
If Git Bash is found but not in PATH, add its directory to the project environment.
|
|
62
|
+
"""
|
|
63
|
+
global _check_cache
|
|
64
|
+
if _check_cache is not None:
|
|
65
|
+
return
|
|
66
|
+
|
|
67
|
+
if not PlatformAdapter.is_windows():
|
|
68
|
+
_check_cache = True
|
|
69
|
+
return
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
git_bash_path = _find_git_bash()
|
|
73
|
+
_handle_git_bash_result(git_bash_path)
|
|
74
|
+
except Exception as e:
|
|
75
|
+
log_info(f"Warning: Error checking for Git Bash: {e}")
|
|
76
|
+
log_info(f"警告: 检查Git Bash时出错: {e}")
|
|
77
|
+
finally:
|
|
78
|
+
_check_cache = True
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Error handling utilities for initialization commands."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from functools import wraps
|
|
5
|
+
from typing import Any, TypeVar, cast
|
|
6
|
+
|
|
7
|
+
from wl_cli.exceptions import CommandError
|
|
8
|
+
from wl_cli.utils.logging import log_error_pair
|
|
9
|
+
|
|
10
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def error_handler(
|
|
14
|
+
message: str | F | None = None,
|
|
15
|
+
) -> Callable[..., Any] | F:
|
|
16
|
+
"""
|
|
17
|
+
Decorator to handle exceptions in initialization functions.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
message: Custom error message to use if provided,
|
|
21
|
+
or the function itself when used without parentheses
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
Callable: Decorated function
|
|
25
|
+
"""
|
|
26
|
+
# Case 1: decorator used without parentheses: @error_handler
|
|
27
|
+
if message is not None and callable(message):
|
|
28
|
+
func: F = message
|
|
29
|
+
|
|
30
|
+
@wraps(func)
|
|
31
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
32
|
+
try:
|
|
33
|
+
return func(*args, **kwargs)
|
|
34
|
+
except Exception as e:
|
|
35
|
+
# Generate error message
|
|
36
|
+
error_msg = str(e)
|
|
37
|
+
|
|
38
|
+
# Log the error
|
|
39
|
+
log_error_pair(f"Error: {error_msg}", f"错误: {error_msg}")
|
|
40
|
+
|
|
41
|
+
# Raise CommandError with the error message
|
|
42
|
+
raise CommandError(error_msg) from e
|
|
43
|
+
|
|
44
|
+
return cast("F", wrapper)
|
|
45
|
+
|
|
46
|
+
# Case 2: decorator used with parentheses: @error_handler("custom message")
|
|
47
|
+
message_str: str | None = message
|
|
48
|
+
|
|
49
|
+
def decorator(func: F) -> F:
|
|
50
|
+
@wraps(func)
|
|
51
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
52
|
+
try:
|
|
53
|
+
return func(*args, **kwargs)
|
|
54
|
+
except Exception as e:
|
|
55
|
+
# Generate error message
|
|
56
|
+
error_msg = str(e)
|
|
57
|
+
if message_str:
|
|
58
|
+
error_msg = f"{message_str}: {error_msg}"
|
|
59
|
+
|
|
60
|
+
# Log the error
|
|
61
|
+
log_error_pair(f"Error: {error_msg}", f"错误: {error_msg}")
|
|
62
|
+
|
|
63
|
+
# Raise CommandError with the error message
|
|
64
|
+
raise CommandError(error_msg) from e
|
|
65
|
+
|
|
66
|
+
return cast("F", wrapper)
|
|
67
|
+
|
|
68
|
+
return decorator
|