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,417 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# WL-Pre-Push-Hook-Version: config-driven-git-flow-protection-v3
|
|
3
|
+
# Bump the marker VALUE on any body change: the cheap per-command gate
|
|
4
|
+
# (hooks_manager.hooks_need_sync) compares this value against the deployed
|
|
5
|
+
# copy to decide whether to re-sync (#158/B3).
|
|
6
|
+
"""
|
|
7
|
+
Pre-push hook: enforces Git Flow branch protection and push policy.
|
|
8
|
+
|
|
9
|
+
Replaces the previous hardcoded {main, develop} protection with a
|
|
10
|
+
config-driven approach:
|
|
11
|
+
|
|
12
|
+
- Protected branches come from GitFlowConfig (.wl/git-flow.yaml overrides
|
|
13
|
+
plus the built-in defaults where main/develop have protected: True).
|
|
14
|
+
- If the config cannot be read, protection falls back to the safe defaults
|
|
15
|
+
{main, develop} (protection is never opened up because the config was
|
|
16
|
+
unavailable).
|
|
17
|
+
- Git Flow policy is hard-enforced at push: release/hotfix branches must
|
|
18
|
+
bump VERSION and CHANGELOG.md, branch naming is validated, and direct
|
|
19
|
+
pushes to protected main/develop are blocked (require_pr semantics).
|
|
20
|
+
|
|
21
|
+
Backend resolution (hybrid):
|
|
22
|
+
1. In-process import of wl_cli.commands.git.push_update for the stdin parse
|
|
23
|
+
when available; otherwise a minimal local fallback parser (marked below),
|
|
24
|
+
which still feeds branch protection and the live-head gate.
|
|
25
|
+
2. In-process import of wl_cli.commands.git.* when available.
|
|
26
|
+
3. Otherwise fall back to the `wl` CLI via subprocess.
|
|
27
|
+
4. Otherwise fall back to default {main, develop} branch protection only
|
|
28
|
+
(the git-flow policy check is skipped when wl is unavailable).
|
|
29
|
+
|
|
30
|
+
Behavior:
|
|
31
|
+
- Parses stdin for git pre-push input (one line per ref being pushed) ONCE
|
|
32
|
+
into PushUpdate records; every later check reads fields from them (#163).
|
|
33
|
+
- Blocks pushes where the remote ref is a protected branch.
|
|
34
|
+
- Blocks pushes that violate Git Flow policy.
|
|
35
|
+
- Blocks --no-verify / -n flags (prevents bypass attempts).
|
|
36
|
+
- All other refs (feature branches, tags, deletions) are allowed.
|
|
37
|
+
- On stdin read/parse failure, prints a warning and exits 0 (fail-open).
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
# PEP 604 unions in the annotations below (``int | None``, ``dict | str``,
|
|
41
|
+
# ``frozenset[str] | None``) would otherwise be evaluated at function-definition
|
|
42
|
+
# time and raise ``TypeError`` on Python < 3.10 -- i.e. on the system ``python3``
|
|
43
|
+
# (3.9) that ``#!/usr/bin/env python3`` resolves to on macOS, breaking every
|
|
44
|
+
# push. ``from __future__ import annotations`` makes just those *annotations*
|
|
45
|
+
# lazy, so the ``from __future__`` line itself is 3.7+-compatible; the hook as a
|
|
46
|
+
# whole still requires Python 3.9+, because it calls ``str.removeprefix()``
|
|
47
|
+
# below (3.9+). (#156)
|
|
48
|
+
from __future__ import annotations
|
|
49
|
+
|
|
50
|
+
import os
|
|
51
|
+
import shutil
|
|
52
|
+
import subprocess
|
|
53
|
+
import sys
|
|
54
|
+
|
|
55
|
+
# Set stdout/stderr encoding to UTF-8 for cross-platform emoji support.
|
|
56
|
+
# On Windows with Chinese locale (GBK), emoji characters crash print().
|
|
57
|
+
if sys.stdout.encoding and sys.stdout.encoding.upper() not in ("UTF-8", "UTF8"):
|
|
58
|
+
_stdout = sys.stdout.buffer if hasattr(sys.stdout, "buffer") else None
|
|
59
|
+
if _stdout:
|
|
60
|
+
sys.stdout = type(sys.stdout)(
|
|
61
|
+
_stdout,
|
|
62
|
+
encoding="utf-8",
|
|
63
|
+
errors="replace",
|
|
64
|
+
)
|
|
65
|
+
_stderr = sys.stderr.buffer if hasattr(sys.stderr, "buffer") else None
|
|
66
|
+
if _stderr:
|
|
67
|
+
sys.stderr = type(sys.stderr)(
|
|
68
|
+
_stderr,
|
|
69
|
+
encoding="utf-8",
|
|
70
|
+
errors="replace",
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def check_no_verify(argv: list[str]) -> int | None:
|
|
75
|
+
"""Return exit code 1 if --no-verify or -n is present, None otherwise."""
|
|
76
|
+
if "--no-verify" in argv or "-n" in argv:
|
|
77
|
+
print(
|
|
78
|
+
"错误: 不允许使用 --no-verify 或 -n 选项绕过 pre-push 检查。",
|
|
79
|
+
file=sys.stderr,
|
|
80
|
+
)
|
|
81
|
+
print(
|
|
82
|
+
"请使用 feature 分支推送代码,而不是绕过分支保护。",
|
|
83
|
+
file=sys.stderr,
|
|
84
|
+
)
|
|
85
|
+
print(file=sys.stderr)
|
|
86
|
+
print(
|
|
87
|
+
"ERROR: --no-verify/-n is not allowed for protected branches.",
|
|
88
|
+
file=sys.stderr,
|
|
89
|
+
)
|
|
90
|
+
print(
|
|
91
|
+
"Use a feature branch instead of bypassing branch protection.",
|
|
92
|
+
file=sys.stderr,
|
|
93
|
+
)
|
|
94
|
+
return 1
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def read_stdin() -> str:
|
|
99
|
+
"""Read all stdin input, handling bytes decoding."""
|
|
100
|
+
try:
|
|
101
|
+
raw = sys.stdin.buffer.read()
|
|
102
|
+
return raw.decode("utf-8", errors="replace")
|
|
103
|
+
except AttributeError:
|
|
104
|
+
# Fallback when sys.stdin.buffer is unavailable
|
|
105
|
+
return sys.stdin.read()
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class _FallbackPushUpdate:
|
|
109
|
+
"""Minimal PushUpdate stand-in for the degraded (no wl_cli) path.
|
|
110
|
+
|
|
111
|
+
Mirrors ``wl_cli.commands.git.push_update.PushUpdate``'s four fields and
|
|
112
|
+
branch predicates, so the checks below read one shape whether or not the
|
|
113
|
+
package imported.
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
__slots__ = ("local_ref", "local_sha", "remote_ref", "remote_sha")
|
|
117
|
+
|
|
118
|
+
def __init__(
|
|
119
|
+
self,
|
|
120
|
+
local_ref: str,
|
|
121
|
+
local_sha: str,
|
|
122
|
+
remote_ref: str,
|
|
123
|
+
remote_sha: str,
|
|
124
|
+
) -> None:
|
|
125
|
+
self.local_ref = local_ref
|
|
126
|
+
self.local_sha = local_sha
|
|
127
|
+
self.remote_ref = remote_ref
|
|
128
|
+
self.remote_sha = remote_sha
|
|
129
|
+
|
|
130
|
+
@property
|
|
131
|
+
def is_branch(self) -> bool:
|
|
132
|
+
return self.remote_ref.startswith("refs/heads/")
|
|
133
|
+
|
|
134
|
+
@property
|
|
135
|
+
def is_delete(self) -> bool:
|
|
136
|
+
return self.local_sha == "(delete)" or (
|
|
137
|
+
self.local_sha != "" and self.local_sha.strip("0") == ""
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
@property
|
|
141
|
+
def branch_name(self) -> str | None:
|
|
142
|
+
if not self.is_branch:
|
|
143
|
+
return None
|
|
144
|
+
return self.remote_ref.removeprefix("refs/heads/")
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _parse_push_updates_fallback(data: str) -> list[_FallbackPushUpdate]:
|
|
148
|
+
"""Degraded path: parse stdin here only because wl_cli is not importable.
|
|
149
|
+
|
|
150
|
+
This is the *only* place besides ``wl_cli.commands.git.push_update`` that
|
|
151
|
+
splits pre-push stdin; when the package imports, it is never called.
|
|
152
|
+
"""
|
|
153
|
+
updates: list[_FallbackPushUpdate] = []
|
|
154
|
+
for line in data.splitlines():
|
|
155
|
+
line = line.strip()
|
|
156
|
+
if not line:
|
|
157
|
+
continue
|
|
158
|
+
parts = line.split()
|
|
159
|
+
# Format: local-ref local-sha remote-ref remote-sha
|
|
160
|
+
if len(parts) < 4:
|
|
161
|
+
continue
|
|
162
|
+
updates.append(
|
|
163
|
+
_FallbackPushUpdate(parts[0], parts[1], parts[2], parts[3])
|
|
164
|
+
)
|
|
165
|
+
return updates
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _load_push_update_parser():
|
|
169
|
+
"""Resolve the single stdin parser for this run (hybrid backend).
|
|
170
|
+
|
|
171
|
+
Prefers the in-process ``wl_cli.commands.git.push_update`` module so the
|
|
172
|
+
hook and the Python helpers share one definition of a push. When wl_cli
|
|
173
|
+
cannot be imported -- e.g. the hook runs under a bare system ``python3`` --
|
|
174
|
+
this degrades to :func:`_parse_push_updates_fallback`; the Git Flow policy
|
|
175
|
+
already has its own degraded paths (``wl`` subprocess / defaults).
|
|
176
|
+
"""
|
|
177
|
+
try:
|
|
178
|
+
from wl_cli.commands.git.push_update import parse_push_updates
|
|
179
|
+
|
|
180
|
+
return parse_push_updates
|
|
181
|
+
except Exception: # noqa: BLE001 - the hook must stay runnable either way
|
|
182
|
+
return _parse_push_updates_fallback
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def parse_push_updates(data: str):
|
|
186
|
+
"""Parse pre-push stdin into ``PushUpdate`` records, exactly once per run."""
|
|
187
|
+
return _load_push_update_parser()(data)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def parse_remote_refs(data: str) -> list[str]:
|
|
191
|
+
"""
|
|
192
|
+
Parse git pre-push stdin and extract remote branch ref names.
|
|
193
|
+
|
|
194
|
+
Thin wrapper over :func:`parse_push_updates` kept for compatibility: the
|
|
195
|
+
hook itself filters ``update.remote_ref`` from its single parse instead of
|
|
196
|
+
calling this again (#163).
|
|
197
|
+
|
|
198
|
+
Git pre-push format (one line per ref being pushed):
|
|
199
|
+
<local-ref> <local-sha1> <remote-ref> <remote-sha1>
|
|
200
|
+
|
|
201
|
+
Returns list of remote refs like 'refs/heads/main'.
|
|
202
|
+
"""
|
|
203
|
+
return [update.remote_ref for update in parse_push_updates(data) if update.is_branch]
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _has_live_head_update(updates) -> bool:
|
|
207
|
+
"""Whether the parsed updates carry a real (non-deletion) refs/heads update.
|
|
208
|
+
|
|
209
|
+
Deletion lines use ``(delete)`` / an all-zero local sha and only remove a
|
|
210
|
+
branch, so they carry no content for git-flow evidence to judge.
|
|
211
|
+
"""
|
|
212
|
+
for update in updates:
|
|
213
|
+
if update.is_branch and not update.is_delete:
|
|
214
|
+
return True
|
|
215
|
+
return False
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _resolve_protected_branches() -> frozenset[str]:
|
|
219
|
+
"""Resolve the protected branch ref set from GitFlowConfig.
|
|
220
|
+
|
|
221
|
+
Protected branches come from the configured main/develop branch names.
|
|
222
|
+
Falls back to the safe default {main, develop} whenever the config cannot
|
|
223
|
+
be read — protection is never opened up because the config was missing,
|
|
224
|
+
malformed, or wl_cli could not be imported.
|
|
225
|
+
"""
|
|
226
|
+
try:
|
|
227
|
+
from wl_cli.commands.git.config import GitFlowConfig
|
|
228
|
+
|
|
229
|
+
config = GitFlowConfig()
|
|
230
|
+
return frozenset(
|
|
231
|
+
{
|
|
232
|
+
f"refs/heads/{config.main_branch}",
|
|
233
|
+
f"refs/heads/{config.develop_branch}",
|
|
234
|
+
}
|
|
235
|
+
)
|
|
236
|
+
except Exception:
|
|
237
|
+
return frozenset({"refs/heads/main", "refs/heads/develop"})
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def check_branch_protection(
|
|
241
|
+
remote_refs: list[str],
|
|
242
|
+
protected_branches: frozenset[str] | None = None,
|
|
243
|
+
) -> int:
|
|
244
|
+
"""Check if any remote ref is a protected branch. Returns 1 if blocked."""
|
|
245
|
+
if protected_branches is None:
|
|
246
|
+
protected_branches = _resolve_protected_branches()
|
|
247
|
+
|
|
248
|
+
for ref in remote_refs:
|
|
249
|
+
if ref in protected_branches:
|
|
250
|
+
branch = ref.removeprefix("refs/heads/")
|
|
251
|
+
print(f'🚫 PUSH BLOCKED: Direct push to "{branch}" branch is not allowed.')
|
|
252
|
+
print()
|
|
253
|
+
print("📋 Git Flow Policy:")
|
|
254
|
+
print(" - Never push directly to main or develop")
|
|
255
|
+
print(" - Create a feature/fix branch for your changes")
|
|
256
|
+
print(" - Submit changes via Pull Request only")
|
|
257
|
+
print()
|
|
258
|
+
print("🤖 For AI Agents:")
|
|
259
|
+
print(" - Use `git checkout -b feature/<description>` to create a branch")
|
|
260
|
+
print(
|
|
261
|
+
" - Push to your feature branch instead:"
|
|
262
|
+
" `git push origin feature/<description>`"
|
|
263
|
+
)
|
|
264
|
+
print(" - Then create a Pull Request for review")
|
|
265
|
+
return 1
|
|
266
|
+
|
|
267
|
+
return 0
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _load_git_flow_backend() -> dict | str:
|
|
271
|
+
"""Load the in-process Git Flow backend.
|
|
272
|
+
|
|
273
|
+
Returns:
|
|
274
|
+
dict: backend with config/validator/hook/git_facts when available.
|
|
275
|
+
"subprocess": wl_cli is not importable; use the wl CLI.
|
|
276
|
+
"defaults": the config could not be read; skip policy validation.
|
|
277
|
+
"""
|
|
278
|
+
try:
|
|
279
|
+
from wl_cli.commands.git import git_facts
|
|
280
|
+
from wl_cli.commands.git import hook as git_hook
|
|
281
|
+
from wl_cli.commands.git.config import GitFlowConfig
|
|
282
|
+
from wl_cli.commands.git.policy import GitFlowPolicyValidator
|
|
283
|
+
|
|
284
|
+
config = GitFlowConfig()
|
|
285
|
+
return {
|
|
286
|
+
"config": config,
|
|
287
|
+
"validator": GitFlowPolicyValidator(config),
|
|
288
|
+
"hook": git_hook,
|
|
289
|
+
"git_facts": git_facts,
|
|
290
|
+
}
|
|
291
|
+
except ImportError:
|
|
292
|
+
return "subprocess"
|
|
293
|
+
except Exception:
|
|
294
|
+
return "defaults"
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def _run_wl_subprocess_check() -> int:
|
|
298
|
+
"""Validate the push via the `wl` CLI subprocess.
|
|
299
|
+
|
|
300
|
+
Returns 1 when the CLI blocks the push, 0 when allowed or wl is missing.
|
|
301
|
+
"""
|
|
302
|
+
wl_exe = shutil.which("wl")
|
|
303
|
+
if not wl_exe:
|
|
304
|
+
return 0
|
|
305
|
+
try:
|
|
306
|
+
result = subprocess.run(
|
|
307
|
+
[wl_exe, "git-flow", "check"],
|
|
308
|
+
capture_output=True,
|
|
309
|
+
text=True,
|
|
310
|
+
check=False,
|
|
311
|
+
)
|
|
312
|
+
if result.stdout:
|
|
313
|
+
print(result.stdout.strip())
|
|
314
|
+
if result.stderr:
|
|
315
|
+
print(result.stderr.strip(), file=sys.stderr)
|
|
316
|
+
return 1 if result.returncode != 0 else 0
|
|
317
|
+
except (OSError, subprocess.SubprocessError):
|
|
318
|
+
return 0
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def _run_git_flow_policy(updates) -> int:
|
|
322
|
+
"""Enforce Git Flow policy for the push. Returns 1 if blocked, else 0."""
|
|
323
|
+
backend = _load_git_flow_backend()
|
|
324
|
+
if backend == "defaults":
|
|
325
|
+
# Policy skipped; branch protection already ran with default {main, develop}.
|
|
326
|
+
return 0
|
|
327
|
+
if backend == "subprocess":
|
|
328
|
+
return _run_wl_subprocess_check()
|
|
329
|
+
|
|
330
|
+
config = backend["config"]
|
|
331
|
+
validator = backend["validator"]
|
|
332
|
+
git_hook = backend["hook"]
|
|
333
|
+
git_facts = backend["git_facts"]
|
|
334
|
+
|
|
335
|
+
local_branch = git_facts.get_current_branch()
|
|
336
|
+
local_type = validator.classify_branch(local_branch)
|
|
337
|
+
source = (
|
|
338
|
+
config.source_branch(local_type)
|
|
339
|
+
if local_type in {"feature", "release", "hotfix"}
|
|
340
|
+
else None
|
|
341
|
+
)
|
|
342
|
+
changed_files = (
|
|
343
|
+
git_facts.get_changed_files(source, file_filters=["VERSION", "CHANGELOG.md"])
|
|
344
|
+
if source
|
|
345
|
+
else frozenset()
|
|
346
|
+
)
|
|
347
|
+
context = git_hook.build_push_context(
|
|
348
|
+
local_branch=local_branch,
|
|
349
|
+
updates=tuple(updates),
|
|
350
|
+
changed_files=changed_files,
|
|
351
|
+
)
|
|
352
|
+
result = validator.validate_push(context)
|
|
353
|
+
if not result.ok:
|
|
354
|
+
print(git_hook.format_hook_failure(result))
|
|
355
|
+
return 1
|
|
356
|
+
return 0
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def main() -> int:
|
|
360
|
+
"""Main entry point for the pre-push hook."""
|
|
361
|
+
|
|
362
|
+
# Step 1: Block --no-verify bypass attempts
|
|
363
|
+
result = check_no_verify(sys.argv)
|
|
364
|
+
if result is not None:
|
|
365
|
+
return result
|
|
366
|
+
|
|
367
|
+
# Step 2: Read stdin (fail-open on read errors)
|
|
368
|
+
try:
|
|
369
|
+
data = read_stdin()
|
|
370
|
+
except Exception as exc:
|
|
371
|
+
print(
|
|
372
|
+
f"Warning: Could not read pre-push input: {exc}",
|
|
373
|
+
file=sys.stderr,
|
|
374
|
+
)
|
|
375
|
+
print(
|
|
376
|
+
"Warning: Branch protection check skipped.",
|
|
377
|
+
file=sys.stderr,
|
|
378
|
+
)
|
|
379
|
+
return 0 # fail-open
|
|
380
|
+
|
|
381
|
+
# Step 3: Parse stdin ONCE into PushUpdate records (fail-open on errors).
|
|
382
|
+
# Branch protection, the live-head gate and the policy context all read
|
|
383
|
+
# these records; nothing below splits stdin a second time (#163).
|
|
384
|
+
try:
|
|
385
|
+
updates = tuple(parse_push_updates(data))
|
|
386
|
+
except Exception as exc:
|
|
387
|
+
print(
|
|
388
|
+
f"Warning: Could not parse pre-push input: {exc}",
|
|
389
|
+
file=sys.stderr,
|
|
390
|
+
)
|
|
391
|
+
print(
|
|
392
|
+
"Warning: Branch protection check skipped.",
|
|
393
|
+
file=sys.stderr,
|
|
394
|
+
)
|
|
395
|
+
return 0 # fail-open
|
|
396
|
+
|
|
397
|
+
# Step 4: Branch protection (config-driven with safe default fallback)
|
|
398
|
+
protected = _resolve_protected_branches()
|
|
399
|
+
remote_refs = [update.remote_ref for update in updates if update.is_branch]
|
|
400
|
+
if check_branch_protection(remote_refs, protected) != 0:
|
|
401
|
+
return 1
|
|
402
|
+
|
|
403
|
+
# Step 4.5: Pushes with no live head update -- only tags, or branch
|
|
404
|
+
# deletions (e.g. `git push origin v1.2.3` / `--tags`, or
|
|
405
|
+
# `git push origin --delete release/1.0`) -- carry no content for the
|
|
406
|
+
# policy's evidence check to judge, so the local-branch fallback and
|
|
407
|
+
# release/hotfix evidence rules must not block them (#43 / review D2).
|
|
408
|
+
# Branch protection above still blocks deletions/updates of main/develop.
|
|
409
|
+
if not _has_live_head_update(updates):
|
|
410
|
+
return 0
|
|
411
|
+
|
|
412
|
+
# Step 5: Git Flow policy validation (hybrid backend)
|
|
413
|
+
return _run_git_flow_policy(updates)
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
if __name__ == "__main__":
|
|
417
|
+
sys.exit(main())
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
|
|
3
|
+
# An example hook script to verify what is about to be pushed. Called by "git
|
|
4
|
+
# push" after it has checked the remote status, but before anything has been
|
|
5
|
+
# pushed. If this script exits with a non-zero status nothing will be pushed.
|
|
6
|
+
#
|
|
7
|
+
# This hook is called with the following parameters:
|
|
8
|
+
#
|
|
9
|
+
# $1 -- Name of the remote to which the push is being done
|
|
10
|
+
# $2 -- URL to which the push is being done
|
|
11
|
+
#
|
|
12
|
+
# If pushing without using a named remote those arguments will be equal.
|
|
13
|
+
#
|
|
14
|
+
# Information about the commits which are being pushed is supplied as lines to
|
|
15
|
+
# the standard input in the form:
|
|
16
|
+
#
|
|
17
|
+
# <local ref> <local oid> <remote ref> <remote oid>
|
|
18
|
+
#
|
|
19
|
+
# This sample shows how to prevent push of commits where the log message starts
|
|
20
|
+
# with "WIP" (work in progress).
|
|
21
|
+
|
|
22
|
+
remote="$1"
|
|
23
|
+
url="$2"
|
|
24
|
+
|
|
25
|
+
zero=$(git hash-object --stdin </dev/null | tr '[0-9a-f]' '0')
|
|
26
|
+
|
|
27
|
+
while read local_ref local_oid remote_ref remote_oid
|
|
28
|
+
do
|
|
29
|
+
if test "$local_oid" = "$zero"
|
|
30
|
+
then
|
|
31
|
+
# Handle delete
|
|
32
|
+
:
|
|
33
|
+
else
|
|
34
|
+
if test "$remote_oid" = "$zero"
|
|
35
|
+
then
|
|
36
|
+
# New branch, examine all commits
|
|
37
|
+
range="$local_oid"
|
|
38
|
+
else
|
|
39
|
+
# Update to existing branch, examine new commits
|
|
40
|
+
range="$remote_oid..$local_oid"
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
# Check for WIP commit
|
|
44
|
+
commit=$(git rev-list -n 1 --grep '^WIP' "$range")
|
|
45
|
+
if test -n "$commit"
|
|
46
|
+
then
|
|
47
|
+
echo >&2 "Found WIP commit in $local_ref, not pushing"
|
|
48
|
+
exit 1
|
|
49
|
+
fi
|
|
50
|
+
fi
|
|
51
|
+
done
|
|
52
|
+
|
|
53
|
+
exit 0
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
#
|
|
3
|
+
# Copyright (c) 2006, 2008 Junio C Hamano
|
|
4
|
+
#
|
|
5
|
+
# The "pre-rebase" hook is run just before "git rebase" starts doing
|
|
6
|
+
# its job, and can prevent the command from running by exiting with
|
|
7
|
+
# non-zero status.
|
|
8
|
+
#
|
|
9
|
+
# The hook is called with the following parameters:
|
|
10
|
+
#
|
|
11
|
+
# $1 -- the upstream the series was forked from.
|
|
12
|
+
# $2 -- the branch being rebased (or empty when rebasing the current branch).
|
|
13
|
+
#
|
|
14
|
+
# This sample shows how to prevent topic branches that are already
|
|
15
|
+
# merged to 'next' branch from getting rebased, because allowing it
|
|
16
|
+
# would result in rebasing already published history.
|
|
17
|
+
|
|
18
|
+
publish=next
|
|
19
|
+
basebranch="$1"
|
|
20
|
+
if test "$#" = 2
|
|
21
|
+
then
|
|
22
|
+
topic="refs/heads/$2"
|
|
23
|
+
else
|
|
24
|
+
topic=`git symbolic-ref HEAD` ||
|
|
25
|
+
exit 0 ;# we do not interrupt rebasing detached HEAD
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
case "$topic" in
|
|
29
|
+
refs/heads/??/*)
|
|
30
|
+
;;
|
|
31
|
+
*)
|
|
32
|
+
exit 0 ;# we do not interrupt others.
|
|
33
|
+
;;
|
|
34
|
+
esac
|
|
35
|
+
|
|
36
|
+
# Now we are dealing with a topic branch being rebased
|
|
37
|
+
# on top of master. Is it OK to rebase it?
|
|
38
|
+
|
|
39
|
+
# Does the topic really exist?
|
|
40
|
+
git show-ref -q "$topic" || {
|
|
41
|
+
echo >&2 "No such branch $topic"
|
|
42
|
+
exit 1
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
# Is topic fully merged to master?
|
|
46
|
+
not_in_master=`git rev-list --pretty=oneline ^master "$topic"`
|
|
47
|
+
if test -z "$not_in_master"
|
|
48
|
+
then
|
|
49
|
+
echo >&2 "$topic is fully merged to master; better remove it."
|
|
50
|
+
exit 1 ;# we could allow it, but there is no point.
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
# Is topic ever merged to next? If so you should not be rebasing it.
|
|
54
|
+
only_next_1=`git rev-list ^master "^$topic" ${publish} | sort`
|
|
55
|
+
only_next_2=`git rev-list ^master ${publish} | sort`
|
|
56
|
+
if test "$only_next_1" = "$only_next_2"
|
|
57
|
+
then
|
|
58
|
+
not_in_topic=`git rev-list "^$topic" master`
|
|
59
|
+
if test -z "$not_in_topic"
|
|
60
|
+
then
|
|
61
|
+
echo >&2 "$topic is already up to date with master"
|
|
62
|
+
exit 1 ;# we could allow it, but there is no point.
|
|
63
|
+
else
|
|
64
|
+
exit 0
|
|
65
|
+
fi
|
|
66
|
+
else
|
|
67
|
+
not_in_next=`git rev-list --pretty=oneline ^${publish} "$topic"`
|
|
68
|
+
/usr/bin/perl -e '
|
|
69
|
+
my $topic = $ARGV[0];
|
|
70
|
+
my $msg = "* $topic has commits already merged to public branch:\n";
|
|
71
|
+
my (%not_in_next) = map {
|
|
72
|
+
/^([0-9a-f]+) /;
|
|
73
|
+
($1 => 1);
|
|
74
|
+
} split(/\n/, $ARGV[1]);
|
|
75
|
+
for my $elem (map {
|
|
76
|
+
/^([0-9a-f]+) (.*)$/;
|
|
77
|
+
[$1 => $2];
|
|
78
|
+
} split(/\n/, $ARGV[2])) {
|
|
79
|
+
if (!exists $not_in_next{$elem->[0]}) {
|
|
80
|
+
if ($msg) {
|
|
81
|
+
print STDERR $msg;
|
|
82
|
+
undef $msg;
|
|
83
|
+
}
|
|
84
|
+
print STDERR " $elem->[1]\n";
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
' "$topic" "$not_in_next" "$not_in_master"
|
|
88
|
+
exit 1
|
|
89
|
+
fi
|
|
90
|
+
|
|
91
|
+
<<\DOC_END
|
|
92
|
+
|
|
93
|
+
This sample hook safeguards topic branches that have been
|
|
94
|
+
published from being rewound.
|
|
95
|
+
|
|
96
|
+
The workflow assumed here is:
|
|
97
|
+
|
|
98
|
+
* Once a topic branch forks from "master", "master" is never
|
|
99
|
+
merged into it again (either directly or indirectly).
|
|
100
|
+
|
|
101
|
+
* Once a topic branch is fully cooked and merged into "master",
|
|
102
|
+
it is deleted. If you need to build on top of it to correct
|
|
103
|
+
earlier mistakes, a new topic branch is created by forking at
|
|
104
|
+
the tip of the "master". This is not strictly necessary, but
|
|
105
|
+
it makes it easier to keep your history simple.
|
|
106
|
+
|
|
107
|
+
* Whenever you need to test or publish your changes to topic
|
|
108
|
+
branches, merge them into "next" branch.
|
|
109
|
+
|
|
110
|
+
The script, being an example, hardcodes the publish branch name
|
|
111
|
+
to be "next", but it is trivial to make it configurable via
|
|
112
|
+
$GIT_DIR/config mechanism.
|
|
113
|
+
|
|
114
|
+
With this workflow, you would want to know:
|
|
115
|
+
|
|
116
|
+
(1) ... if a topic branch has ever been merged to "next". Young
|
|
117
|
+
topic branches can have stupid mistakes you would rather
|
|
118
|
+
clean up before publishing, and things that have not been
|
|
119
|
+
merged into other branches can be easily rebased without
|
|
120
|
+
affecting other people. But once it is published, you would
|
|
121
|
+
not want to rewind it.
|
|
122
|
+
|
|
123
|
+
(2) ... if a topic branch has been fully merged to "master".
|
|
124
|
+
Then you can delete it. More importantly, you should not
|
|
125
|
+
build on top of it -- other people may already want to
|
|
126
|
+
change things related to the topic as patches against your
|
|
127
|
+
"master", so if you need further changes, it is better to
|
|
128
|
+
fork the topic (perhaps with the same name) afresh from the
|
|
129
|
+
tip of "master".
|
|
130
|
+
|
|
131
|
+
Let's look at this example:
|
|
132
|
+
|
|
133
|
+
o---o---o---o---o---o---o---o---o---o "next"
|
|
134
|
+
/ / / /
|
|
135
|
+
/ a---a---b A / /
|
|
136
|
+
/ / / /
|
|
137
|
+
/ / c---c---c---c B /
|
|
138
|
+
/ / / \ /
|
|
139
|
+
/ / / b---b C \ /
|
|
140
|
+
/ / / / \ /
|
|
141
|
+
---o---o---o---o---o---o---o---o---o---o---o "master"
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
A, B and C are topic branches.
|
|
145
|
+
|
|
146
|
+
* A has one fix since it was merged up to "next".
|
|
147
|
+
|
|
148
|
+
* B has finished. It has been fully merged up to "master" and "next",
|
|
149
|
+
and is ready to be deleted.
|
|
150
|
+
|
|
151
|
+
* C has not merged to "next" at all.
|
|
152
|
+
|
|
153
|
+
We would want to allow C to be rebased, refuse A, and encourage
|
|
154
|
+
B to be deleted.
|
|
155
|
+
|
|
156
|
+
To compute (1):
|
|
157
|
+
|
|
158
|
+
git rev-list ^master ^topic next
|
|
159
|
+
git rev-list ^master next
|
|
160
|
+
|
|
161
|
+
if these match, topic has not merged in next at all.
|
|
162
|
+
|
|
163
|
+
To compute (2):
|
|
164
|
+
|
|
165
|
+
git rev-list master..topic
|
|
166
|
+
|
|
167
|
+
if this is empty, it is fully merged to "master".
|
|
168
|
+
|
|
169
|
+
DOC_END
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
#
|
|
3
|
+
# An example hook script to make use of push options.
|
|
4
|
+
# The example simply echoes all push options that start with 'echoback='
|
|
5
|
+
# and rejects all pushes when the "reject" push option is used.
|
|
6
|
+
#
|
|
7
|
+
# To enable this hook, rename this file to "pre-receive".
|
|
8
|
+
|
|
9
|
+
if test -n "$GIT_PUSH_OPTION_COUNT"
|
|
10
|
+
then
|
|
11
|
+
i=0
|
|
12
|
+
while test "$i" -lt "$GIT_PUSH_OPTION_COUNT"
|
|
13
|
+
do
|
|
14
|
+
eval "value=\$GIT_PUSH_OPTION_$i"
|
|
15
|
+
case "$value" in
|
|
16
|
+
echoback=*)
|
|
17
|
+
echo "echo from the pre-receive-hook: ${value#*=}" >&2
|
|
18
|
+
;;
|
|
19
|
+
reject)
|
|
20
|
+
exit 1
|
|
21
|
+
esac
|
|
22
|
+
i=$((i + 1))
|
|
23
|
+
done
|
|
24
|
+
fi
|