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,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,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)
|