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,531 @@
|
|
|
1
|
+
"""LFS Lock workflow initialization (moved from initenv/services)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import shutil
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from ...utils.platform_adapter import PlatformAdapter
|
|
11
|
+
from ...utils.root import Root, RootKind
|
|
12
|
+
from ...utils.root_fallback import current_root
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def lfs_lock_init_failure_message(project_root: Path | str) -> str:
|
|
16
|
+
"""Return the bilingual error text for a missing ``.wl`` directory.
|
|
17
|
+
|
|
18
|
+
``LfsLockInitializer.initialize()`` reports a project that was never
|
|
19
|
+
initialized by returning False; every caller must surface that as a failed
|
|
20
|
+
command instead of printing success (#194).
|
|
21
|
+
"""
|
|
22
|
+
return (
|
|
23
|
+
"LFS lock workflow was not initialized: no .wl directory at "
|
|
24
|
+
f"{project_root}. Run 'wl init env' first.\n"
|
|
25
|
+
f"LFS 锁工作流未初始化:{project_root} 下没有 .wl 目录,请先运行 "
|
|
26
|
+
"'wl init env'。"
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class LfsLockInitializer:
|
|
31
|
+
"""Coordinates the deployment of LFS lock workflow configuration."""
|
|
32
|
+
|
|
33
|
+
def __init__(self, project_root: Path | None = None) -> None:
|
|
34
|
+
# Root-anchored (#125): the *project* root the CWD belongs to, not the
|
|
35
|
+
# CWD itself, so ``wl init lfs`` from a subdirectory writes the lock
|
|
36
|
+
# config into the root's ``.wl`` (see ``initialize``) instead of
|
|
37
|
+
# silently no-op'ing because the subdirectory has no ``.wl``.
|
|
38
|
+
self.project_root = project_root or current_root(RootKind.PROJECT).path
|
|
39
|
+
self._wl_python_cache: str | None = None
|
|
40
|
+
|
|
41
|
+
def _wl_dir(self) -> Path:
|
|
42
|
+
"""Return the root-anchored ``.wl`` directory of this project (#125)."""
|
|
43
|
+
return Root(self.project_root, RootKind.PROJECT).wl_dir()
|
|
44
|
+
|
|
45
|
+
def wl_dir_ready(self) -> bool:
|
|
46
|
+
"""Return True when the project has a ``.wl`` directory.
|
|
47
|
+
|
|
48
|
+
Side-effect free, so callers can probe the precondition *before*
|
|
49
|
+
anything that mutates the repository runs (installing hooks,
|
|
50
|
+
rewriting config, deploying ``work.bat``). ``initialize()`` re-checks
|
|
51
|
+
because the directory could disappear between the probe and the
|
|
52
|
+
write (#198).
|
|
53
|
+
"""
|
|
54
|
+
return self._wl_dir().is_dir()
|
|
55
|
+
|
|
56
|
+
def _run_uv_install(self, python_path: str, package_spec: str) -> bool:
|
|
57
|
+
"""Try to install a package using uv pip install --python."""
|
|
58
|
+
uv_cmd = shutil.which("uv") or "uv"
|
|
59
|
+
try:
|
|
60
|
+
result = subprocess.run(
|
|
61
|
+
[uv_cmd, "pip", "install", "--python", python_path, package_spec],
|
|
62
|
+
capture_output=True,
|
|
63
|
+
text=True,
|
|
64
|
+
timeout=120,
|
|
65
|
+
)
|
|
66
|
+
return result.returncode == 0
|
|
67
|
+
except (
|
|
68
|
+
subprocess.CalledProcessError,
|
|
69
|
+
FileNotFoundError,
|
|
70
|
+
subprocess.TimeoutExpired,
|
|
71
|
+
):
|
|
72
|
+
return False
|
|
73
|
+
|
|
74
|
+
def _run_pip_install(self, python_path: str, package_spec: str) -> bool:
|
|
75
|
+
"""Try to install a package using python -m pip install."""
|
|
76
|
+
try:
|
|
77
|
+
result = subprocess.run(
|
|
78
|
+
[python_path, "-m", "pip", "install", package_spec],
|
|
79
|
+
capture_output=True,
|
|
80
|
+
text=True,
|
|
81
|
+
timeout=120,
|
|
82
|
+
)
|
|
83
|
+
return result.returncode == 0
|
|
84
|
+
except (
|
|
85
|
+
subprocess.CalledProcessError,
|
|
86
|
+
FileNotFoundError,
|
|
87
|
+
subprocess.TimeoutExpired,
|
|
88
|
+
):
|
|
89
|
+
return False
|
|
90
|
+
|
|
91
|
+
def _check_watchdog_installed(self) -> bool:
|
|
92
|
+
"""检查 watchdog 是否已安装。
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
True if watchdog is installed, False otherwise.
|
|
96
|
+
"""
|
|
97
|
+
try:
|
|
98
|
+
import watchdog # noqa: F401 # availability probe (optional LFS extra)
|
|
99
|
+
|
|
100
|
+
return True
|
|
101
|
+
except ImportError:
|
|
102
|
+
return False
|
|
103
|
+
|
|
104
|
+
def _check_watchdog_in_wl_python(self) -> bool:
|
|
105
|
+
"""检查 wl.exe 的 Python 环境中是否安装了 watchdog。
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
True if watchdog is installed in wl's Python, False otherwise.
|
|
109
|
+
"""
|
|
110
|
+
wl_python = self._find_wl_python_path()
|
|
111
|
+
if not wl_python:
|
|
112
|
+
return False
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
result = subprocess.run(
|
|
116
|
+
[wl_python, "-c", "import watchdog"],
|
|
117
|
+
capture_output=True,
|
|
118
|
+
text=True,
|
|
119
|
+
timeout=30,
|
|
120
|
+
)
|
|
121
|
+
return result.returncode == 0
|
|
122
|
+
except (
|
|
123
|
+
subprocess.CalledProcessError,
|
|
124
|
+
FileNotFoundError,
|
|
125
|
+
subprocess.TimeoutExpired,
|
|
126
|
+
):
|
|
127
|
+
return False
|
|
128
|
+
|
|
129
|
+
def _get_python_executable(self) -> str:
|
|
130
|
+
"""获取可用的 Python 可执行文件路径。
|
|
131
|
+
|
|
132
|
+
优先使用系统 Python,避免使用 wl.exe 绑定的 uv 环境。
|
|
133
|
+
总是返回一个路径(最后回退到 sys.executable)。
|
|
134
|
+
"""
|
|
135
|
+
# 尝试常见的 Python 命令
|
|
136
|
+
python_commands = ["python", "python3", "py"]
|
|
137
|
+
|
|
138
|
+
for cmd in python_commands:
|
|
139
|
+
python_path = shutil.which(cmd)
|
|
140
|
+
if python_path:
|
|
141
|
+
return python_path
|
|
142
|
+
|
|
143
|
+
# 如果都找不到,返回 sys.executable 作为最后手段
|
|
144
|
+
return sys.executable
|
|
145
|
+
|
|
146
|
+
def _find_wl_python_path(self) -> str | None:
|
|
147
|
+
"""查找 wl.exe 使用的 Python 解释器路径。
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
Python 解释器路径,如果找不到则返回 None。
|
|
151
|
+
"""
|
|
152
|
+
if self._wl_python_cache is not None:
|
|
153
|
+
return self._wl_python_cache
|
|
154
|
+
|
|
155
|
+
# 方法1:从 uv tools 目录查找
|
|
156
|
+
result = self._find_wl_python_from_uv_tools()
|
|
157
|
+
if result:
|
|
158
|
+
self._wl_python_cache = result
|
|
159
|
+
return result
|
|
160
|
+
|
|
161
|
+
# 方法2:从 wl.exe 位置查找
|
|
162
|
+
result = self._find_wl_python_from_wl_exe()
|
|
163
|
+
self._wl_python_cache = result
|
|
164
|
+
return result
|
|
165
|
+
|
|
166
|
+
def _find_wl_python_from_uv_tools(self) -> str | None:
|
|
167
|
+
"""从 uv tools 目录查找 Python 解释器。"""
|
|
168
|
+
if PlatformAdapter.is_windows():
|
|
169
|
+
uv_tools_base = Path.home() / "AppData" / "Roaming" / "uv" / "tools"
|
|
170
|
+
else:
|
|
171
|
+
uv_tools_base = Path.home() / ".local" / "share" / "uv" / "tools"
|
|
172
|
+
|
|
173
|
+
if uv_tools_base.exists():
|
|
174
|
+
wl_cli_dir = uv_tools_base / "py-wlcli"
|
|
175
|
+
if wl_cli_dir.exists():
|
|
176
|
+
scripts_dir = wl_cli_dir / "Scripts"
|
|
177
|
+
|
|
178
|
+
# 尝试在 Scripts 目录中查找
|
|
179
|
+
python_in_scripts = scripts_dir / "python.exe"
|
|
180
|
+
if python_in_scripts.exists():
|
|
181
|
+
return str(python_in_scripts)
|
|
182
|
+
|
|
183
|
+
# 直接在 wl-cli 目录中查找
|
|
184
|
+
python_in_tools = wl_cli_dir / "python.exe"
|
|
185
|
+
if python_in_tools.exists():
|
|
186
|
+
return str(python_in_tools)
|
|
187
|
+
|
|
188
|
+
# 查找 python3.exe
|
|
189
|
+
python3_in_scripts = scripts_dir / "python3.exe"
|
|
190
|
+
if python3_in_scripts.exists():
|
|
191
|
+
return str(python3_in_scripts)
|
|
192
|
+
|
|
193
|
+
return None
|
|
194
|
+
|
|
195
|
+
def _find_wl_python_from_wl_exe(self) -> str | None:
|
|
196
|
+
"""从 wl.exe 位置查找 Python 解释器。"""
|
|
197
|
+
for cmd_name in ["wl", "wl.exe"]:
|
|
198
|
+
try:
|
|
199
|
+
result = subprocess.run(
|
|
200
|
+
["where" if PlatformAdapter.is_windows() else "which", cmd_name],
|
|
201
|
+
capture_output=True,
|
|
202
|
+
text=True,
|
|
203
|
+
shell=False,
|
|
204
|
+
)
|
|
205
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
206
|
+
wl_path = Path(result.stdout.strip().split("\n")[0].strip())
|
|
207
|
+
if wl_path.exists():
|
|
208
|
+
return self._search_python_around_wl_path(wl_path)
|
|
209
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
210
|
+
continue
|
|
211
|
+
|
|
212
|
+
return None
|
|
213
|
+
|
|
214
|
+
def _search_python_around_wl_path(self, wl_path: Path) -> str | None:
|
|
215
|
+
"""在 wl.exe 路径周围搜索 Python 解释器。"""
|
|
216
|
+
# wl.exe 在 Scripts 目录中
|
|
217
|
+
scripts_dir = wl_path.parent
|
|
218
|
+
python_in_scripts = scripts_dir / "python.exe"
|
|
219
|
+
if python_in_scripts.exists():
|
|
220
|
+
return str(python_in_scripts)
|
|
221
|
+
|
|
222
|
+
# 向上查找
|
|
223
|
+
tools_dir = scripts_dir.parent
|
|
224
|
+
python_in_tools = tools_dir / "python.exe"
|
|
225
|
+
if python_in_tools.exists():
|
|
226
|
+
return str(python_in_tools)
|
|
227
|
+
|
|
228
|
+
# 在父目录中查找 Scripts
|
|
229
|
+
return self._search_parent_dirs_for_python(tools_dir)
|
|
230
|
+
|
|
231
|
+
def _search_parent_dirs_for_python(self, start_dir: Path) -> str | None:
|
|
232
|
+
"""在父目录中搜索 Python 解释器。"""
|
|
233
|
+
for parent in start_dir.parents:
|
|
234
|
+
python_candidates = [
|
|
235
|
+
parent / "python.exe",
|
|
236
|
+
parent / "Scripts" / "python.exe",
|
|
237
|
+
parent / "bin" / "python",
|
|
238
|
+
parent / "bin" / "python.exe",
|
|
239
|
+
]
|
|
240
|
+
for python_candidate in python_candidates:
|
|
241
|
+
if python_candidate.exists():
|
|
242
|
+
return str(python_candidate)
|
|
243
|
+
return None
|
|
244
|
+
|
|
245
|
+
def _find_wl_python_version(self) -> str | None:
|
|
246
|
+
"""查找 wl.exe 的 Python 版本。
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
Python 版本字符串,如 "3.11", "3.12" 等,如果找不到则返回 None。
|
|
250
|
+
"""
|
|
251
|
+
wl_python = self._find_wl_python_path()
|
|
252
|
+
if not wl_python:
|
|
253
|
+
return None
|
|
254
|
+
|
|
255
|
+
try:
|
|
256
|
+
result = subprocess.run(
|
|
257
|
+
[wl_python, "--version"],
|
|
258
|
+
capture_output=True,
|
|
259
|
+
text=True,
|
|
260
|
+
timeout=10,
|
|
261
|
+
)
|
|
262
|
+
if result.returncode == 0:
|
|
263
|
+
# 输出格式: "Python 3.11.x"
|
|
264
|
+
version_str = result.stdout.strip()
|
|
265
|
+
if "Python" in version_str:
|
|
266
|
+
parts = version_str.split()
|
|
267
|
+
if len(parts) >= 2:
|
|
268
|
+
return parts[1] # 返回 "3.11.x"
|
|
269
|
+
except (
|
|
270
|
+
subprocess.CalledProcessError,
|
|
271
|
+
FileNotFoundError,
|
|
272
|
+
subprocess.TimeoutExpired,
|
|
273
|
+
):
|
|
274
|
+
pass
|
|
275
|
+
|
|
276
|
+
return None
|
|
277
|
+
|
|
278
|
+
def _install_watchdog(self) -> bool:
|
|
279
|
+
import click
|
|
280
|
+
|
|
281
|
+
click.echo("Installing watchdog for LFS watch functionality...")
|
|
282
|
+
click.echo("正在安装 watchdog 以支持 LFS watch 功能...")
|
|
283
|
+
|
|
284
|
+
wl_python = self._find_wl_python_path()
|
|
285
|
+
if wl_python:
|
|
286
|
+
if self._run_uv_install(wl_python, "watchdog"):
|
|
287
|
+
click.echo(f"✓ watchdog installed via uv for {wl_python}")
|
|
288
|
+
click.echo("✓ watchdog 安装成功(通过 uv)")
|
|
289
|
+
return True
|
|
290
|
+
if self._run_pip_install(wl_python, "watchdog"):
|
|
291
|
+
click.echo(f"✓ watchdog installed via pip for {wl_python}")
|
|
292
|
+
click.echo("✓ watchdog 安装成功(通过 pip)")
|
|
293
|
+
return True
|
|
294
|
+
|
|
295
|
+
system_python = self._get_python_executable()
|
|
296
|
+
if self._run_pip_install(system_python, "watchdog"):
|
|
297
|
+
click.echo("✓ watchdog installed via system pip")
|
|
298
|
+
click.echo("✓ watchdog 安装成功")
|
|
299
|
+
return True
|
|
300
|
+
|
|
301
|
+
click.echo("Failed to install watchdog automatically.", err=True)
|
|
302
|
+
click.echo("无法自动安装 watchdog。", err=True)
|
|
303
|
+
return False
|
|
304
|
+
|
|
305
|
+
def _check_pyyaml_installed(self) -> bool:
|
|
306
|
+
"""检查 wl.exe 的 Python 环境中是否安装了 PyYAML。
|
|
307
|
+
|
|
308
|
+
Returns:
|
|
309
|
+
True if PyYAML is installed in wl's Python, False otherwise.
|
|
310
|
+
"""
|
|
311
|
+
wl_python = self._find_wl_python_path()
|
|
312
|
+
if not wl_python:
|
|
313
|
+
return False
|
|
314
|
+
|
|
315
|
+
try:
|
|
316
|
+
result = subprocess.run(
|
|
317
|
+
[wl_python, "-c", "import yaml"],
|
|
318
|
+
capture_output=True,
|
|
319
|
+
text=True,
|
|
320
|
+
timeout=30,
|
|
321
|
+
)
|
|
322
|
+
return result.returncode == 0
|
|
323
|
+
except (
|
|
324
|
+
subprocess.CalledProcessError,
|
|
325
|
+
FileNotFoundError,
|
|
326
|
+
subprocess.TimeoutExpired,
|
|
327
|
+
):
|
|
328
|
+
return False
|
|
329
|
+
|
|
330
|
+
def _install_pyyaml(self) -> bool:
|
|
331
|
+
import click
|
|
332
|
+
|
|
333
|
+
click.echo("Installing PyYAML for LFS lock configuration...")
|
|
334
|
+
click.echo("正在安装 PyYAML 以支持 LFS lock 配置...")
|
|
335
|
+
|
|
336
|
+
wl_python = self._find_wl_python_path()
|
|
337
|
+
if wl_python:
|
|
338
|
+
if self._run_uv_install(wl_python, "pyyaml>=6.0.0"):
|
|
339
|
+
click.echo(f"✓ PyYAML installed via uv for {wl_python}")
|
|
340
|
+
click.echo("✓ PyYAML 安装成功(通过 uv)")
|
|
341
|
+
return True
|
|
342
|
+
if self._run_pip_install(wl_python, "pyyaml>=6.0.0"):
|
|
343
|
+
click.echo(f"✓ PyYAML installed via pip for {wl_python}")
|
|
344
|
+
click.echo("✓ PyYAML 安装成功(通过 pip)")
|
|
345
|
+
return True
|
|
346
|
+
|
|
347
|
+
system_python = self._get_python_executable()
|
|
348
|
+
if self._run_pip_install(system_python, "pyyaml>=6.0.0"):
|
|
349
|
+
click.echo("✓ PyYAML installed via system pip")
|
|
350
|
+
click.echo("✓ PyYAML 安装成功")
|
|
351
|
+
return True
|
|
352
|
+
|
|
353
|
+
click.echo("Failed to install PyYAML automatically.", err=True)
|
|
354
|
+
click.echo("无法自动安装 PyYAML。", err=True)
|
|
355
|
+
return False
|
|
356
|
+
|
|
357
|
+
def _check_and_install_pyyaml(self) -> bool:
|
|
358
|
+
"""检查并安装 PyYAML,如果未安装则提示用户。
|
|
359
|
+
|
|
360
|
+
Returns:
|
|
361
|
+
True if PyYAML is installed (either was already or just installed),
|
|
362
|
+
False if PyYAML could not be installed.
|
|
363
|
+
"""
|
|
364
|
+
import click
|
|
365
|
+
|
|
366
|
+
# 检查 wl.exe 的 Python 中是否有 PyYAML
|
|
367
|
+
wl_has_pyyaml = self._check_pyyaml_installed()
|
|
368
|
+
|
|
369
|
+
if wl_has_pyyaml:
|
|
370
|
+
click.echo("✓ PyYAML is already installed")
|
|
371
|
+
click.echo("✓ PyYAML 已安装")
|
|
372
|
+
return True
|
|
373
|
+
|
|
374
|
+
click.echo("")
|
|
375
|
+
click.echo("=" * 50)
|
|
376
|
+
click.echo("WARNING: PyYAML is not installed in wl's Python!", err=True)
|
|
377
|
+
click.echo("警告: wl 的 Python 环境中未安装 PyYAML!", err=True)
|
|
378
|
+
click.echo("")
|
|
379
|
+
click.echo("PyYAML is required for loading LFS lock configuration.")
|
|
380
|
+
click.echo("Without PyYAML, only default configuration will be available.")
|
|
381
|
+
click.echo("")
|
|
382
|
+
click.echo("PyYAML 是加载 LFS lock 配置文件的必要依赖。")
|
|
383
|
+
click.echo("没有 PyYAML,只能使用默认配置。")
|
|
384
|
+
click.echo("=" * 50)
|
|
385
|
+
|
|
386
|
+
# 尝试自动安装
|
|
387
|
+
if self._install_pyyaml():
|
|
388
|
+
if self._check_pyyaml_installed():
|
|
389
|
+
return True
|
|
390
|
+
|
|
391
|
+
click.echo("")
|
|
392
|
+
click.echo("Please install PyYAML manually by running:")
|
|
393
|
+
click.echo("请手动安装 PyYAML,运行以下命令:")
|
|
394
|
+
click.echo("")
|
|
395
|
+
click.echo(" uv pip install --python <wl-python-path> pyyaml>=6.0.0")
|
|
396
|
+
click.echo("")
|
|
397
|
+
wl_python = self._find_wl_python_path()
|
|
398
|
+
if wl_python:
|
|
399
|
+
click.echo(
|
|
400
|
+
f' Example: uv pip install --python "{wl_python}" pyyaml>=6.0.0'
|
|
401
|
+
)
|
|
402
|
+
click.echo("")
|
|
403
|
+
|
|
404
|
+
return False
|
|
405
|
+
|
|
406
|
+
def _check_and_install_watchdog(self) -> bool:
|
|
407
|
+
"""检查并安装 watchdog,如果未安装则提示用户。
|
|
408
|
+
|
|
409
|
+
Returns:
|
|
410
|
+
True if watchdog is installed (either was already or just installed),
|
|
411
|
+
False if watchdog could not be installed.
|
|
412
|
+
"""
|
|
413
|
+
import click
|
|
414
|
+
|
|
415
|
+
# 检查用户系统的 Python 中是否有 watchdog
|
|
416
|
+
system_has_watchdog = self._check_watchdog_installed()
|
|
417
|
+
|
|
418
|
+
# 检查 wl.exe 的 Python 中是否有 watchdog
|
|
419
|
+
wl_has_watchdog = self._check_watchdog_in_wl_python()
|
|
420
|
+
|
|
421
|
+
if system_has_watchdog and wl_has_watchdog:
|
|
422
|
+
click.echo("✓ watchdog is already installed")
|
|
423
|
+
click.echo("✓ watchdog 已安装")
|
|
424
|
+
return True
|
|
425
|
+
|
|
426
|
+
if not wl_has_watchdog:
|
|
427
|
+
click.echo("")
|
|
428
|
+
click.echo("=" * 50)
|
|
429
|
+
click.echo("WARNING: watchdog is not installed in wl's Python!", err=True)
|
|
430
|
+
click.echo("警告: wl 的 Python 环境中未安装 watchdog!", err=True)
|
|
431
|
+
click.echo("")
|
|
432
|
+
click.echo("watchdog is required for 'wl lfs watch' functionality.")
|
|
433
|
+
click.echo("The work.bat script needs watchdog to monitor file changes.")
|
|
434
|
+
click.echo("")
|
|
435
|
+
click.echo("watchdog 是 'wl lfs watch' 功能的必要依赖。")
|
|
436
|
+
click.echo("work.bat 脚本需要 watchdog 来监控文件变化。")
|
|
437
|
+
click.echo("")
|
|
438
|
+
|
|
439
|
+
# 尝试自动安装
|
|
440
|
+
if self._install_watchdog():
|
|
441
|
+
# 再次检查是否安装成功
|
|
442
|
+
if self._check_watchdog_in_wl_python():
|
|
443
|
+
return True
|
|
444
|
+
|
|
445
|
+
click.echo("")
|
|
446
|
+
click.echo("Please install watchdog manually by running:")
|
|
447
|
+
click.echo("请手动安装 watchdog,运行以下命令:")
|
|
448
|
+
click.echo("")
|
|
449
|
+
click.echo(" uv pip install --python <wl-python-path> watchdog")
|
|
450
|
+
click.echo("")
|
|
451
|
+
wl_python = self._find_wl_python_path()
|
|
452
|
+
if wl_python:
|
|
453
|
+
click.echo(f' Example: uv pip install --python "{wl_python}" watchdog')
|
|
454
|
+
click.echo("")
|
|
455
|
+
click.echo("=" * 50)
|
|
456
|
+
return False
|
|
457
|
+
|
|
458
|
+
click.echo("✓ watchdog is already installed")
|
|
459
|
+
click.echo("✓ watchdog 已安装")
|
|
460
|
+
return True
|
|
461
|
+
|
|
462
|
+
def _deploy_work_bat(self) -> None:
|
|
463
|
+
"""Copy work.bat template to project root on Windows platforms."""
|
|
464
|
+
if not PlatformAdapter.is_windows():
|
|
465
|
+
return
|
|
466
|
+
|
|
467
|
+
import importlib.resources
|
|
468
|
+
|
|
469
|
+
bat_path = self.project_root / "work.bat"
|
|
470
|
+
try:
|
|
471
|
+
template = importlib.resources.files("wl_cli.vendors.hooks.win").joinpath(
|
|
472
|
+
"work.bat"
|
|
473
|
+
)
|
|
474
|
+
shutil.copy(str(template), str(bat_path))
|
|
475
|
+
import click
|
|
476
|
+
|
|
477
|
+
click.echo(f" work.bat created at {bat_path}")
|
|
478
|
+
click.echo(" Double-click work.bat to start an LFS work session.")
|
|
479
|
+
except Exception as e:
|
|
480
|
+
import click
|
|
481
|
+
|
|
482
|
+
click.echo(f" Warning: could not create work.bat: {e}", err=True)
|
|
483
|
+
|
|
484
|
+
def initialize(self) -> bool:
|
|
485
|
+
"""Write .wl/lfs-lock.yml if it does not exist, or upgrade from v1.
|
|
486
|
+
|
|
487
|
+
Returns:
|
|
488
|
+
True when the lock workflow config is in place, False when the
|
|
489
|
+
project has no ``.wl`` directory yet (the project was never
|
|
490
|
+
initialized). Callers must treat False as a failure and tell the
|
|
491
|
+
user to run ``wl init env`` first — never as a silent skip (#194).
|
|
492
|
+
"""
|
|
493
|
+
wl_dir = self._wl_dir()
|
|
494
|
+
if not self.wl_dir_ready():
|
|
495
|
+
return False
|
|
496
|
+
|
|
497
|
+
# 检查并安装必要依赖
|
|
498
|
+
self._check_and_install_watchdog()
|
|
499
|
+
self._check_and_install_pyyaml()
|
|
500
|
+
|
|
501
|
+
config_path = wl_dir / "lfs-lock.yml"
|
|
502
|
+
if config_path.exists():
|
|
503
|
+
existing = config_path.read_text()
|
|
504
|
+
if "auto_lock:" in existing:
|
|
505
|
+
# Deploy work.bat even when config already exists
|
|
506
|
+
self._deploy_work_bat()
|
|
507
|
+
return True
|
|
508
|
+
# v1 config detected — upgrade to v2
|
|
509
|
+
|
|
510
|
+
content = (
|
|
511
|
+
"# Git LFS file lock workflow configuration\n"
|
|
512
|
+
"# See: wl lfs --help\n"
|
|
513
|
+
"\n"
|
|
514
|
+
"lock:\n"
|
|
515
|
+
" mode: strict # strict | warn | off\n"
|
|
516
|
+
"\n"
|
|
517
|
+
"auto_lock:\n"
|
|
518
|
+
" fallback: true # pre-push auto-lock when watch not running\n"
|
|
519
|
+
" skip_locked_by_others: true\n"
|
|
520
|
+
"\n"
|
|
521
|
+
"auto_unlock:\n"
|
|
522
|
+
" on_push: true # auto-unlock after push detection\n"
|
|
523
|
+
" retry_count: 3\n"
|
|
524
|
+
" retry_delay: 2\n"
|
|
525
|
+
"\n"
|
|
526
|
+
"checks:\n"
|
|
527
|
+
" pre_push: true\n"
|
|
528
|
+
)
|
|
529
|
+
config_path.write_text(content)
|
|
530
|
+
self._deploy_work_bat()
|
|
531
|
+
return True
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""LFS lock manager — wraps git lfs lock/unlock/locks and git check-attr."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import subprocess
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class LfsLockManager:
|
|
10
|
+
"""Manages git LFS file locks by wrapping native git lfs commands."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, cwd: str | None = None) -> None:
|
|
13
|
+
self.cwd = cwd
|
|
14
|
+
|
|
15
|
+
def _run_git(self, args: list[str], **kwargs) -> subprocess.CompletedProcess:
|
|
16
|
+
return subprocess.run(
|
|
17
|
+
["git", *args],
|
|
18
|
+
capture_output=True,
|
|
19
|
+
text=True,
|
|
20
|
+
encoding="utf-8",
|
|
21
|
+
cwd=self.cwd,
|
|
22
|
+
**kwargs,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
def get_lfs_files(self, changed_files: frozenset[str]) -> frozenset[str]:
|
|
26
|
+
"""Filter a set of file paths to only those tracked by Git LFS."""
|
|
27
|
+
return frozenset(f for f in changed_files if self.is_file_lfs_tracked(f))
|
|
28
|
+
|
|
29
|
+
def is_file_lfs_tracked(self, file_path: str) -> bool:
|
|
30
|
+
"""Check if a file matches an LFS filter in .gitattributes."""
|
|
31
|
+
result = self._run_git(["check-attr", "filter", "--", file_path])
|
|
32
|
+
if result.returncode != 0:
|
|
33
|
+
return False
|
|
34
|
+
return "filter: lfs" in result.stdout
|
|
35
|
+
|
|
36
|
+
def lock(self, file_path: str) -> tuple[bool, str]:
|
|
37
|
+
"""Lock an LFS file.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
tuple of (success: bool, error_reason: str).
|
|
41
|
+
error_reason is empty string on success.
|
|
42
|
+
"""
|
|
43
|
+
result = self._run_git(["lfs", "lock", file_path])
|
|
44
|
+
if result.returncode == 0:
|
|
45
|
+
return True, ""
|
|
46
|
+
|
|
47
|
+
# 解析错误原因
|
|
48
|
+
stderr = (result.stderr or "").lower()
|
|
49
|
+
if "already locked" in stderr:
|
|
50
|
+
# 提取锁定者信息
|
|
51
|
+
if "by" in stderr:
|
|
52
|
+
locked_by = stderr.split("by")[-1].strip()
|
|
53
|
+
if locked_by:
|
|
54
|
+
return False, f"already locked by {locked_by}"
|
|
55
|
+
return False, "already locked"
|
|
56
|
+
elif "not found" in stderr or "does not exist" in stderr:
|
|
57
|
+
return False, "file not found"
|
|
58
|
+
elif "not tracked" in stderr or "not an lfs object" in stderr:
|
|
59
|
+
return False, "not tracked by LFS"
|
|
60
|
+
elif "authentication" in stderr or "login" in stderr:
|
|
61
|
+
return False, "authentication required"
|
|
62
|
+
elif "network" in stderr or "connection" in stderr or "timeout" in stderr:
|
|
63
|
+
return False, "network error"
|
|
64
|
+
else:
|
|
65
|
+
return False, "unknown error"
|
|
66
|
+
|
|
67
|
+
def unlock(self, file_path: str, force: bool = False) -> bool:
|
|
68
|
+
"""Unlock an LFS file. Use force=True for admin force-unlock."""
|
|
69
|
+
cmd = ["lfs", "unlock"]
|
|
70
|
+
if force:
|
|
71
|
+
cmd.append("--force")
|
|
72
|
+
cmd.append(file_path)
|
|
73
|
+
result = self._run_git(cmd)
|
|
74
|
+
return result.returncode == 0
|
|
75
|
+
|
|
76
|
+
def get_locks(self, timeout: int | None = None) -> list[dict]:
|
|
77
|
+
"""Return all current LFS locks as a list of dicts."""
|
|
78
|
+
result = self._run_git(["lfs", "locks", "--json"], timeout=timeout)
|
|
79
|
+
if result.returncode != 0 or not result.stdout.strip():
|
|
80
|
+
return []
|
|
81
|
+
return [json.loads(line) for line in result.stdout.splitlines() if line]
|
|
82
|
+
|
|
83
|
+
def lock_files(self, files: list[str]) -> dict[str, tuple[bool, str]]:
|
|
84
|
+
"""Lock multiple LFS files. Returns {file_path: (success, reason)} for each."""
|
|
85
|
+
results: dict[str, tuple[bool, str]] = {}
|
|
86
|
+
for file_path in files:
|
|
87
|
+
results[file_path] = self.lock(file_path)
|
|
88
|
+
return results
|
|
89
|
+
|
|
90
|
+
def unlock_files(self, files: list[str]) -> dict[str, bool]:
|
|
91
|
+
"""Unlock multiple LFS files. Returns {file_path: success} for each."""
|
|
92
|
+
results: dict[str, bool] = {}
|
|
93
|
+
for file_path in files:
|
|
94
|
+
results[file_path] = self.unlock(file_path)
|
|
95
|
+
return results
|