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.
Files changed (320) hide show
  1. py_wlcli-0.8.0.dist-info/METADATA +139 -0
  2. py_wlcli-0.8.0.dist-info/RECORD +320 -0
  3. py_wlcli-0.8.0.dist-info/WHEEL +4 -0
  4. py_wlcli-0.8.0.dist-info/entry_points.txt +2 -0
  5. py_wlcli-0.8.0.dist-info/licenses/LICENSE +21 -0
  6. wl_cli/__init__.py +27 -0
  7. wl_cli/__main__.py +130 -0
  8. wl_cli/capabilities.py +79 -0
  9. wl_cli/cc/__init__.py +5 -0
  10. wl_cli/cc/checks.py +1019 -0
  11. wl_cli/cc/cli.py +370 -0
  12. wl_cli/cc/mutate.py +1350 -0
  13. wl_cli/cli_click.py +156 -0
  14. wl_cli/commands/__init__.py +84 -0
  15. wl_cli/commands/base.py +80 -0
  16. wl_cli/commands/buildcommands/__init__.py +1 -0
  17. wl_cli/commands/buildcommands/build.py +71 -0
  18. wl_cli/commands/buildcommands/build_utils.py +47 -0
  19. wl_cli/commands/buildcommands/builders/__init__.py +18 -0
  20. wl_cli/commands/buildcommands/builders/cleanup.py +35 -0
  21. wl_cli/commands/buildcommands/builders/coordinator.py +122 -0
  22. wl_cli/commands/buildcommands/builders/environment.py +81 -0
  23. wl_cli/commands/buildcommands/builders/rust_builder.py +39 -0
  24. wl_cli/commands/buildcommands/builders/stubs.py +58 -0
  25. wl_cli/commands/buildcommands/dist.py +326 -0
  26. wl_cli/commands/buildcommands/test.py +89 -0
  27. wl_cli/commands/clean/__init__.py +1 -0
  28. wl_cli/commands/clean/build.py +151 -0
  29. wl_cli/commands/clean/clean.py +294 -0
  30. wl_cli/commands/clean/clean_utils.py +77 -0
  31. wl_cli/commands/clean/core.py +236 -0
  32. wl_cli/commands/clean/dead_code/__init__.py +0 -0
  33. wl_cli/commands/clean/dead_code/dead_code_command.py +70 -0
  34. wl_cli/commands/clean/dead_code/detector.py +161 -0
  35. wl_cli/commands/clean/dead_code/fixer.py +176 -0
  36. wl_cli/commands/clean/dead_code/reporter.py +295 -0
  37. wl_cli/commands/clean/dead_code/utils.py +0 -0
  38. wl_cli/commands/clean/lfs.py +215 -0
  39. wl_cli/commands/clean/python.py +81 -0
  40. wl_cli/commands/clean/rust.py +91 -0
  41. wl_cli/commands/clean/utils/__init__.py +34 -0
  42. wl_cli/commands/clean/venv.py +91 -0
  43. wl_cli/commands/cleanup.py +73 -0
  44. wl_cli/commands/click_base.py +79 -0
  45. wl_cli/commands/config/__init__.py +1 -0
  46. wl_cli/commands/config/config.py +291 -0
  47. wl_cli/commands/debug/__init__.py +6 -0
  48. wl_cli/commands/debug/debug.py +309 -0
  49. wl_cli/commands/debug/debug_cache.py +86 -0
  50. wl_cli/commands/debug/debug_subagent.py +156 -0
  51. wl_cli/commands/debug/output_formatter.py +165 -0
  52. wl_cli/commands/debug/source_hasher.py +92 -0
  53. wl_cli/commands/format/__init__.py +13 -0
  54. wl_cli/commands/format/format_cache_adapter.py +125 -0
  55. wl_cli/commands/format/format_command.py +197 -0
  56. wl_cli/commands/format/format_coordinator.py +191 -0
  57. wl_cli/commands/format/format_executor.py +470 -0
  58. wl_cli/commands/format/format_outcome.py +44 -0
  59. wl_cli/commands/format/format_path_handler.py +1016 -0
  60. wl_cli/commands/format/format_progress.py +62 -0
  61. wl_cli/commands/format/format_reporter.py +31 -0
  62. wl_cli/commands/format/python_formatter.py +506 -0
  63. wl_cli/commands/format/rust_formatter.py +106 -0
  64. wl_cli/commands/gate/__init__.py +1 -0
  65. wl_cli/commands/gate/gate_command.py +862 -0
  66. wl_cli/commands/gate/preflight.py +445 -0
  67. wl_cli/commands/git/__init__.py +1 -0
  68. wl_cli/commands/git/config.py +169 -0
  69. wl_cli/commands/git/git_facts.py +60 -0
  70. wl_cli/commands/git/git_flow.py +86 -0
  71. wl_cli/commands/git/hook.py +112 -0
  72. wl_cli/commands/git/policy.py +216 -0
  73. wl_cli/commands/git/push_update.py +82 -0
  74. wl_cli/commands/initenv/__init__.py +1 -0
  75. wl_cli/commands/initenv/common/__init__.py +23 -0
  76. wl_cli/commands/initenv/common/check_git_bash.py +78 -0
  77. wl_cli/commands/initenv/common/error_handler.py +68 -0
  78. wl_cli/commands/initenv/common/exceptions.py +52 -0
  79. wl_cli/commands/initenv/components/__init__.py +4 -0
  80. wl_cli/commands/initenv/components/cargo_sync.py +240 -0
  81. wl_cli/commands/initenv/components/config_manager.py +106 -0
  82. wl_cli/commands/initenv/components/directory_utils.py +71 -0
  83. wl_cli/commands/initenv/components/git/__init__.py +9 -0
  84. wl_cli/commands/initenv/components/git/branch_creator.py +143 -0
  85. wl_cli/commands/initenv/components/git/git_flow_interface.py +24 -0
  86. wl_cli/commands/initenv/components/git/git_flow_manager.py +159 -0
  87. wl_cli/commands/initenv/components/git/git_lfs_manager.py +272 -0
  88. wl_cli/commands/initenv/components/git/git_utility.py +254 -0
  89. wl_cli/commands/initenv/components/git/gitignore_manager.py +53 -0
  90. wl_cli/commands/initenv/components/git_initializer.py +212 -0
  91. wl_cli/commands/initenv/components/i18n_manager.py +104 -0
  92. wl_cli/commands/initenv/components/log_manager.py +241 -0
  93. wl_cli/commands/initenv/components/project_structure/__init__.py +23 -0
  94. wl_cli/commands/initenv/components/project_structure/config_generator.py +360 -0
  95. wl_cli/commands/initenv/components/project_structure/directory_creator.py +184 -0
  96. wl_cli/commands/initenv/components/project_structure/hook_source.py +110 -0
  97. wl_cli/commands/initenv/components/project_structure/hooks_manager.py +995 -0
  98. wl_cli/commands/initenv/components/project_structure/readme_handler.py +56 -0
  99. wl_cli/commands/initenv/components/project_structure/rust_files_handler.py +30 -0
  100. wl_cli/commands/initenv/components/project_structure/setup_handler.py +103 -0
  101. wl_cli/commands/initenv/components/project_structure/sop_generator.py +73 -0
  102. wl_cli/commands/initenv/components/project_structure/template_copier.py +104 -0
  103. wl_cli/commands/initenv/components/project_structure/template_manager.py +195 -0
  104. wl_cli/commands/initenv/components/pyproject_generator.py +194 -0
  105. wl_cli/commands/initenv/components/rust_initializer.py +145 -0
  106. wl_cli/commands/initenv/components/symlink_cleaner.py +99 -0
  107. wl_cli/commands/initenv/components/symlink_manager.py +52 -0
  108. wl_cli/commands/initenv/components/venv_manager.py +118 -0
  109. wl_cli/commands/initenv/initenv.py +169 -0
  110. wl_cli/commands/initenv/services/__init__.py +11 -0
  111. wl_cli/commands/initenv/services/environment_initializer.py +288 -0
  112. wl_cli/commands/initenv/services/git_lfs_initializer.py +26 -0
  113. wl_cli/commands/initenv/services/project_structure_initializer.py +52 -0
  114. wl_cli/commands/lfs/README.md +221 -0
  115. wl_cli/commands/lfs/__init__.py +5 -0
  116. wl_cli/commands/lfs/cli.py +106 -0
  117. wl_cli/commands/lfs/config.py +164 -0
  118. wl_cli/commands/lfs/hook.py +61 -0
  119. wl_cli/commands/lfs/initializer.py +531 -0
  120. wl_cli/commands/lfs/manager.py +95 -0
  121. wl_cli/commands/lfs/policy.py +165 -0
  122. wl_cli/commands/lfs/watcher.py +294 -0
  123. wl_cli/commands/lint/__init__.py +1 -0
  124. wl_cli/commands/lint/lint_command.py +297 -0
  125. wl_cli/commands/lint/lint_executor.py +409 -0
  126. wl_cli/commands/lint/lint_reporter.py +579 -0
  127. wl_cli/commands/lint/lint_run.py +258 -0
  128. wl_cli/commands/lint/mypy_config.py +90 -0
  129. wl_cli/commands/registry.py +103 -0
  130. wl_cli/commands/self/__init__.py +7 -0
  131. wl_cli/commands/self/common/__init__.py +8 -0
  132. wl_cli/commands/self/common/capabilities.py +30 -0
  133. wl_cli/commands/self/common/config.py +27 -0
  134. wl_cli/commands/self/common/version_cache.py +85 -0
  135. wl_cli/commands/self/handlers/__init__.py +19 -0
  136. wl_cli/commands/self/handlers/base_handler.py +28 -0
  137. wl_cli/commands/self/handlers/install_handler.py +85 -0
  138. wl_cli/commands/self/handlers/status_handler.py +66 -0
  139. wl_cli/commands/self/handlers/uninstall_handler.py +58 -0
  140. wl_cli/commands/self/handlers/update_handler.py +102 -0
  141. wl_cli/commands/self/handlers/version_handler.py +16 -0
  142. wl_cli/commands/self/self_command.py +237 -0
  143. wl_cli/commands/self/services/__init__.py +15 -0
  144. wl_cli/commands/self/services/installation_manager.py +187 -0
  145. wl_cli/commands/self/services/local_installer.py +165 -0
  146. wl_cli/commands/self/services/pypi_updater.py +278 -0
  147. wl_cli/commands/self/services/windows_update.py +174 -0
  148. wl_cli/commands/testcommands/__init__.py +8 -0
  149. wl_cli/commands/testcommands/base_dependency_analyzer.py +198 -0
  150. wl_cli/commands/testcommands/batch_processor.py +278 -0
  151. wl_cli/commands/testcommands/cache_manager.py +1156 -0
  152. wl_cli/commands/testcommands/coverage_analyzer.py +955 -0
  153. wl_cli/commands/testcommands/coverage_cache_manager.py +183 -0
  154. wl_cli/commands/testcommands/progress_manager.py +79 -0
  155. wl_cli/commands/testcommands/python_env_detector.py +207 -0
  156. wl_cli/commands/testcommands/report_generator.py +101 -0
  157. wl_cli/commands/testcommands/single_test_executor.py +250 -0
  158. wl_cli/commands/testcommands/speed_checker.py +118 -0
  159. wl_cli/commands/testcommands/test_command_builder.py +165 -0
  160. wl_cli/commands/testcommands/test_commands.py +786 -0
  161. wl_cli/commands/testcommands/test_reporter.py +181 -0
  162. wl_cli/commands/testcommands/test_result_handler.py +96 -0
  163. wl_cli/commands/testcommands/test_runner.py +1343 -0
  164. wl_cli/commands/testcommands/test_utils.py +197 -0
  165. wl_cli/commands/testcommands/utils/__init__.py +17 -0
  166. wl_cli/commands/testcommands/utils/coverage_parser.py +98 -0
  167. wl_cli/commands/testcommands/utils/env_info.py +36 -0
  168. wl_cli/commands/testcommands/utils/junit_parser.py +75 -0
  169. wl_cli/commands/testcommands/utils/markdown_utils.py +20 -0
  170. wl_cli/commands/testcommands/utils/project_root.py +61 -0
  171. wl_cli/commands/update/__init__.py +9 -0
  172. wl_cli/commands/update/services/__init__.py +21 -0
  173. wl_cli/commands/update/services/backup_manager.py +207 -0
  174. wl_cli/commands/update/services/component_manager.py +83 -0
  175. wl_cli/commands/update/services/conflict_resolver.py +189 -0
  176. wl_cli/commands/update/services/vendors_updater.py +85 -0
  177. wl_cli/commands/update/services/version_tracker.py +127 -0
  178. wl_cli/commands/update/update_command.py +270 -0
  179. wl_cli/commands/update/utils/__init__.py +3 -0
  180. wl_cli/commands/version/__init__.py +9 -0
  181. wl_cli/commands/version/version_command.py +155 -0
  182. wl_cli/commands/workflow.py +477 -0
  183. wl_cli/env/__init__.py +5 -0
  184. wl_cli/env/cli.py +167 -0
  185. wl_cli/env_status.py +157 -0
  186. wl_cli/envelope.py +91 -0
  187. wl_cli/exceptions.py +85 -0
  188. wl_cli/lib/__init__.py +3 -0
  189. wl_cli/lib/rust_utils.py +35 -0
  190. wl_cli/mcp/__init__.py +6 -0
  191. wl_cli/mcp/capabilities.py +19 -0
  192. wl_cli/mcp/cli.py +22 -0
  193. wl_cli/mcp/gitlab.py +553 -0
  194. wl_cli/mcp/k8s.py +344 -0
  195. wl_cli/mcp/protocol.py +159 -0
  196. wl_cli/platform.py +139 -0
  197. wl_cli/quality/__init__.py +18 -0
  198. wl_cli/utils/__init__.py +7 -0
  199. wl_cli/utils/cache.py +491 -0
  200. wl_cli/utils/cache_ledger.py +355 -0
  201. wl_cli/utils/cache_store.py +546 -0
  202. wl_cli/utils/config/__init__.py +27 -0
  203. wl_cli/utils/config/config_loader.py +163 -0
  204. wl_cli/utils/config/core.py +862 -0
  205. wl_cli/utils/config/defaults.py +142 -0
  206. wl_cli/utils/config/env_exporter.py +86 -0
  207. wl_cli/utils/config/env_parser.py +108 -0
  208. wl_cli/utils/config/validation.py +280 -0
  209. wl_cli/utils/error_handler.py +165 -0
  210. wl_cli/utils/file_copier.py +56 -0
  211. wl_cli/utils/file_operations.py +506 -0
  212. wl_cli/utils/language_utils.py +122 -0
  213. wl_cli/utils/lazy_import.py +240 -0
  214. wl_cli/utils/log_levels.py +35 -0
  215. wl_cli/utils/log_rotators.py +74 -0
  216. wl_cli/utils/logging.py +458 -0
  217. wl_cli/utils/output_context.py +142 -0
  218. wl_cli/utils/performance/__init__.py +1 -0
  219. wl_cli/utils/performance/decorators.py +111 -0
  220. wl_cli/utils/performance/exporter.py +38 -0
  221. wl_cli/utils/performance/models.py +16 -0
  222. wl_cli/utils/performance/monitor.py +161 -0
  223. wl_cli/utils/performance_monitor.py +32 -0
  224. wl_cli/utils/platform_adapter.py +131 -0
  225. wl_cli/utils/pre_commit.py +266 -0
  226. wl_cli/utils/pre_commit_config.py +72 -0
  227. wl_cli/utils/root.py +521 -0
  228. wl_cli/utils/root_fallback.py +256 -0
  229. wl_cli/utils/run_evidence.py +236 -0
  230. wl_cli/utils/structured_logger.py +18 -0
  231. wl_cli/utils/structured_logging/__init__.py +19 -0
  232. wl_cli/utils/structured_logging/config.py +337 -0
  233. wl_cli/utils/structured_logging/core.py +177 -0
  234. wl_cli/utils/structured_logging/env.py +56 -0
  235. wl_cli/utils/structured_logging/filtering.py +75 -0
  236. wl_cli/utils/structured_logging/filters.py +55 -0
  237. wl_cli/utils/structured_logging/formatters.py +251 -0
  238. wl_cli/utils/structured_logging/handlers.py +308 -0
  239. wl_cli/utils/structured_logging/logger.py +354 -0
  240. wl_cli/utils/subprocess.py +71 -0
  241. wl_cli/utils/subprocess_utils.py +260 -0
  242. wl_cli/utils/test_utils/__init__.py +5 -0
  243. wl_cli/utils/test_utils/test_filter.py +36 -0
  244. wl_cli/utils/uv_run.py +79 -0
  245. wl_cli/utils/uv_tool.py +117 -0
  246. wl_cli/utils/uv_tree.py +71 -0
  247. wl_cli/utils/uv_workspace.py +174 -0
  248. wl_cli/utils/version/__init__.py +15 -0
  249. wl_cli/utils/version/pypi_version_checker.py +95 -0
  250. wl_cli/utils/version/version_comparator.py +133 -0
  251. wl_cli/utils/version/version_detectors.py +312 -0
  252. wl_cli/utils/version/version_service.py +131 -0
  253. wl_cli/utils/version/version_updater.py +198 -0
  254. wl_cli/utils/wl_dir_updater.py +1440 -0
  255. wl_cli/utils/workspace_detector/__init__.py +28 -0
  256. wl_cli/utils/workspace_detector/core.py +161 -0
  257. wl_cli/utils/workspace_detector/detection_strategies.py +107 -0
  258. wl_cli/utils/workspace_detector/exceptions.py +5 -0
  259. wl_cli/utils/workspace_detector/types.py +17 -0
  260. wl_cli/utils/workspace_detector/venv_resolver.py +84 -0
  261. wl_cli/vendors/config/.codespellrc +2 -0
  262. wl_cli/vendors/config/dependencies.toml +10 -0
  263. wl_cli/vendors/config/mypy.ini +62 -0
  264. wl_cli/vendors/config/pyproject_defaults.toml +28 -0
  265. wl_cli/vendors/config/template_config.toml +24 -0
  266. wl_cli/vendors/config/tool_configs.toml +76 -0
  267. wl_cli/vendors/git/.gitignore +122 -0
  268. wl_cli/vendors/git/.pre-commit-config.yaml +37 -0
  269. wl_cli/vendors/hooks/applypatch-msg.sample +15 -0
  270. wl_cli/vendors/hooks/commit-msg.sample +24 -0
  271. wl_cli/vendors/hooks/fsmonitor-watchman.sample +174 -0
  272. wl_cli/vendors/hooks/post-update.sample +8 -0
  273. wl_cli/vendors/hooks/pre-applypatch.sample +14 -0
  274. wl_cli/vendors/hooks/pre-commit +45 -0
  275. wl_cli/vendors/hooks/pre-commit.legacy +31 -0
  276. wl_cli/vendors/hooks/pre-commit.sample +49 -0
  277. wl_cli/vendors/hooks/pre-merge-commit.sample +13 -0
  278. wl_cli/vendors/hooks/pre-push.sample +60 -0
  279. wl_cli/vendors/hooks/pre-rebase.sample +169 -0
  280. wl_cli/vendors/hooks/pre-receive.sample +24 -0
  281. wl_cli/vendors/hooks/prepare-commit-msg.sample +42 -0
  282. wl_cli/vendors/hooks/push-to-checkout.sample +78 -0
  283. wl_cli/vendors/hooks/sendemail-validate.sample +77 -0
  284. wl_cli/vendors/hooks/unix/applypatch-msg.sample +15 -0
  285. wl_cli/vendors/hooks/unix/check_lockfile.py +58 -0
  286. wl_cli/vendors/hooks/unix/check_lockfile.sh +36 -0
  287. wl_cli/vendors/hooks/unix/commit-msg.sample +24 -0
  288. wl_cli/vendors/hooks/unix/fsmonitor-watchman.sample +174 -0
  289. wl_cli/vendors/hooks/unix/post-update.sample +8 -0
  290. wl_cli/vendors/hooks/unix/pre-applypatch.sample +14 -0
  291. wl_cli/vendors/hooks/unix/pre-commit +190 -0
  292. wl_cli/vendors/hooks/unix/pre-commit.legacy +31 -0
  293. wl_cli/vendors/hooks/unix/pre-commit.py +81 -0
  294. wl_cli/vendors/hooks/unix/pre-commit.sample +49 -0
  295. wl_cli/vendors/hooks/unix/pre-merge-commit.sample +13 -0
  296. wl_cli/vendors/hooks/unix/pre-push +417 -0
  297. wl_cli/vendors/hooks/unix/pre-push.sample +53 -0
  298. wl_cli/vendors/hooks/unix/pre-rebase.sample +169 -0
  299. wl_cli/vendors/hooks/unix/pre-receive.sample +24 -0
  300. wl_cli/vendors/hooks/unix/prepare-commit-msg.sample +42 -0
  301. wl_cli/vendors/hooks/update.sample +128 -0
  302. wl_cli/vendors/hooks/win/work.bat +63 -0
  303. wl_cli/vendors/project/.gitignore +90 -0
  304. wl_cli/vendors/project/.python-version +1 -0
  305. wl_cli/vendors/project/LICENSE +21 -0
  306. wl_cli/vendors/project/README.md +0 -0
  307. wl_cli/vendors/project/pyproject.toml +27 -0
  308. wl_cli/vendors/project/pyproject_no_workspace.toml +16 -0
  309. wl_cli/vendors/project/pyproject_workspace.toml +14 -0
  310. wl_cli/vendors/readme/README.md +120 -0
  311. wl_cli/vendors/rust/Cargo.toml +14 -0
  312. wl_cli/vendors/rust/lib/__init__.py +3 -0
  313. wl_cli/vendors/rust/lib/rust_utils.py +31 -0
  314. wl_cli/vendors/rust/src/lib.rs +13 -0
  315. wl_cli/vendors/rust/src/main.rs +3 -0
  316. wl_cli/vendors/rust/tests/test_rust_fallback.py +40 -0
  317. wl_cli/vendors/sop_doc/git-dev-sop.md +220 -0
  318. wl_cli/vendors/sop_doc/industrial-standard-sop.md +145 -0
  319. wl_cli/vendors/sop_doc/python-sop.md +342 -0
  320. 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