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,207 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Backup Manager - manages backup and rollback of .wl directory.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import shutil
|
|
8
|
+
import time
|
|
9
|
+
import uuid
|
|
10
|
+
from typing import TYPE_CHECKING
|
|
11
|
+
|
|
12
|
+
from wl_cli.utils.logging import log_info_pair
|
|
13
|
+
|
|
14
|
+
from ....utils.root import RootKind
|
|
15
|
+
from ....utils.root_fallback import current_root
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class BackupManager:
|
|
22
|
+
"""Manage backup and rollback of .wl directory."""
|
|
23
|
+
|
|
24
|
+
MAX_BACKUPS = 5
|
|
25
|
+
|
|
26
|
+
def __init__(self, wl_dir: Path | None = None):
|
|
27
|
+
"""Initialize BackupManager.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
wl_dir: Path to .wl directory. Defaults to './.wl'.
|
|
31
|
+
"""
|
|
32
|
+
# Root-anchored (#125): the deploy root's ``.wl``, not the process
|
|
33
|
+
# CWD's, so ``wl update rollback`` from a subdirectory finds the root's
|
|
34
|
+
# backups instead of none. The scratch snapshots below
|
|
35
|
+
# (``temp_backup / ".wl"``) are deliberately *not* root-derived: they are
|
|
36
|
+
# a matched write/restore pair, not a deployed config path.
|
|
37
|
+
self.wl_dir = wl_dir or current_root(RootKind.DEPLOY).wl_dir()
|
|
38
|
+
self.backup_dir = self.wl_dir / "backup"
|
|
39
|
+
|
|
40
|
+
def _ensure_backup_dir(self) -> None:
|
|
41
|
+
"""Ensure backup directory exists."""
|
|
42
|
+
self.backup_dir.mkdir(parents=True, exist_ok=True)
|
|
43
|
+
|
|
44
|
+
def _generate_backup_id(self) -> str:
|
|
45
|
+
"""Generate a unique backup ID based on timestamp and random UUID.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
Backup ID string.
|
|
49
|
+
"""
|
|
50
|
+
import time as time_module
|
|
51
|
+
|
|
52
|
+
timestamp = time_module.strftime("%Y%m%d_%H%M%S")
|
|
53
|
+
micros = str(int(time_module.time() * 1_000_000))[-6:]
|
|
54
|
+
random_suffix = str(uuid.uuid4())[:4]
|
|
55
|
+
return f"backup_{timestamp}_{micros}_{random_suffix}"
|
|
56
|
+
|
|
57
|
+
def create_backup(self) -> str:
|
|
58
|
+
"""Create a backup of the current .wl directory.
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
Backup ID of the created backup.
|
|
62
|
+
"""
|
|
63
|
+
backup_id = self._generate_backup_id()
|
|
64
|
+
backup_path = self.backup_dir / backup_id
|
|
65
|
+
|
|
66
|
+
log_info_pair(f"Creating backup: {backup_id}", f"正在创建备份:{backup_id}")
|
|
67
|
+
|
|
68
|
+
self._ensure_backup_dir()
|
|
69
|
+
backup_path.mkdir(parents=True, exist_ok=True)
|
|
70
|
+
|
|
71
|
+
if self.wl_dir.exists():
|
|
72
|
+
self._copy_directory_contents(self.wl_dir, backup_path)
|
|
73
|
+
|
|
74
|
+
self.cleanup_old_backups()
|
|
75
|
+
|
|
76
|
+
log_info_pair(f"✓ Backup created: {backup_id}", f"✓ 备份已创建:{backup_id}")
|
|
77
|
+
|
|
78
|
+
return backup_id
|
|
79
|
+
|
|
80
|
+
def _copy_directory_contents(self, source: Path, dest: Path) -> None:
|
|
81
|
+
"""Copy contents from source directory to destination, skipping backup.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
source: Source directory path
|
|
85
|
+
dest: Destination directory path
|
|
86
|
+
"""
|
|
87
|
+
for item in source.iterdir():
|
|
88
|
+
if item.name == "backup":
|
|
89
|
+
continue
|
|
90
|
+
target = dest / item.name
|
|
91
|
+
dest.mkdir(parents=True, exist_ok=True)
|
|
92
|
+
if item.is_dir():
|
|
93
|
+
shutil.copytree(str(item), str(target), dirs_exist_ok=True)
|
|
94
|
+
else:
|
|
95
|
+
shutil.copy2(str(item), str(target))
|
|
96
|
+
|
|
97
|
+
def _clear_wl_directory(self) -> None:
|
|
98
|
+
"""Clear contents of .wl directory, keeping backup folder."""
|
|
99
|
+
self.wl_dir.mkdir(parents=True, exist_ok=True)
|
|
100
|
+
for item in list(self.wl_dir.iterdir()):
|
|
101
|
+
if item.name == "backup":
|
|
102
|
+
continue
|
|
103
|
+
if item.is_dir():
|
|
104
|
+
shutil.rmtree(str(item))
|
|
105
|
+
else:
|
|
106
|
+
item.unlink()
|
|
107
|
+
|
|
108
|
+
def _restore_from_temp_backup(self, temp_backup: Path) -> None:
|
|
109
|
+
"""Restore from temporary backup directory.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
temp_backup: Temporary backup directory path
|
|
113
|
+
"""
|
|
114
|
+
# `.wl` here is the scratch snapshot's inner directory name, matched
|
|
115
|
+
# between write and restore -- not a root-derived config path (#121).
|
|
116
|
+
temp_wl_dir = temp_backup / ".wl"
|
|
117
|
+
if temp_wl_dir.exists():
|
|
118
|
+
self._clear_wl_directory()
|
|
119
|
+
self._copy_directory_contents(temp_wl_dir, self.wl_dir)
|
|
120
|
+
shutil.rmtree(str(temp_backup))
|
|
121
|
+
|
|
122
|
+
def rollback_to_backup(self, backup_id: str | None = None) -> bool:
|
|
123
|
+
"""Rollback to a specific backup or the latest one.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
backup_id: Optional backup ID to rollback to. If None, uses latest.
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
True if rollback successful, False otherwise.
|
|
130
|
+
"""
|
|
131
|
+
if not backup_id:
|
|
132
|
+
backups = self.list_backups()
|
|
133
|
+
if not backups:
|
|
134
|
+
log_info_pair(
|
|
135
|
+
"No backups available to rollback to", "没有可用的备份可回滚"
|
|
136
|
+
)
|
|
137
|
+
return False
|
|
138
|
+
backup_id = backups[-1]
|
|
139
|
+
|
|
140
|
+
backup_path = self.backup_dir / backup_id
|
|
141
|
+
|
|
142
|
+
if not backup_path.exists():
|
|
143
|
+
log_info_pair(f"Backup not found: {backup_id}", f"备份不存在:{backup_id}")
|
|
144
|
+
return False
|
|
145
|
+
|
|
146
|
+
log_info_pair(
|
|
147
|
+
f"Rolling back to backup: {backup_id}", f"正在回滚到备份:{backup_id}"
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
temp_backup = None
|
|
151
|
+
try:
|
|
152
|
+
if self.wl_dir.exists():
|
|
153
|
+
temp_backup = (
|
|
154
|
+
self.wl_dir.parent / f".wl_rollback_temp_{int(time.time())}"
|
|
155
|
+
)
|
|
156
|
+
# Scratch snapshot layout name; matched by _restore_from_temp_backup.
|
|
157
|
+
temp_wl_dir = temp_backup / ".wl"
|
|
158
|
+
self._copy_directory_contents(self.wl_dir, temp_wl_dir)
|
|
159
|
+
|
|
160
|
+
self._clear_wl_directory()
|
|
161
|
+
self._copy_directory_contents(backup_path, self.wl_dir)
|
|
162
|
+
|
|
163
|
+
if temp_backup and temp_backup.exists():
|
|
164
|
+
shutil.rmtree(str(temp_backup))
|
|
165
|
+
|
|
166
|
+
log_info_pair(
|
|
167
|
+
f"✓ Rollback successful to: {backup_id}", f"✓ 成功回滚到:{backup_id}"
|
|
168
|
+
)
|
|
169
|
+
return True
|
|
170
|
+
|
|
171
|
+
except Exception as e:
|
|
172
|
+
if temp_backup and temp_backup.exists():
|
|
173
|
+
self._restore_from_temp_backup(temp_backup)
|
|
174
|
+
|
|
175
|
+
log_info_pair(f"Rollback failed: {e}", f"回滚失败:{e}")
|
|
176
|
+
return False
|
|
177
|
+
|
|
178
|
+
def list_backups(self) -> list[str]:
|
|
179
|
+
"""List all available backups.
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
List of backup IDs, sorted from oldest to newest.
|
|
183
|
+
"""
|
|
184
|
+
if not self.backup_dir.exists():
|
|
185
|
+
return []
|
|
186
|
+
|
|
187
|
+
backups = []
|
|
188
|
+
for item in self.backup_dir.iterdir():
|
|
189
|
+
if item.is_dir() and item.name.startswith("backup_"):
|
|
190
|
+
backups.append(item.name)
|
|
191
|
+
|
|
192
|
+
backups.sort()
|
|
193
|
+
return backups
|
|
194
|
+
|
|
195
|
+
def cleanup_old_backups(self) -> None:
|
|
196
|
+
"""Clean up old backups, keeping only the most recent MAX_BACKUPS."""
|
|
197
|
+
backups = self.list_backups()
|
|
198
|
+
|
|
199
|
+
if len(backups) > self.MAX_BACKUPS:
|
|
200
|
+
to_remove = backups[: len(backups) - self.MAX_BACKUPS]
|
|
201
|
+
|
|
202
|
+
for backup_id in to_remove:
|
|
203
|
+
backup_path = self.backup_dir / backup_id
|
|
204
|
+
log_info_pair(
|
|
205
|
+
f"Removing old backup: {backup_id}", f"移除旧备份:{backup_id}"
|
|
206
|
+
)
|
|
207
|
+
shutil.rmtree(backup_path)
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Component manager for managing updatable components.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ComponentManager:
|
|
11
|
+
"""Manage updatable components."""
|
|
12
|
+
|
|
13
|
+
def __init__(self):
|
|
14
|
+
"""Initialize ComponentManager with default components."""
|
|
15
|
+
self._components: dict[str, dict] = {
|
|
16
|
+
"git_config": {
|
|
17
|
+
"description": "Git configuration files",
|
|
18
|
+
"source_path": "vendors/git/",
|
|
19
|
+
},
|
|
20
|
+
"editor_config": {
|
|
21
|
+
"description": "Editor configuration",
|
|
22
|
+
"source_path": "vendors/config/",
|
|
23
|
+
},
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
def get_all_components(self) -> dict[str, dict]:
|
|
27
|
+
"""Get all updatable components.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
Dictionary of all components with their information.
|
|
31
|
+
"""
|
|
32
|
+
return self._components.copy()
|
|
33
|
+
|
|
34
|
+
def get_component(self, component_id: str) -> dict | None:
|
|
35
|
+
"""Get component information by ID.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
component_id: ID of the component to get.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
Component information if found, None otherwise.
|
|
42
|
+
"""
|
|
43
|
+
return self._components.get(component_id)
|
|
44
|
+
|
|
45
|
+
def component_exists(self, component_id: str) -> bool:
|
|
46
|
+
"""Check if component exists.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
component_id: ID of the component to check.
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
True if component exists, False otherwise.
|
|
53
|
+
"""
|
|
54
|
+
return component_id in self._components
|
|
55
|
+
|
|
56
|
+
def get_component_source_path(self, component_id: str) -> Path | None:
|
|
57
|
+
"""Get component source path.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
component_id: ID of the component.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
Source path as Path object if component exists, None otherwise.
|
|
64
|
+
"""
|
|
65
|
+
component = self.get_component(component_id)
|
|
66
|
+
if component:
|
|
67
|
+
return Path(component["source_path"])
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
def register_component(
|
|
71
|
+
self, component_id: str, description: str, source_path: str
|
|
72
|
+
) -> None:
|
|
73
|
+
"""Register a new component.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
component_id: ID of the new component.
|
|
77
|
+
description: Description of the new component.
|
|
78
|
+
source_path: Source path of the new component.
|
|
79
|
+
"""
|
|
80
|
+
self._components[component_id] = {
|
|
81
|
+
"description": description,
|
|
82
|
+
"source_path": source_path,
|
|
83
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Conflict Resolver - detects and resolves file conflicts.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import hashlib
|
|
8
|
+
from enum import Enum
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
10
|
+
|
|
11
|
+
from wl_cli.commands.update.services.version_tracker import VersionTracker
|
|
12
|
+
from wl_cli.utils.logging import log_info_pair
|
|
13
|
+
|
|
14
|
+
from ....utils.root import RootKind
|
|
15
|
+
from ....utils.root_fallback import current_root
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ConflictResolution(Enum):
|
|
22
|
+
"""Conflict resolution strategies."""
|
|
23
|
+
|
|
24
|
+
OVERWRITE = "overwrite"
|
|
25
|
+
KEEP = "keep"
|
|
26
|
+
SKIP = "skip"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ConflictResolver:
|
|
30
|
+
"""Detect and resolve file conflicts."""
|
|
31
|
+
|
|
32
|
+
def __init__(self, wl_dir: Path | None = None):
|
|
33
|
+
"""Initialize ConflictResolver.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
wl_dir: Path to .wl directory. Defaults to './.wl'.
|
|
37
|
+
"""
|
|
38
|
+
# Root-anchored (#125): the deploy root's ``.wl``, matching the update
|
|
39
|
+
# family this resolver serves. Was a bare ``Path(".wl")``, i.e. the
|
|
40
|
+
# process CWD's ``.wl`` (no production caller yet, but same ruling).
|
|
41
|
+
self.wl_dir = wl_dir or current_root(RootKind.DEPLOY).wl_dir()
|
|
42
|
+
self.version_tracker = VersionTracker(self.wl_dir)
|
|
43
|
+
|
|
44
|
+
def compute_file_hash(self, file_path: Path) -> str:
|
|
45
|
+
"""Compute SHA-256 hash of a file.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
file_path: Path to the file.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
SHA-256 hash of the file content.
|
|
52
|
+
"""
|
|
53
|
+
if not file_path.exists():
|
|
54
|
+
return ""
|
|
55
|
+
|
|
56
|
+
hash_obj = hashlib.sha256()
|
|
57
|
+
with open(file_path, "rb") as f:
|
|
58
|
+
for chunk in iter(lambda: f.read(4096), b""):
|
|
59
|
+
hash_obj.update(chunk)
|
|
60
|
+
return hash_obj.hexdigest()
|
|
61
|
+
|
|
62
|
+
def detect_conflicts(
|
|
63
|
+
self, source_dir: Path, target_dir: Path
|
|
64
|
+
) -> dict[str, ConflictResolution]:
|
|
65
|
+
"""Detect conflicts between source and target files.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
source_dir: Source directory path.
|
|
69
|
+
target_dir: Target directory path.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
Dictionary mapping file paths to conflict resolution status.
|
|
73
|
+
"""
|
|
74
|
+
conflicts: dict[str, ConflictResolution] = {}
|
|
75
|
+
|
|
76
|
+
if not source_dir.exists() or not target_dir.exists():
|
|
77
|
+
return conflicts
|
|
78
|
+
|
|
79
|
+
for source_file in source_dir.rglob("*"):
|
|
80
|
+
if not source_file.is_file():
|
|
81
|
+
continue
|
|
82
|
+
|
|
83
|
+
relative_path = source_file.relative_to(source_dir)
|
|
84
|
+
target_file = target_dir / relative_path
|
|
85
|
+
|
|
86
|
+
if target_file.exists():
|
|
87
|
+
source_hash = self.compute_file_hash(source_file)
|
|
88
|
+
target_hash = self.compute_file_hash(target_file)
|
|
89
|
+
|
|
90
|
+
if source_hash != target_hash:
|
|
91
|
+
file_path_str = str(relative_path)
|
|
92
|
+
|
|
93
|
+
if self.version_tracker.is_custom_file(file_path_str):
|
|
94
|
+
conflicts[file_path_str] = ConflictResolution.KEEP
|
|
95
|
+
else:
|
|
96
|
+
conflicts[file_path_str] = ConflictResolution.OVERWRITE
|
|
97
|
+
|
|
98
|
+
return conflicts
|
|
99
|
+
|
|
100
|
+
def resolve_conflict(
|
|
101
|
+
self,
|
|
102
|
+
file_path: str,
|
|
103
|
+
source_file: Path,
|
|
104
|
+
target_file: Path,
|
|
105
|
+
resolution: ConflictResolution,
|
|
106
|
+
) -> bool:
|
|
107
|
+
"""Resolve a file conflict using specified strategy.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
file_path: Relative path to the file.
|
|
111
|
+
source_file: Source file path.
|
|
112
|
+
target_file: Target file path.
|
|
113
|
+
resolution: Conflict resolution strategy.
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
True if conflict resolved successfully, False otherwise.
|
|
117
|
+
"""
|
|
118
|
+
if resolution == ConflictResolution.OVERWRITE:
|
|
119
|
+
log_info_pair(f"Overwriting: {file_path}", f"正在覆盖:{file_path}")
|
|
120
|
+
|
|
121
|
+
target_file.parent.mkdir(parents=True, exist_ok=True)
|
|
122
|
+
import shutil
|
|
123
|
+
|
|
124
|
+
shutil.copy2(source_file, target_file)
|
|
125
|
+
|
|
126
|
+
if self.version_tracker.is_custom_file(file_path):
|
|
127
|
+
self.version_tracker.remove_custom_file(file_path)
|
|
128
|
+
|
|
129
|
+
return True
|
|
130
|
+
|
|
131
|
+
elif resolution == ConflictResolution.KEEP:
|
|
132
|
+
log_info_pair(
|
|
133
|
+
f"Keeping user modified: {file_path}", f"保留用户修改:{file_path}"
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
if not self.version_tracker.is_custom_file(file_path):
|
|
137
|
+
self.version_tracker.add_custom_file(file_path)
|
|
138
|
+
|
|
139
|
+
return True
|
|
140
|
+
|
|
141
|
+
elif resolution == ConflictResolution.SKIP:
|
|
142
|
+
log_info_pair(f"Skipping: {file_path}", f"跳过:{file_path}")
|
|
143
|
+
return True
|
|
144
|
+
|
|
145
|
+
return False
|
|
146
|
+
|
|
147
|
+
def identify_custom_files(self, source_dir: Path, target_dir: Path) -> list[str]:
|
|
148
|
+
"""Identify files in target that have been modified by user.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
source_dir: Source directory (original template).
|
|
152
|
+
target_dir: Target directory (user's copy).
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
List of custom file paths.
|
|
156
|
+
"""
|
|
157
|
+
custom_files: list[str] = []
|
|
158
|
+
|
|
159
|
+
if not source_dir.exists() or not target_dir.exists():
|
|
160
|
+
return custom_files
|
|
161
|
+
|
|
162
|
+
for target_file in target_dir.rglob("*"):
|
|
163
|
+
if not target_file.is_file():
|
|
164
|
+
continue
|
|
165
|
+
|
|
166
|
+
relative_path = target_file.relative_to(target_dir)
|
|
167
|
+
source_file = source_dir / relative_path
|
|
168
|
+
|
|
169
|
+
if source_file.exists():
|
|
170
|
+
source_hash = self.compute_file_hash(source_file)
|
|
171
|
+
target_hash = self.compute_file_hash(target_file)
|
|
172
|
+
|
|
173
|
+
if source_hash != target_hash:
|
|
174
|
+
custom_files.append(str(relative_path))
|
|
175
|
+
else:
|
|
176
|
+
custom_files.append(str(relative_path))
|
|
177
|
+
|
|
178
|
+
return custom_files
|
|
179
|
+
|
|
180
|
+
def mark_as_custom(self, file_path: str) -> None:
|
|
181
|
+
"""Mark a file as user-customized.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
file_path: Path to the file.
|
|
185
|
+
"""
|
|
186
|
+
self.version_tracker.add_custom_file(file_path)
|
|
187
|
+
log_info_pair(
|
|
188
|
+
f"Marked as custom: {file_path}", f"已标记为自定义文件:{file_path}"
|
|
189
|
+
)
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Vendors Updater - manages updating vendor templates from package.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
from wl_cli.utils.logging import log_info_pair
|
|
10
|
+
from wl_cli.utils.wl_dir_updater import (
|
|
11
|
+
_get_package_vendors_path,
|
|
12
|
+
check_and_update_vendors,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
from ....utils.root import RootKind
|
|
16
|
+
from ....utils.root_fallback import current_root
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class VendorsUpdater:
|
|
23
|
+
"""Manage updating vendor templates from package."""
|
|
24
|
+
|
|
25
|
+
def __init__(self, wl_dir: Path | None = None):
|
|
26
|
+
"""Initialize VendorsUpdater.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
wl_dir: Path to .wl directory. Defaults to './.wl'.
|
|
30
|
+
"""
|
|
31
|
+
# Root-anchored (#125): the deploy root's ``.wl``, matching the update
|
|
32
|
+
# family. ``self.wl_dir`` is currently never read, so the value is inert
|
|
33
|
+
# -- fixed for consistency, not because a path was observed wrong.
|
|
34
|
+
self.wl_dir = wl_dir or current_root(RootKind.DEPLOY).wl_dir()
|
|
35
|
+
self.package_vendors_path = _get_package_vendors_path()
|
|
36
|
+
|
|
37
|
+
def update(
|
|
38
|
+
self,
|
|
39
|
+
dry_run: bool = False,
|
|
40
|
+
component_id: str | None = None,
|
|
41
|
+
force: bool = False,
|
|
42
|
+
) -> dict:
|
|
43
|
+
"""Update vendors directory.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
dry_run: If True, only show what would be done.
|
|
47
|
+
component_id: Optional specific component to update.
|
|
48
|
+
force: Copy even when the vendor stamp is already current.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
Update result dictionary.
|
|
52
|
+
"""
|
|
53
|
+
log_info_pair("Starting vendor update...", "开始更新供应商模板...")
|
|
54
|
+
|
|
55
|
+
result: dict = check_and_update_vendors(
|
|
56
|
+
dry_run=dry_run, component_id=component_id, force=force
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
if dry_run:
|
|
60
|
+
log_info_pair(
|
|
61
|
+
"Dry run completed. No changes made.", "干运行完成,未做任何更改。"
|
|
62
|
+
)
|
|
63
|
+
else:
|
|
64
|
+
if result.get("updated"):
|
|
65
|
+
log_info_pair("Vendors updated successfully!", "供应商模板更新成功!")
|
|
66
|
+
else:
|
|
67
|
+
log_info_pair(
|
|
68
|
+
"Vendors are already up to date.", "供应商模板已是最新版本。"
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
return result
|
|
72
|
+
|
|
73
|
+
def check(self) -> dict:
|
|
74
|
+
"""Check for vendor updates.
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
Check result dictionary.
|
|
78
|
+
"""
|
|
79
|
+
log_info_pair("Checking for vendor updates...", "检查供应商模板更新...")
|
|
80
|
+
|
|
81
|
+
result: dict = check_and_update_vendors(dry_run=True)
|
|
82
|
+
|
|
83
|
+
log_info_pair("Check completed.", "检查完成。")
|
|
84
|
+
|
|
85
|
+
return result
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Version tracker for managing version information of vendor templates.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class VersionTracker:
|
|
16
|
+
"""Manage version tracking for vendor templates."""
|
|
17
|
+
|
|
18
|
+
VERSION_FILE = ".version.json"
|
|
19
|
+
|
|
20
|
+
def __init__(self, wl_dir: Path):
|
|
21
|
+
"""Initialize VersionTracker.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
wl_dir: Path to .wl directory.
|
|
25
|
+
"""
|
|
26
|
+
self.wl_dir = wl_dir
|
|
27
|
+
self.vendors_dir = wl_dir / "vendors"
|
|
28
|
+
self.version_file = self.vendors_dir / self.VERSION_FILE
|
|
29
|
+
|
|
30
|
+
def ensure_vendors_dir(self) -> None:
|
|
31
|
+
"""Ensure vendors directory exists."""
|
|
32
|
+
self.vendors_dir.mkdir(parents=True, exist_ok=True)
|
|
33
|
+
|
|
34
|
+
def get_version_info(self) -> dict | None:
|
|
35
|
+
"""Get current version information.
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
Version information dict if file exists and valid, None otherwise.
|
|
39
|
+
"""
|
|
40
|
+
if not self.version_file.exists():
|
|
41
|
+
return None
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
with open(self.version_file, encoding="utf-8") as f:
|
|
45
|
+
data: dict = json.load(f)
|
|
46
|
+
return data
|
|
47
|
+
except (OSError, json.JSONDecodeError):
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
def update_version_info(
|
|
51
|
+
self, version: str, source: str, custom_files: list[str] | None = None
|
|
52
|
+
) -> None:
|
|
53
|
+
"""Update version information.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
version: Version string.
|
|
57
|
+
source: Source of the update.
|
|
58
|
+
custom_files: Optional list of custom files to track.
|
|
59
|
+
"""
|
|
60
|
+
self.ensure_vendors_dir()
|
|
61
|
+
|
|
62
|
+
version_info = {
|
|
63
|
+
"version": version,
|
|
64
|
+
"last_updated": datetime.now().isoformat(),
|
|
65
|
+
"source": source,
|
|
66
|
+
"custom_files": custom_files or [],
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
with open(self.version_file, "w", encoding="utf-8") as f:
|
|
70
|
+
json.dump(version_info, f, indent=2, ensure_ascii=False)
|
|
71
|
+
|
|
72
|
+
def add_custom_file(self, file_path: str) -> None:
|
|
73
|
+
"""Add custom file to tracking list.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
file_path: Path to custom file.
|
|
77
|
+
"""
|
|
78
|
+
version_info = self.get_version_info() or {}
|
|
79
|
+
custom_files = version_info.get("custom_files", [])
|
|
80
|
+
|
|
81
|
+
if file_path not in custom_files:
|
|
82
|
+
custom_files.append(file_path)
|
|
83
|
+
version_info["custom_files"] = custom_files
|
|
84
|
+
|
|
85
|
+
if "version" in version_info and "source" in version_info:
|
|
86
|
+
self.update_version_info(
|
|
87
|
+
version_info["version"],
|
|
88
|
+
version_info["source"],
|
|
89
|
+
custom_files,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
def remove_custom_file(self, file_path: str) -> None:
|
|
93
|
+
"""Remove custom file from tracking list.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
file_path: Path to custom file to remove.
|
|
97
|
+
"""
|
|
98
|
+
version_info = self.get_version_info()
|
|
99
|
+
if not version_info:
|
|
100
|
+
return
|
|
101
|
+
|
|
102
|
+
custom_files = version_info.get("custom_files", [])
|
|
103
|
+
if file_path in custom_files:
|
|
104
|
+
custom_files.remove(file_path)
|
|
105
|
+
version_info["custom_files"] = custom_files
|
|
106
|
+
|
|
107
|
+
if "version" in version_info and "source" in version_info:
|
|
108
|
+
self.update_version_info(
|
|
109
|
+
version_info["version"],
|
|
110
|
+
version_info["source"],
|
|
111
|
+
custom_files,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
def is_custom_file(self, file_path: str) -> bool:
|
|
115
|
+
"""Check if file is a custom file.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
file_path: Path to check.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
True if file is in custom files list, False otherwise.
|
|
122
|
+
"""
|
|
123
|
+
version_info = self.get_version_info()
|
|
124
|
+
if not version_info:
|
|
125
|
+
return False
|
|
126
|
+
|
|
127
|
+
return file_path in version_info.get("custom_files", [])
|