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,221 @@
1
+ # LFS Lock 模块
2
+
3
+ 对标 `git-flow` 子系统的 **Git LFS 文件锁协作工作流**。解决二进制文件(`.sldprt`、`.dwg` 等)无法被 Git merge 的问题 —— 通过全局文件锁确保同一时刻只有一人编辑。
4
+
5
+ ## 架构
6
+
7
+ ```
8
+ CLI (cli.py) Pre-push Hook (hook.py)
9
+ │ │
10
+ ▼ ▼
11
+ LfsLockPolicyValidator (policy.py) ← Pre-push fallback auto-lock (v2)
12
+ │
13
+ ├── LfsLockConfig (config.py) ← 读取 .wl/lfs-lock.yml (v2: auto_lock/auto_unlock)
14
+ └── LfsLockManager (manager.py) ← 封装 git lfs 命令 (v2: lock_files/unlock_files)
15
+
16
+ wl lfs watch (watcher.py) ← v2: watchdog 文件监听 + auto lock/unlock
17
+ ```
18
+
19
+ | 层 | 文件 | 职责 |
20
+ |----|------|------|
21
+ | CLI | `cli.py` | Click 命令组:`check`、`lock`、`unlock` |
22
+ | Hook | `hook.py` | pre-push hook 集成:策略验证 + 失败格式化 |
23
+ | Policy | `policy.py` | 策略引擎:strict/warn/off 三种模式 |
24
+ | Config | `config.py` | 配置加载:读 `.wl/lfs-lock.yml`,合并默认值 |
25
+ | Manager | `manager.py` | 命令封装:`git lfs lock/unlock/locks` + `git check-attr` |
26
+ | Watcher | `watcher.py` | watchdog 文件监听:自动 lock/unlock + pre-push 检测 |
27
+
28
+ ## 命令
29
+
30
+ ```bash
31
+ wl lfs check # 推送前预检:变更的 LFS 文件是否全部已 lock
32
+ wl lfs lock <file> # 锁定文件(全局锁)
33
+ wl lfs unlock <file> # 解锁文件,--force 管理员强制解锁
34
+ ```
35
+
36
+ ## 工作流
37
+
38
+ ```
39
+ wl lfs lock model.sldprt
40
+ 开始编辑 ──────────────────────────────────────────► 编辑 ──► git push
41
+ │
42
+ pre-push hook ──┤
43
+ (wl lfs check) │
44
+ ✓ ✗
45
+ 推送成功 被阻止
46
+
47
+ wl init lfs --with-locks 一键部署上述全部
48
+ ```
49
+
50
+ ## 三种模式
51
+
52
+ | 模式 | 未锁文件 | 被他人锁定的文件 | 适用场景 |
53
+ |------|---------|-----------------|---------|
54
+ | `strict` | BLOCKED | BLOCKED | 正式团队,零容忍 |
55
+ | `warn` | WARN(放行) | BLOCKED | 过渡期,逐步推进 |
56
+ | `off` | OK(放行) | OK(放行) | 关闭检查 |
57
+
58
+ 配置在 `.wl/lfs-lock.yml` 中修改:
59
+
60
+ ```yaml
61
+ lock:
62
+ mode: strict # strict | warn | off
63
+ scope: global # 仅支持全局锁
64
+ checks:
65
+ pre_commit: false
66
+ pre_push: true # 推送时自动检查
67
+ patterns:
68
+ locked_by_self: false # 是否将本人锁定也视为阻断
69
+ notify:
70
+ unlocked_files: true
71
+ force_unlock_token: "" # 管理员强制解锁令牌
72
+ stale_lock_warning_days: 7
73
+ exempt_patterns: [] # 豁免检查的文件模式
74
+ ```
75
+
76
+ ## Pre-push Hook 集成
77
+
78
+ `wl init lfs --with-locks` 会自动在 pre-push hook 中注入 LFS 锁检查。推送时:
79
+
80
+ 1. 先执行 Git Flow 策略检查(如果已配置)
81
+ 2. 再执行 LFS Lock 检查
82
+ 3. 任一失败则阻止推送
83
+
84
+ 被阻止时输出示例:
85
+
86
+ ```
87
+ LFS Lock check: BLOCKED
88
+ Unlocked LFS files: models/part.sldprt
89
+ Locked by others: drawings/design.dwg
90
+ Config: .wl/lfs-lock.yml
91
+ Run `wl lfs check` for details.
92
+ ```
93
+
94
+ ## 清理
95
+
96
+ ```bash
97
+ wl clean lfs # 移除 .wl/lfs-lock.yml
98
+ ```
99
+
100
+ ## 模块依赖
101
+
102
+ ```
103
+ wl_cli.commands.lfs
104
+ ├── .config → LfsLockConfig (YAML 配置)
105
+ ├── .manager → LfsLockManager (git 命令)
106
+ ├── .policy → LfsLockPolicyValidator (策略)
107
+ ├── .hook → validate_lfs_locks (hook 集成)
108
+ ├── .cli → Click 命令注册
109
+ └── .initializer → LfsLockInitializer (写入 .wl/lfs-lock.yml 默认配置)
110
+
111
+ wl_cli.commands.clean.lfs
112
+ └── _clean_lock_config
113
+ ```
114
+
115
+ ## 设计决策
116
+
117
+ - **全局锁**:二进制文件不可 merge,分支级锁无意义
118
+ - **`git check-attr`**:不依赖 `git lfs` 已安装即可检测 LFS 跟踪
119
+ - **10 秒超时**:`git lfs locks --json` 调用设置超时,避免 LFS 服务器不可用时 hang
120
+ - **幂等部署**:`LfsLockInitializer` 不会覆盖已有的 `.wl/lfs-lock.yml`
121
+
122
+ ## v2: Watch & Auto Lock/Unlock
123
+
124
+ `wl lfs watch` is a foreground process that monitors your LFS-tracked files:
125
+
126
+ - **Auto-lock on edit** — when you modify an LFS file, it's locked automatically
127
+ - **Auto-unlock on push** — after a successful push, locked files are unlocked
128
+ - **Pre-push fallback** — if watch isn't running, the pre-push hook auto-locks
129
+ files before allowing the push
130
+
131
+ ### Quick Start
132
+
133
+ ```bash
134
+ # 1. Deploy lock workflow
135
+ wl init lfs --with-locks
136
+
137
+ # 2. Start the watcher in your project directory
138
+ wl lfs watch
139
+
140
+ # 3. Edit your CAD files normally — they auto-lock
141
+ # 4. Push as usual — locked files auto-unlock after push
142
+ # 5. Ctrl-C to stop watching
143
+ ```
144
+
145
+ ### wl lfs watch
146
+
147
+ ```
148
+ $ wl lfs watch
149
+ LFS Watch started. Ctrl-C to stop.
150
+ Scanning 12 LFS-tracked files...
151
+ models/bracket.sldprt → modified, locking... locked
152
+ models/housing.dwg → already locked by you
153
+ Watch: 12 files monitored. Waiting for changes...
154
+ [14:32:01] models/bracket.sldprt modified → auto-locked
155
+ [14:35:18] push detected (branch: main) → auto-unlocked models/bracket.sldprt
156
+ ^C
157
+ LFS Watch stopped. Session: 2 lock(s), 1 unlock(s), 0 skipped.
158
+ ```
159
+
160
+ ### Pre-push Fallback
161
+
162
+ If `wl lfs watch` is not running, the pre-push hook still protects you:
163
+
164
+ 1. `git push` triggers the pre-push hook
165
+ 2. Hook detects unlocked LFS files
166
+ 3. If `auto_lock.fallback: true` (default), files are auto-locked
167
+ 4. Push proceeds if all files were locked successfully
168
+ 5. In CI environments (CI=true), auto-lock is skipped — strict validation only
169
+
170
+ ### Configuration (.wl/lfs-lock.yml)
171
+
172
+ ```yaml
173
+ lock:
174
+ mode: strict # strict | warn | off
175
+
176
+ auto_lock:
177
+ fallback: true # pre-push auto-lock fallback
178
+ skip_locked_by_others: true
179
+
180
+ auto_unlock:
181
+ on_push: true # auto-unlock after push
182
+ retry_count: 3 # unlock retry attempts
183
+ retry_delay: 2 # seconds between retries
184
+
185
+ checks:
186
+ pre_push: true
187
+ ```
188
+
189
+ ### Troubleshooting
190
+
191
+ | Symptom | Cause | Fix |
192
+ |---------|-------|-----|
193
+ | watchdog not installed | lfs extra missing | `pip install "py_wlcli[lfs]"` or `uv pip install "py_wlcli[lfs]"` |
194
+ | Watch doesn't detect file changes | NFS/SMB/WSL filesystem | Fallback: pre-push auto-lock still works |
195
+ | File not auto-locked | File not in .gitattributes | Run `wl init lfs` with tracking patterns |
196
+ | "Already running" error | Stale PID file | Delete `.wl/lfs-watch.pid` |
197
+
198
+ ## Windows 一键工作会话 (work.bat)
199
+
200
+ 在 Windows 平台上,`wl init lfs --with-locks` 或 `wl lfs init` 会在项目根目录生成 `work.bat`。
201
+
202
+ 双击 `work.bat` 即可启动完整的 LFS 工作会话:
203
+ 1. 自动拉取最新代码(`git pull`)
204
+ 2. 启动 `wl lfs watch`(自动 lock/unlock)
205
+ 3. 退出时提示是否 push(Y 确认后自动 add + commit + push)
206
+
207
+ **使用方法:**
208
+ ```cmd
209
+ :: 初始化项目(自动生成 work.bat)
210
+ wl init project
211
+ wl init lfs --with-locks
212
+
213
+ :: 双击 work.bat 开始工作
214
+ :: 保持命令行窗口打开,在 CAD 软件中编辑文件
215
+ :: 完成后关闭窗口,按 Y 确认 push
216
+ ```
217
+
218
+ **注意:**
219
+ - 命令行窗口必须保持打开,`wl lfs watch` 才能持续工作
220
+ - 如果上次未正常退出,脚本会自动清理残留的 PID 文件
221
+ - 不想 push 时按任意非 Y 键跳过
@@ -0,0 +1,5 @@
1
+ """LFS Lock command module."""
2
+
3
+ from .watcher import LfsWatcher
4
+
5
+ __all__ = ["LfsWatcher"]
@@ -0,0 +1,106 @@
1
+ """LFS Lock CLI commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import click
6
+
7
+ from ...commands.git.git_facts import get_changed_files
8
+ from .config import LfsLockConfig
9
+ from .manager import LfsLockManager
10
+ from .policy import LfsLockPolicyValidator
11
+
12
+
13
+ @click.group(name="lfs")
14
+ def lfs_command() -> None:
15
+ """Git LFS lock workflow commands."""
16
+
17
+
18
+ @lfs_command.command(name="check")
19
+ def check_command() -> None:
20
+ """Check that all changed LFS files are locked before push."""
21
+ config = LfsLockConfig()
22
+ source = "main"
23
+ changed_files = get_changed_files(source)
24
+
25
+ if not changed_files:
26
+ click.echo("LFS Lock check: OK (no LFS files changed)")
27
+ return
28
+
29
+ validator = LfsLockPolicyValidator(config)
30
+ result = validator.validate(changed_files)
31
+
32
+ click.echo(f"LFS Lock check: {result.status}")
33
+ if result.locked_files:
34
+ click.echo(f"Locked: {', '.join(sorted(result.locked_files))}")
35
+ if result.unlocked_files:
36
+ click.echo(f"Unlocked: {', '.join(sorted(result.unlocked_files))}")
37
+ if result.locked_by_others:
38
+ click.echo(f"Locked by others: {', '.join(sorted(result.locked_by_others))}")
39
+ click.echo(f"Config: {result.config_source}")
40
+
41
+ if not result.ok:
42
+ raise click.ClickException("LFS Lock policy check failed")
43
+
44
+
45
+ @lfs_command.command(name="lock")
46
+ @click.argument("file")
47
+ def lock_command(file: str) -> None:
48
+ """Lock an LFS file."""
49
+ manager = LfsLockManager()
50
+ if manager.lock(file):
51
+ click.echo(f"Locked: {file}")
52
+ else:
53
+ raise click.ClickException(f"Failed to lock: {file}")
54
+
55
+
56
+ @lfs_command.command(name="unlock")
57
+ @click.argument("file")
58
+ @click.option("--force", is_flag=True, help="Force unlock (admin override)")
59
+ def unlock_command(file: str, force: bool) -> None:
60
+ """Unlock an LFS file."""
61
+ manager = LfsLockManager()
62
+ if manager.unlock(file, force=force):
63
+ click.echo(f"Unlocked: {file}")
64
+ else:
65
+ raise click.ClickException(f"Failed to unlock: {file}")
66
+
67
+
68
+ @lfs_command.command(name="init")
69
+ def init_command() -> None:
70
+ """Initialize Git LFS with lock workflow and work.bat.
71
+
72
+ Equivalent to: wl init lfs --with-locks
73
+ """
74
+ from ...commands.initenv.services.git_lfs_initializer import GitLfsInitializer
75
+ from .initializer import LfsLockInitializer, lfs_lock_init_failure_message
76
+
77
+ lock_initializer = LfsLockInitializer()
78
+ # Probe before Git LFS touches the repo: that step installs filters/hooks
79
+ # and would otherwise leave a half-initialized project behind when the
80
+ # lock step then reports the missing .wl directory (#198).
81
+ if not lock_initializer.wl_dir_ready():
82
+ raise click.ClickException(
83
+ lfs_lock_init_failure_message(lock_initializer.project_root)
84
+ )
85
+
86
+ GitLfsInitializer().initialize()
87
+ if lock_initializer.initialize() is False:
88
+ raise click.ClickException(
89
+ lfs_lock_init_failure_message(lock_initializer.project_root)
90
+ )
91
+
92
+
93
+ @lfs_command.command(name="watch")
94
+ @click.option("--verbose", is_flag=True, help="Show detailed event output")
95
+ def watch_command(verbose: bool) -> None:
96
+ """Start LFS file watcher (foreground process, Ctrl-C to exit).
97
+
98
+ Automatically locks LFS-tracked files when they are modified,
99
+ and unlocks them when pushed to the remote.
100
+ """
101
+ from .watcher import LfsWatcher
102
+
103
+ watcher = LfsWatcher(verbose=verbose)
104
+ exit_code = watcher.run()
105
+ if exit_code != 0:
106
+ raise click.ClickException("LFS watch failed")
@@ -0,0 +1,164 @@
1
+ """LFS Lock configuration loading."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, cast
6
+
7
+ try:
8
+ from yaml import YAMLError
9
+ except ImportError:
10
+ YAMLError = ValueError
11
+
12
+ from ...utils.root import RootKind
13
+ from ...utils.root_fallback import current_root
14
+
15
+ if TYPE_CHECKING:
16
+ from pathlib import Path
17
+
18
+
19
+ class LfsLockConfigError(ValueError):
20
+ """Raised when LFS Lock configuration is malformed."""
21
+
22
+
23
+ class LfsLockConfig:
24
+ """Loads and provides access to .wl/lfs-lock.yml configuration."""
25
+
26
+ DEFAULT_CONFIG: dict = {
27
+ "lock": {
28
+ "mode": "strict",
29
+ "scope": "global",
30
+ },
31
+ "auto_lock": {
32
+ "fallback": True,
33
+ "skip_locked_by_others": True,
34
+ },
35
+ "auto_unlock": {
36
+ "on_push": True,
37
+ "retry_count": 3,
38
+ "retry_delay": 2,
39
+ },
40
+ "checks": {
41
+ "pre_commit": False,
42
+ "pre_push": True,
43
+ },
44
+ "patterns": {
45
+ "locked_by_self": False,
46
+ },
47
+ "notify": {
48
+ "unlocked_files": True,
49
+ },
50
+ "force_unlock_token": "",
51
+ "stale_lock_warning_days": 7,
52
+ "exempt_patterns": [],
53
+ }
54
+
55
+ def __init__(self, config_path: Path | None = None) -> None:
56
+ # Root-anchored (#125): the *project* root's ``.wl`` -- the kind the LFS
57
+ # initializer and watcher already use -- not the process CWD's, so
58
+ # ``wl lfs check`` from a subdirectory reads the root's policy instead
59
+ # of silently falling back to built-in defaults.
60
+ self.config_path = config_path or (
61
+ current_root(RootKind.PROJECT).wl_dir() / "lfs-lock.yml"
62
+ )
63
+ self._config = self._load_config()
64
+
65
+ @property
66
+ def config_source(self) -> str:
67
+ if self.config_path.exists():
68
+ return str(self.config_path)
69
+ return "built-in defaults"
70
+
71
+ @property
72
+ def mode(self) -> str:
73
+ return cast("str", self._config["lock"]["mode"])
74
+
75
+ @property
76
+ def pre_push(self) -> bool:
77
+ return cast("bool", self._config["checks"]["pre_push"])
78
+
79
+ @property
80
+ def locked_by_self(self) -> bool:
81
+ return cast("bool", self._config["patterns"]["locked_by_self"])
82
+
83
+ @property
84
+ def force_unlock_token(self) -> str:
85
+ return cast("str", self._config["force_unlock_token"])
86
+
87
+ @property
88
+ def stale_lock_warning_days(self) -> int:
89
+ return cast("int", self._config["stale_lock_warning_days"])
90
+
91
+ @property
92
+ def server_mode(self) -> str:
93
+ return cast("str", self._config.get("server_mode", "default"))
94
+
95
+ @property
96
+ def exempt_patterns(self) -> list[str]:
97
+ return list(self._config["exempt_patterns"])
98
+
99
+ @property
100
+ def auto_lock_fallback(self) -> bool:
101
+ return cast("bool", self._config["auto_lock"]["fallback"])
102
+
103
+ @property
104
+ def auto_lock_skip_locked_by_others(self) -> bool:
105
+ return cast("bool", self._config["auto_lock"]["skip_locked_by_others"])
106
+
107
+ @property
108
+ def auto_unlock_on_push(self) -> bool:
109
+ return cast("bool", self._config["auto_unlock"]["on_push"])
110
+
111
+ @property
112
+ def auto_unlock_retry_count(self) -> int:
113
+ return cast("int", self._config["auto_unlock"]["retry_count"])
114
+
115
+ @property
116
+ def auto_unlock_retry_delay(self) -> int:
117
+ return cast("int", self._config["auto_unlock"]["retry_delay"])
118
+
119
+ def _load_config(self) -> dict:
120
+ if not self.config_path.exists():
121
+ return dict(self.DEFAULT_CONFIG)
122
+
123
+ try:
124
+ import yaml
125
+
126
+ loaded = yaml.safe_load(self.config_path.read_text(encoding="utf-8"))
127
+ except ImportError:
128
+ # PyYAML not available — fall back to defaults
129
+ import logging
130
+
131
+ logging.getLogger(__name__).warning(
132
+ "PyYAML is not installed, using default LFS Lock config"
133
+ )
134
+ return dict(self.DEFAULT_CONFIG)
135
+ except YAMLError as exc:
136
+ raise LfsLockConfigError(
137
+ f"{self.config_path}: invalid YAML: {exc}"
138
+ ) from exc
139
+
140
+ if loaded is None:
141
+ return dict(self.DEFAULT_CONFIG)
142
+ if not isinstance(loaded, dict):
143
+ raise LfsLockConfigError(f"{self.config_path}: config must be a mapping")
144
+
145
+ return self._merge_config(loaded)
146
+
147
+ def _merge_config(self, loaded: dict) -> dict:
148
+ config = dict(self.DEFAULT_CONFIG)
149
+ # Merge nested sections
150
+ for section in (
151
+ "lock",
152
+ "auto_lock",
153
+ "auto_unlock",
154
+ "checks",
155
+ "patterns",
156
+ "notify",
157
+ ):
158
+ if section in loaded and isinstance(loaded[section], dict):
159
+ config[section] = {**config[section], **loaded[section]}
160
+ # Merge top-level keys
161
+ for key in ("force_unlock_token", "stale_lock_warning_days", "exempt_patterns"):
162
+ if key in loaded:
163
+ config[key] = loaded[key]
164
+ return config
@@ -0,0 +1,61 @@
1
+ """Pre-push hook helpers for LFS lock policy validation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .config import LfsLockConfig
6
+ from .policy import LfsLockPolicyValidator, LockCheckResult
7
+
8
+
9
+ def validate_lfs_locks(changed_files: frozenset[str]) -> LockCheckResult | None:
10
+ """Run LFS lock policy validation against changed files.
11
+
12
+ Returns None if the module cannot be loaded (graceful degradation).
13
+ Returns LockCheckResult with ok=True/False otherwise.
14
+ """
15
+ try:
16
+ config = LfsLockConfig()
17
+ except Exception:
18
+ return None
19
+
20
+ if not changed_files:
21
+ return LockCheckResult(
22
+ ok=True,
23
+ status="OK",
24
+ config_source=config.config_source,
25
+ )
26
+
27
+ validator = LfsLockPolicyValidator(config)
28
+ return validator.validate(changed_files)
29
+
30
+
31
+ def validate_lfs_locks_with_fallback(
32
+ changed_files: frozenset[str],
33
+ ) -> LockCheckResult | None:
34
+ """Run LFS lock check with auto-lock fallback.
35
+
36
+ If files are unlocked, attempt to auto-lock them before blocking.
37
+ Requires config.auto_lock.fallback = true (default).
38
+ In CI environments, falls back to strict validation (no auto-lock).
39
+ """
40
+ try:
41
+ config = LfsLockConfig()
42
+ except Exception:
43
+ return None
44
+
45
+ if not changed_files:
46
+ return LockCheckResult(ok=True, status="OK", config_source=config.config_source)
47
+
48
+ validator = LfsLockPolicyValidator(config)
49
+ return validator.validate_with_fallback(changed_files)
50
+
51
+
52
+ def format_lfs_hook_failure(result: LockCheckResult) -> str:
53
+ """Format a LockCheckResult for pre-push hook error output."""
54
+ lines = [f"LFS Lock check: {result.status}"]
55
+ if result.unlocked_files:
56
+ lines.append("Unlocked LFS files: " + ", ".join(sorted(result.unlocked_files)))
57
+ if result.locked_by_others:
58
+ lines.append("Locked by others: " + ", ".join(sorted(result.locked_by_others)))
59
+ lines.append(f"Config: {result.config_source}")
60
+ lines.append("Run `wl lfs check` for details.")
61
+ return "\n".join(lines)