redeploy 0.2.6__tar.gz → 0.2.8__tar.gz
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.
- {redeploy-0.2.6 → redeploy-0.2.8}/PKG-INFO +6 -6
- {redeploy-0.2.6 → redeploy-0.2.8}/README.md +5 -5
- {redeploy-0.2.6 → redeploy-0.2.8}/pyproject.toml +1 -1
- {redeploy-0.2.6 → redeploy-0.2.8}/redeploy/__init__.py +1 -1
- {redeploy-0.2.6 → redeploy-0.2.8}/redeploy/cli.py +296 -35
- {redeploy-0.2.6 → redeploy-0.2.8}/redeploy/tests/test_version_cli.py +200 -0
- {redeploy-0.2.6 → redeploy-0.2.8}/redeploy.egg-info/PKG-INFO +6 -6
- {redeploy-0.2.6 → redeploy-0.2.8}/redeploy/apply/__init__.py +0 -0
- {redeploy-0.2.6 → redeploy-0.2.8}/redeploy/apply/executor.py +0 -0
- {redeploy-0.2.6 → redeploy-0.2.8}/redeploy/data_sync.py +0 -0
- {redeploy-0.2.6 → redeploy-0.2.8}/redeploy/detect/__init__.py +0 -0
- {redeploy-0.2.6 → redeploy-0.2.8}/redeploy/detect/detector.py +0 -0
- {redeploy-0.2.6 → redeploy-0.2.8}/redeploy/detect/probes.py +0 -0
- {redeploy-0.2.6 → redeploy-0.2.8}/redeploy/detect/remote.py +0 -0
- {redeploy-0.2.6 → redeploy-0.2.8}/redeploy/detect/templates.py +0 -0
- {redeploy-0.2.6 → redeploy-0.2.8}/redeploy/detect/workflow.py +0 -0
- {redeploy-0.2.6 → redeploy-0.2.8}/redeploy/discovery.py +0 -0
- {redeploy-0.2.6 → redeploy-0.2.8}/redeploy/dsl/__init__.py +0 -0
- {redeploy-0.2.6 → redeploy-0.2.8}/redeploy/dsl/loader.py +0 -0
- {redeploy-0.2.6 → redeploy-0.2.8}/redeploy/dsl/parser.py +0 -0
- {redeploy-0.2.6 → redeploy-0.2.8}/redeploy/dsl_python/__init__.py +0 -0
- {redeploy-0.2.6 → redeploy-0.2.8}/redeploy/dsl_python/context.py +0 -0
- {redeploy-0.2.6 → redeploy-0.2.8}/redeploy/dsl_python/decorators.py +0 -0
- {redeploy-0.2.6 → redeploy-0.2.8}/redeploy/dsl_python/docker_steps.py +0 -0
- {redeploy-0.2.6 → redeploy-0.2.8}/redeploy/dsl_python/exceptions.py +0 -0
- {redeploy-0.2.6 → redeploy-0.2.8}/redeploy/dsl_python/runner.py +0 -0
- {redeploy-0.2.6 → redeploy-0.2.8}/redeploy/dsl_python/steps.py +0 -0
- {redeploy-0.2.6 → redeploy-0.2.8}/redeploy/fleet.py +0 -0
- {redeploy-0.2.6 → redeploy-0.2.8}/redeploy/iac/__init__.py +0 -0
- {redeploy-0.2.6 → redeploy-0.2.8}/redeploy/iac/base.py +0 -0
- {redeploy-0.2.6 → redeploy-0.2.8}/redeploy/iac/docker_compose.py +0 -0
- {redeploy-0.2.6 → redeploy-0.2.8}/redeploy/iac/parsers/__init__.py +0 -0
- {redeploy-0.2.6 → redeploy-0.2.8}/redeploy/iac/parsers/compose.py +0 -0
- {redeploy-0.2.6 → redeploy-0.2.8}/redeploy/iac/registry.py +0 -0
- {redeploy-0.2.6 → redeploy-0.2.8}/redeploy/models.py +0 -0
- {redeploy-0.2.6 → redeploy-0.2.8}/redeploy/observe.py +0 -0
- {redeploy-0.2.6 → redeploy-0.2.8}/redeploy/parse.py +0 -0
- {redeploy-0.2.6 → redeploy-0.2.8}/redeploy/patterns.py +0 -0
- {redeploy-0.2.6 → redeploy-0.2.8}/redeploy/plan/__init__.py +0 -0
- {redeploy-0.2.6 → redeploy-0.2.8}/redeploy/plan/planner.py +0 -0
- {redeploy-0.2.6 → redeploy-0.2.8}/redeploy/plugins/__init__.py +0 -0
- {redeploy-0.2.6 → redeploy-0.2.8}/redeploy/plugins/builtin/__init__.py +0 -0
- {redeploy-0.2.6 → redeploy-0.2.8}/redeploy/plugins/builtin/browser_reload.py +0 -0
- {redeploy-0.2.6 → redeploy-0.2.8}/redeploy/plugins/builtin/notify.py +0 -0
- {redeploy-0.2.6 → redeploy-0.2.8}/redeploy/plugins/builtin/systemd_reload.py +0 -0
- {redeploy-0.2.6 → redeploy-0.2.8}/redeploy/ssh.py +0 -0
- {redeploy-0.2.6 → redeploy-0.2.8}/redeploy/steps.py +0 -0
- {redeploy-0.2.6 → redeploy-0.2.8}/redeploy/tests/__init__.py +0 -0
- {redeploy-0.2.6 → redeploy-0.2.8}/redeploy/tests/test_cli.py +0 -0
- {redeploy-0.2.6 → redeploy-0.2.8}/redeploy/tests/test_data_sync.py +0 -0
- {redeploy-0.2.6 → redeploy-0.2.8}/redeploy/tests/test_discovery.py +0 -0
- {redeploy-0.2.6 → redeploy-0.2.8}/redeploy/tests/test_dsl.py +0 -0
- {redeploy-0.2.6 → redeploy-0.2.8}/redeploy/tests/test_examples.py +0 -0
- {redeploy-0.2.6 → redeploy-0.2.8}/redeploy/tests/test_executor.py +0 -0
- {redeploy-0.2.6 → redeploy-0.2.8}/redeploy/tests/test_fleet.py +0 -0
- {redeploy-0.2.6 → redeploy-0.2.8}/redeploy/tests/test_git_integration.py +0 -0
- {redeploy-0.2.6 → redeploy-0.2.8}/redeploy/tests/test_iac.py +0 -0
- {redeploy-0.2.6 → redeploy-0.2.8}/redeploy/tests/test_manifest.py +0 -0
- {redeploy-0.2.6 → redeploy-0.2.8}/redeploy/tests/test_models.py +0 -0
- {redeploy-0.2.6 → redeploy-0.2.8}/redeploy/tests/test_observe.py +0 -0
- {redeploy-0.2.6 → redeploy-0.2.8}/redeploy/tests/test_parse.py +0 -0
- {redeploy-0.2.6 → redeploy-0.2.8}/redeploy/tests/test_patterns.py +0 -0
- {redeploy-0.2.6 → redeploy-0.2.8}/redeploy/tests/test_planner_edge.py +0 -0
- {redeploy-0.2.6 → redeploy-0.2.8}/redeploy/tests/test_plugins.py +0 -0
- {redeploy-0.2.6 → redeploy-0.2.8}/redeploy/tests/test_probes.py +0 -0
- {redeploy-0.2.6 → redeploy-0.2.8}/redeploy/tests/test_public_api.py +0 -0
- {redeploy-0.2.6 → redeploy-0.2.8}/redeploy/tests/test_ssh.py +0 -0
- {redeploy-0.2.6 → redeploy-0.2.8}/redeploy/tests/test_templates.py +0 -0
- {redeploy-0.2.6 → redeploy-0.2.8}/redeploy/tests/test_verify.py +0 -0
- {redeploy-0.2.6 → redeploy-0.2.8}/redeploy/tests/test_version.py +0 -0
- {redeploy-0.2.6 → redeploy-0.2.8}/redeploy/tests/test_version_changelog.py +0 -0
- {redeploy-0.2.6 → redeploy-0.2.8}/redeploy/tests/test_version_manifest.py +0 -0
- {redeploy-0.2.6 → redeploy-0.2.8}/redeploy/tests/test_version_sources.py +0 -0
- {redeploy-0.2.6 → redeploy-0.2.8}/redeploy/tests/test_workflow.py +0 -0
- {redeploy-0.2.6 → redeploy-0.2.8}/redeploy/verify.py +0 -0
- {redeploy-0.2.6 → redeploy-0.2.8}/redeploy/version/__init__.py +0 -0
- {redeploy-0.2.6 → redeploy-0.2.8}/redeploy/version/bump.py +0 -0
- {redeploy-0.2.6 → redeploy-0.2.8}/redeploy/version/changelog.py +0 -0
- {redeploy-0.2.6 → redeploy-0.2.8}/redeploy/version/commits.py +0 -0
- {redeploy-0.2.6 → redeploy-0.2.8}/redeploy/version/diff.py +0 -0
- {redeploy-0.2.6 → redeploy-0.2.8}/redeploy/version/git_integration.py +0 -0
- {redeploy-0.2.6 → redeploy-0.2.8}/redeploy/version/git_transaction.py +0 -0
- {redeploy-0.2.6 → redeploy-0.2.8}/redeploy/version/manifest.py +0 -0
- {redeploy-0.2.6 → redeploy-0.2.8}/redeploy/version/sources/__init__.py +0 -0
- {redeploy-0.2.6 → redeploy-0.2.8}/redeploy/version/sources/base.py +0 -0
- {redeploy-0.2.6 → redeploy-0.2.8}/redeploy/version/sources/json_.py +0 -0
- {redeploy-0.2.6 → redeploy-0.2.8}/redeploy/version/sources/plain.py +0 -0
- {redeploy-0.2.6 → redeploy-0.2.8}/redeploy/version/sources/regex.py +0 -0
- {redeploy-0.2.6 → redeploy-0.2.8}/redeploy/version/sources/toml_.py +0 -0
- {redeploy-0.2.6 → redeploy-0.2.8}/redeploy/version/sources/yaml_.py +0 -0
- {redeploy-0.2.6 → redeploy-0.2.8}/redeploy/version/transaction.py +0 -0
- {redeploy-0.2.6 → redeploy-0.2.8}/redeploy/version.py +0 -0
- {redeploy-0.2.6 → redeploy-0.2.8}/redeploy.egg-info/SOURCES.txt +0 -0
- {redeploy-0.2.6 → redeploy-0.2.8}/redeploy.egg-info/dependency_links.txt +0 -0
- {redeploy-0.2.6 → redeploy-0.2.8}/redeploy.egg-info/entry_points.txt +0 -0
- {redeploy-0.2.6 → redeploy-0.2.8}/redeploy.egg-info/requires.txt +0 -0
- {redeploy-0.2.6 → redeploy-0.2.8}/redeploy.egg-info/top_level.txt +0 -0
- {redeploy-0.2.6 → redeploy-0.2.8}/setup.cfg +0 -0
- {redeploy-0.2.6 → redeploy-0.2.8}/tests/test_iac.py +0 -0
- {redeploy-0.2.6 → redeploy-0.2.8}/tests/test_parse.py +0 -0
- {redeploy-0.2.6 → redeploy-0.2.8}/tests/test_redeploy.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: redeploy
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.8
|
|
4
4
|
Summary: Infrastructure migration toolkit: detect → plan → apply
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -26,17 +26,17 @@ Requires-Dist: pfix>=0.1.60; extra == "dev"
|
|
|
26
26
|
|
|
27
27
|
## AI Cost Tracking
|
|
28
28
|
|
|
29
|
-
    
|
|
30
|
+
  
|
|
31
31
|
|
|
32
|
-
- 🤖 **LLM usage:** $
|
|
33
|
-
- 👤 **Human dev:** ~$
|
|
32
|
+
- 🤖 **LLM usage:** $4.0500 (27 commits)
|
|
33
|
+
- 👤 **Human dev:** ~$773 (7.7h @ $100/h, 30min dedup)
|
|
34
34
|
|
|
35
35
|
Generated on 2026-04-20 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
|
|
36
36
|
|
|
37
37
|
---
|
|
38
38
|
|
|
39
|
-
    
|
|
40
40
|
|
|
41
41
|
Infrastructure migration and device deploy toolkit — VPS, Raspberry Pi kiosk, Podman Quadlet, k3s.
|
|
42
42
|
|
|
@@ -3,17 +3,17 @@
|
|
|
3
3
|
|
|
4
4
|
## AI Cost Tracking
|
|
5
5
|
|
|
6
|
-
    
|
|
7
|
+
  
|
|
8
8
|
|
|
9
|
-
- 🤖 **LLM usage:** $
|
|
10
|
-
- 👤 **Human dev:** ~$
|
|
9
|
+
- 🤖 **LLM usage:** $4.0500 (27 commits)
|
|
10
|
+
- 👤 **Human dev:** ~$773 (7.7h @ $100/h, 30min dedup)
|
|
11
11
|
|
|
12
12
|
Generated on 2026-04-20 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
|
|
13
13
|
|
|
14
14
|
---
|
|
15
15
|
|
|
16
|
-
    
|
|
17
17
|
|
|
18
18
|
Infrastructure migration and device deploy toolkit — VPS, Raspberry Pi kiosk, Podman Quadlet, k3s.
|
|
19
19
|
|
|
@@ -7,7 +7,7 @@ Public API (stable from 0.2.0, semver guaranteed)::
|
|
|
7
7
|
|
|
8
8
|
Everything not listed in ``__all__`` is internal and may change without notice.
|
|
9
9
|
"""
|
|
10
|
-
__version__ = "0.2.
|
|
10
|
+
__version__ = "0.2.8"
|
|
11
11
|
|
|
12
12
|
# ── Core models ───────────────────────────────────────────────────────────────
|
|
13
13
|
from .models import ( # noqa: F401
|
|
@@ -2953,8 +2953,13 @@ def version_set(version, manifest_path_str, package_name, all_packages, dry_run,
|
|
|
2953
2953
|
|
|
2954
2954
|
@version_cmd.command(name="init")
|
|
2955
2955
|
@click.option("--scan", is_flag=True, help="Auto-detect version sources")
|
|
2956
|
+
@click.option("--review", is_flag=True, help="Review detected sources without writing manifest")
|
|
2957
|
+
@click.option("--interactive", is_flag=True,
|
|
2958
|
+
help="Interactively accept or reject detected sources before writing manifest")
|
|
2959
|
+
@click.option("--exclude", "excluded_paths", multiple=True,
|
|
2960
|
+
help="Exclude a detected source path from scan results (repeatable)")
|
|
2956
2961
|
@click.option("--force", is_flag=True, help="Overwrite existing manifest")
|
|
2957
|
-
def version_init(scan, force):
|
|
2962
|
+
def version_init(scan, review, interactive, excluded_paths, force):
|
|
2958
2963
|
"""Initialize .redeploy/version.yaml manifest."""
|
|
2959
2964
|
from rich.console import Console
|
|
2960
2965
|
from .version.manifest import VersionManifest, SourceConfig, GitConfig
|
|
@@ -2969,46 +2974,302 @@ def version_init(scan, force):
|
|
|
2969
2974
|
|
|
2970
2975
|
manifest_path.parent.mkdir(parents=True, exist_ok=True)
|
|
2971
2976
|
|
|
2972
|
-
|
|
2973
|
-
|
|
2977
|
+
root = Path.cwd()
|
|
2978
|
+
root_sources = []
|
|
2979
|
+
packages = None
|
|
2980
|
+
excluded = _normalize_scan_exclusions(excluded_paths)
|
|
2981
|
+
|
|
2982
|
+
if (review or interactive or excluded) and not scan:
|
|
2983
|
+
console.print("[red]✗ --review, --interactive and --exclude require --scan[/red]")
|
|
2984
|
+
sys.exit(1)
|
|
2985
|
+
|
|
2974
2986
|
if scan:
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
sources.append(SourceConfig(path=Path("VERSION"), format="plain"))
|
|
2978
|
-
if Path("pyproject.toml").exists():
|
|
2979
|
-
sources.append(SourceConfig(path=Path("pyproject.toml"), format="toml", key="project.version"))
|
|
2980
|
-
if Path("package.json").exists():
|
|
2981
|
-
sources.append(SourceConfig(path=Path("package.json"), format="json", key="version"))
|
|
2982
|
-
|
|
2983
|
-
if not sources:
|
|
2984
|
-
# Default minimal manifest
|
|
2985
|
-
sources = [SourceConfig(path=Path("VERSION"), format="plain")]
|
|
2986
|
-
|
|
2987
|
-
# Detect current version
|
|
2988
|
-
current = "0.1.0"
|
|
2989
|
-
for s in sources:
|
|
2990
|
-
if s.path.exists():
|
|
2991
|
-
try:
|
|
2992
|
-
from .version.sources import get_adapter
|
|
2993
|
-
current = get_adapter(s.format).read(s.path, s)
|
|
2994
|
-
break
|
|
2995
|
-
except Exception:
|
|
2996
|
-
pass
|
|
2987
|
+
root_sources = _detect_version_sources_in_dir(root, root, excluded_paths=excluded)
|
|
2988
|
+
packages = _scan_package_version_manifests(root, excluded_paths=excluded)
|
|
2997
2989
|
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
2990
|
+
if interactive:
|
|
2991
|
+
excluded.update(
|
|
2992
|
+
_review_detected_sources_interactively(console, root_sources, packages)
|
|
2993
|
+
)
|
|
2994
|
+
root_sources = _detect_version_sources_in_dir(root, root, excluded_paths=excluded)
|
|
2995
|
+
packages = _scan_package_version_manifests(root, excluded_paths=excluded)
|
|
2996
|
+
|
|
2997
|
+
if not root_sources and not packages:
|
|
2998
|
+
console.print("[yellow]⚠ No sources selected - manifest not written[/yellow]")
|
|
2999
|
+
return
|
|
3000
|
+
|
|
3001
|
+
_print_version_scan_review(console, root_sources, packages)
|
|
3002
|
+
if not click.confirm("Write manifest to .redeploy/version.yaml?", default=True):
|
|
3003
|
+
console.print("[yellow]Review complete - manifest not written[/yellow]")
|
|
3004
|
+
return
|
|
3005
|
+
|
|
3006
|
+
if review and not interactive:
|
|
3007
|
+
_print_version_scan_review(console, root_sources, packages)
|
|
3008
|
+
console.print("[yellow]Review only - manifest not written[/yellow]")
|
|
3009
|
+
return
|
|
3010
|
+
|
|
3011
|
+
if packages:
|
|
3012
|
+
current = _detect_source_version(root_sources, default="0.0.0") if root_sources else "0.0.0"
|
|
3013
|
+
m = VersionManifest(
|
|
3014
|
+
version=current,
|
|
3015
|
+
scheme="semver",
|
|
3016
|
+
policy="independent",
|
|
3017
|
+
sources=root_sources,
|
|
3018
|
+
git=GitConfig(),
|
|
3019
|
+
packages=packages,
|
|
3020
|
+
)
|
|
3021
|
+
else:
|
|
3022
|
+
sources = root_sources
|
|
3023
|
+
if not sources:
|
|
3024
|
+
sources = [SourceConfig(path=Path("VERSION"), format="plain")]
|
|
3025
|
+
|
|
3026
|
+
current = _detect_source_version(sources, default="0.1.0")
|
|
3027
|
+
m = VersionManifest(
|
|
3028
|
+
version=current,
|
|
3029
|
+
scheme="semver",
|
|
3030
|
+
policy="synced",
|
|
3031
|
+
sources=sources,
|
|
3032
|
+
git=GitConfig(),
|
|
3033
|
+
)
|
|
3005
3034
|
|
|
3006
3035
|
m.save(manifest_path)
|
|
3007
3036
|
console.print(f"[green]✓ Created {manifest_path}[/green]")
|
|
3008
3037
|
console.print(f" Current version: {current}")
|
|
3009
|
-
console.print(f"
|
|
3010
|
-
|
|
3011
|
-
|
|
3038
|
+
console.print(f" Policy: {m.policy}")
|
|
3039
|
+
|
|
3040
|
+
if m.sources:
|
|
3041
|
+
console.print(f" Sources: {len(m.sources)}")
|
|
3042
|
+
for source in m.sources:
|
|
3043
|
+
console.print(f" - {source.path} ({source.format})")
|
|
3044
|
+
|
|
3045
|
+
if m.is_monorepo():
|
|
3046
|
+
console.print(f" Packages: {len(m.packages or {})}")
|
|
3047
|
+
for package_name in m.list_packages():
|
|
3048
|
+
package = m.get_package(package_name)
|
|
3049
|
+
console.print(f" - {package_name}: {package.version} ({len(package.sources)} sources)")
|
|
3050
|
+
|
|
3051
|
+
if scan:
|
|
3052
|
+
if m.sources:
|
|
3053
|
+
_report_version_scan_conflict(console, "root", m.sources, m.version)
|
|
3054
|
+
if m.is_monorepo():
|
|
3055
|
+
for package_name in m.list_packages():
|
|
3056
|
+
package = m.get_package(package_name)
|
|
3057
|
+
_report_version_scan_conflict(console, package_name, package.sources, package.version)
|
|
3058
|
+
|
|
3059
|
+
|
|
3060
|
+
def _version_scan_specs():
|
|
3061
|
+
return [
|
|
3062
|
+
("VERSION", "plain", None),
|
|
3063
|
+
("pyproject.toml", "toml", "project.version"),
|
|
3064
|
+
("package.json", "json", "version"),
|
|
3065
|
+
]
|
|
3066
|
+
|
|
3067
|
+
|
|
3068
|
+
def _regex_version_scan_specs():
|
|
3069
|
+
return [
|
|
3070
|
+
("__init__.py", r'__version__\s*=\s*["\']([^"\']+)["\']'),
|
|
3071
|
+
("src/__init__.py", r'__version__\s*=\s*["\']([^"\']+)["\']'),
|
|
3072
|
+
("version.ts", r'(?:export\s+)?(?:const|let|var)\s+VERSION\s*=\s*["\']([^"\']+)["\']'),
|
|
3073
|
+
("src/version.ts", r'(?:export\s+)?(?:const|let|var)\s+VERSION\s*=\s*["\']([^"\']+)["\']'),
|
|
3074
|
+
("version.h", r'#define\s+[A-Z0-9_]*VERSION\s+"([^"]+)"'),
|
|
3075
|
+
("src/version.h", r'#define\s+[A-Z0-9_]*VERSION\s+"([^"]+)"'),
|
|
3076
|
+
("include/version.h", r'#define\s+[A-Z0-9_]*VERSION\s+"([^"]+)"'),
|
|
3077
|
+
]
|
|
3078
|
+
|
|
3079
|
+
|
|
3080
|
+
def _is_scannable_version_path(path: Path) -> bool:
|
|
3081
|
+
ignored = {".git", ".venv", ".redeploy", "node_modules", "__pycache__", "dist", "build"}
|
|
3082
|
+
return not any(part in ignored for part in path.parts)
|
|
3083
|
+
|
|
3084
|
+
|
|
3085
|
+
def _normalize_scan_exclusions(excluded_paths) -> set[str]:
|
|
3086
|
+
return {
|
|
3087
|
+
str(Path(path)).replace("\\", "/")
|
|
3088
|
+
for path in excluded_paths
|
|
3089
|
+
if str(path).strip()
|
|
3090
|
+
}
|
|
3091
|
+
|
|
3092
|
+
|
|
3093
|
+
def _detect_version_sources_in_dir(directory: Path, workspace_root: Path, *, excluded_paths: set[str] | None = None):
|
|
3094
|
+
from .version.manifest import SourceConfig
|
|
3095
|
+
|
|
3096
|
+
excluded_paths = excluded_paths or set()
|
|
3097
|
+
sources = []
|
|
3098
|
+
for filename, format_name, key in _version_scan_specs():
|
|
3099
|
+
candidate = directory / filename
|
|
3100
|
+
if not candidate.exists() or not candidate.is_file():
|
|
3101
|
+
continue
|
|
3102
|
+
|
|
3103
|
+
relative_path = candidate.relative_to(workspace_root)
|
|
3104
|
+
if not _is_scannable_version_path(relative_path):
|
|
3105
|
+
continue
|
|
3106
|
+
if relative_path.as_posix() in excluded_paths:
|
|
3107
|
+
continue
|
|
3108
|
+
|
|
3109
|
+
source_kwargs = {
|
|
3110
|
+
"path": relative_path,
|
|
3111
|
+
"format": format_name,
|
|
3112
|
+
}
|
|
3113
|
+
if key is not None:
|
|
3114
|
+
source_kwargs["key"] = key
|
|
3115
|
+
sources.append(SourceConfig(**source_kwargs))
|
|
3116
|
+
|
|
3117
|
+
for relative_filename, pattern in _regex_version_scan_specs():
|
|
3118
|
+
candidate = directory / relative_filename
|
|
3119
|
+
if not candidate.exists() or not candidate.is_file():
|
|
3120
|
+
continue
|
|
3121
|
+
|
|
3122
|
+
relative_path = candidate.relative_to(workspace_root)
|
|
3123
|
+
if not _is_scannable_version_path(relative_path):
|
|
3124
|
+
continue
|
|
3125
|
+
if relative_path.as_posix() in excluded_paths:
|
|
3126
|
+
continue
|
|
3127
|
+
|
|
3128
|
+
sources.append(SourceConfig(
|
|
3129
|
+
path=relative_path,
|
|
3130
|
+
format="regex",
|
|
3131
|
+
pattern=pattern,
|
|
3132
|
+
))
|
|
3133
|
+
|
|
3134
|
+
return sources
|
|
3135
|
+
|
|
3136
|
+
|
|
3137
|
+
def _detect_source_version(sources, *, default: str):
|
|
3138
|
+
detected = _read_detected_source_versions(sources)
|
|
3139
|
+
return detected[0][1] if detected else default
|
|
3140
|
+
|
|
3141
|
+
|
|
3142
|
+
def _read_detected_source_versions(sources):
|
|
3143
|
+
from .version.sources import get_adapter
|
|
3144
|
+
|
|
3145
|
+
detected = []
|
|
3146
|
+
for source in sources:
|
|
3147
|
+
try:
|
|
3148
|
+
detected.append((source, get_adapter(source.format).read(source.path, source)))
|
|
3149
|
+
except Exception:
|
|
3150
|
+
continue
|
|
3151
|
+
return detected
|
|
3152
|
+
|
|
3153
|
+
|
|
3154
|
+
def _report_version_scan_conflict(console, label: str, sources, chosen_version: str):
|
|
3155
|
+
detected = _read_detected_source_versions(sources)
|
|
3156
|
+
unique_versions = {version for _, version in detected}
|
|
3157
|
+
if len(unique_versions) <= 1:
|
|
3158
|
+
return
|
|
3159
|
+
|
|
3160
|
+
details = ", ".join(f"{source.path}={version}" for source, version in detected)
|
|
3161
|
+
console.print(
|
|
3162
|
+
f"[yellow]⚠ Version conflict in {label}: {details}; using {chosen_version}[/yellow]"
|
|
3163
|
+
)
|
|
3164
|
+
|
|
3165
|
+
|
|
3166
|
+
def _print_version_scan_review(console, root_sources, packages):
|
|
3167
|
+
console.print("[bold]Scan review[/bold]")
|
|
3168
|
+
|
|
3169
|
+
if root_sources:
|
|
3170
|
+
_print_version_scan_group(console, "root", root_sources, default_version="0.0.0")
|
|
3171
|
+
|
|
3172
|
+
if packages:
|
|
3173
|
+
for package_name, package in packages.items():
|
|
3174
|
+
_print_version_scan_group(console, package_name, package.sources, default_version=package.version)
|
|
3175
|
+
|
|
3176
|
+
if not root_sources and not packages:
|
|
3177
|
+
console.print(" No version sources detected")
|
|
3178
|
+
|
|
3179
|
+
|
|
3180
|
+
def _print_version_scan_group(console, label: str, sources, *, default_version: str):
|
|
3181
|
+
detected = _read_detected_source_versions(sources)
|
|
3182
|
+
detected_map = {str(source.path): version for source, version in detected}
|
|
3183
|
+
unique_versions = {version for _, version in detected}
|
|
3184
|
+
chosen_version = detected[0][1] if detected else default_version
|
|
3185
|
+
suffix = " (conflict)" if len(unique_versions) > 1 else ""
|
|
3186
|
+
|
|
3187
|
+
console.print(f" {label}: chosen version {chosen_version}{suffix}")
|
|
3188
|
+
for source in sources:
|
|
3189
|
+
actual = detected_map.get(str(source.path), "(unreadable)")
|
|
3190
|
+
marker = " (conflict)" if len(unique_versions) > 1 and actual != chosen_version else ""
|
|
3191
|
+
console.print(f" - {source.path} ({source.format}) current: {actual}{marker}")
|
|
3192
|
+
|
|
3193
|
+
|
|
3194
|
+
def _iter_version_scan_groups(root_sources, packages):
|
|
3195
|
+
if root_sources:
|
|
3196
|
+
yield "root", root_sources, "0.0.0"
|
|
3197
|
+
|
|
3198
|
+
if packages:
|
|
3199
|
+
for package_name, package in packages.items():
|
|
3200
|
+
yield package_name, package.sources, package.version
|
|
3201
|
+
|
|
3202
|
+
|
|
3203
|
+
def _review_detected_sources_interactively(console, root_sources, packages) -> set[str]:
|
|
3204
|
+
rejected = set()
|
|
3205
|
+
console.print("[bold]Interactive scan review[/bold]")
|
|
3206
|
+
|
|
3207
|
+
for label, sources, default_version in _iter_version_scan_groups(root_sources, packages):
|
|
3208
|
+
detected = _read_detected_source_versions(sources)
|
|
3209
|
+
detected_map = {str(source.path): version for source, version in detected}
|
|
3210
|
+
unique_versions = {version for _, version in detected}
|
|
3211
|
+
chosen_version = detected[0][1] if detected else default_version
|
|
3212
|
+
suffix = " (conflict)" if len(unique_versions) > 1 else ""
|
|
3213
|
+
|
|
3214
|
+
console.print(f"\n[bold]{label}[/bold]: chosen version {chosen_version}{suffix}")
|
|
3215
|
+
for source in sources:
|
|
3216
|
+
actual = detected_map.get(str(source.path), "(unreadable)")
|
|
3217
|
+
keep = click.confirm(
|
|
3218
|
+
f"Keep {source.path} ({source.format}) current={actual}?",
|
|
3219
|
+
default=True,
|
|
3220
|
+
)
|
|
3221
|
+
if not keep:
|
|
3222
|
+
rejected.add(source.path.as_posix())
|
|
3223
|
+
|
|
3224
|
+
return rejected
|
|
3225
|
+
|
|
3226
|
+
|
|
3227
|
+
def _derive_scanned_package_name(package_dir: Path, workspace_root: Path, used_names: set[str]) -> str:
|
|
3228
|
+
relative_dir = package_dir.relative_to(workspace_root)
|
|
3229
|
+
if len(relative_dir.parts) > 1 and relative_dir.parts[0] in {"packages", "apps", "services", "modules"}:
|
|
3230
|
+
candidate = relative_dir.parts[-1]
|
|
3231
|
+
else:
|
|
3232
|
+
candidate = relative_dir.as_posix()
|
|
3233
|
+
|
|
3234
|
+
if candidate in used_names:
|
|
3235
|
+
candidate = relative_dir.as_posix().replace("/", "-")
|
|
3236
|
+
|
|
3237
|
+
return candidate
|
|
3238
|
+
|
|
3239
|
+
|
|
3240
|
+
def _scan_package_version_manifests(workspace_root: Path, *, excluded_paths: set[str] | None = None):
|
|
3241
|
+
from .version.manifest import PackageConfig
|
|
3242
|
+
|
|
3243
|
+
excluded_paths = excluded_paths or set()
|
|
3244
|
+
package_dirs = []
|
|
3245
|
+
for candidate in sorted(workspace_root.rglob("*")):
|
|
3246
|
+
if not candidate.is_dir() or candidate == workspace_root:
|
|
3247
|
+
continue
|
|
3248
|
+
|
|
3249
|
+
relative_path = candidate.relative_to(workspace_root)
|
|
3250
|
+
if not _is_scannable_version_path(relative_path):
|
|
3251
|
+
continue
|
|
3252
|
+
if candidate.name in {"src", "include"}:
|
|
3253
|
+
continue
|
|
3254
|
+
|
|
3255
|
+
if _detect_version_sources_in_dir(candidate, workspace_root, excluded_paths=excluded_paths):
|
|
3256
|
+
package_dirs.append(candidate)
|
|
3257
|
+
|
|
3258
|
+
packages = {}
|
|
3259
|
+
used_names = set()
|
|
3260
|
+
for package_dir in package_dirs:
|
|
3261
|
+
sources = _detect_version_sources_in_dir(package_dir, workspace_root, excluded_paths=excluded_paths)
|
|
3262
|
+
if not sources:
|
|
3263
|
+
continue
|
|
3264
|
+
|
|
3265
|
+
package_name = _derive_scanned_package_name(package_dir, workspace_root, used_names)
|
|
3266
|
+
used_names.add(package_name)
|
|
3267
|
+
packages[package_name] = PackageConfig(
|
|
3268
|
+
version=_detect_source_version(sources, default="0.1.0"),
|
|
3269
|
+
sources=sources,
|
|
3270
|
+
)
|
|
3271
|
+
|
|
3272
|
+
return packages or None
|
|
3012
3273
|
|
|
3013
3274
|
|
|
3014
3275
|
@version_cmd.command(name="diff")
|
|
@@ -238,6 +238,206 @@ class TestVersionCurrent:
|
|
|
238
238
|
assert "2.0.0" in result.output
|
|
239
239
|
|
|
240
240
|
|
|
241
|
+
class TestVersionInit:
|
|
242
|
+
def test_init_scan_creates_synced_manifest_from_root_sources(self, tmp_path):
|
|
243
|
+
runner = _runner()
|
|
244
|
+
with runner.isolated_filesystem(temp_dir=tmp_path):
|
|
245
|
+
Path("VERSION").write_text("2.3.4\n", encoding="utf-8")
|
|
246
|
+
|
|
247
|
+
result = runner.invoke(
|
|
248
|
+
cli,
|
|
249
|
+
["version", "init", "--scan"],
|
|
250
|
+
catch_exceptions=False,
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
assert result.exit_code == 0, result.output
|
|
254
|
+
manifest = yaml.safe_load(Path(".redeploy/version.yaml").read_text(encoding="utf-8"))
|
|
255
|
+
assert manifest["version"]["version"] == "2.3.4"
|
|
256
|
+
assert manifest["version"]["policy"] == "synced"
|
|
257
|
+
assert manifest["version"]["sources"] == [
|
|
258
|
+
{"path": "VERSION", "format": "plain", "optional": False}
|
|
259
|
+
]
|
|
260
|
+
|
|
261
|
+
def test_init_scan_creates_independent_manifest_from_package_dirs(self, tmp_path):
|
|
262
|
+
runner = _runner()
|
|
263
|
+
with runner.isolated_filesystem(temp_dir=tmp_path):
|
|
264
|
+
Path("backend").mkdir(parents=True, exist_ok=True)
|
|
265
|
+
Path("frontend").mkdir(parents=True, exist_ok=True)
|
|
266
|
+
Path("backend/VERSION").write_text("1.0.0\n", encoding="utf-8")
|
|
267
|
+
Path("frontend/package.json").write_text(
|
|
268
|
+
'{"name": "frontend", "version": "2.0.0"}\n',
|
|
269
|
+
encoding="utf-8",
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
result = runner.invoke(
|
|
273
|
+
cli,
|
|
274
|
+
["version", "init", "--scan"],
|
|
275
|
+
catch_exceptions=False,
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
assert result.exit_code == 0, result.output
|
|
279
|
+
manifest = yaml.safe_load(Path(".redeploy/version.yaml").read_text(encoding="utf-8"))
|
|
280
|
+
assert manifest["version"]["policy"] == "independent"
|
|
281
|
+
assert manifest["version"]["version"] == "0.0.0"
|
|
282
|
+
assert manifest["version"].get("sources", []) == []
|
|
283
|
+
assert manifest["version"]["packages"]["backend"]["version"] == "1.0.0"
|
|
284
|
+
assert manifest["version"]["packages"]["backend"]["sources"] == [
|
|
285
|
+
{"path": "backend/VERSION", "format": "plain", "optional": False}
|
|
286
|
+
]
|
|
287
|
+
assert manifest["version"]["packages"]["frontend"]["version"] == "2.0.0"
|
|
288
|
+
assert manifest["version"]["packages"]["frontend"]["sources"] == [
|
|
289
|
+
{"path": "frontend/package.json", "format": "json", "key": "version", "optional": False}
|
|
290
|
+
]
|
|
291
|
+
assert "Policy: independent" in result.output
|
|
292
|
+
assert "Packages: 2" in result.output
|
|
293
|
+
|
|
294
|
+
def test_init_scan_detects_regex_source_in_nested_package(self, tmp_path):
|
|
295
|
+
runner = _runner()
|
|
296
|
+
with runner.isolated_filesystem(temp_dir=tmp_path):
|
|
297
|
+
Path("packages/frontend/src").mkdir(parents=True, exist_ok=True)
|
|
298
|
+
Path("packages/frontend/src/version.ts").write_text(
|
|
299
|
+
'export const VERSION = "2.1.0"\n',
|
|
300
|
+
encoding="utf-8",
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
result = runner.invoke(
|
|
304
|
+
cli,
|
|
305
|
+
["version", "init", "--scan"],
|
|
306
|
+
catch_exceptions=False,
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
assert result.exit_code == 0, result.output
|
|
310
|
+
manifest = yaml.safe_load(Path(".redeploy/version.yaml").read_text(encoding="utf-8"))
|
|
311
|
+
assert manifest["version"]["policy"] == "independent"
|
|
312
|
+
assert manifest["version"]["packages"]["frontend"]["version"] == "2.1.0"
|
|
313
|
+
source = manifest["version"]["packages"]["frontend"]["sources"][0]
|
|
314
|
+
assert source["path"] == "packages/frontend/src/version.ts"
|
|
315
|
+
assert source["format"] == "regex"
|
|
316
|
+
assert source["optional"] is False
|
|
317
|
+
assert "VERSION" in source["pattern"]
|
|
318
|
+
assert "const|let|var" in source["pattern"]
|
|
319
|
+
|
|
320
|
+
def test_init_scan_reports_conflict_between_scanned_sources(self, tmp_path):
|
|
321
|
+
runner = _runner()
|
|
322
|
+
with runner.isolated_filesystem(temp_dir=tmp_path):
|
|
323
|
+
Path("backend/src").mkdir(parents=True, exist_ok=True)
|
|
324
|
+
Path("backend/VERSION").write_text("1.0.0\n", encoding="utf-8")
|
|
325
|
+
Path("backend/src/version.ts").write_text(
|
|
326
|
+
'export const VERSION = "1.1.0"\n',
|
|
327
|
+
encoding="utf-8",
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
result = runner.invoke(
|
|
331
|
+
cli,
|
|
332
|
+
["version", "init", "--scan"],
|
|
333
|
+
catch_exceptions=False,
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
assert result.exit_code == 0, result.output
|
|
337
|
+
assert "Version conflict in backend" in result.output
|
|
338
|
+
assert "backend/VERSION=1.0.0" in result.output
|
|
339
|
+
assert "backend/src/version.ts=1.1.0" in result.output
|
|
340
|
+
assert "using 1.0.0" in result.output
|
|
341
|
+
|
|
342
|
+
manifest = yaml.safe_load(Path(".redeploy/version.yaml").read_text(encoding="utf-8"))
|
|
343
|
+
assert manifest["version"]["packages"]["backend"]["version"] == "1.0.0"
|
|
344
|
+
|
|
345
|
+
def test_init_scan_review_lists_detected_sources_without_writing_manifest(self, tmp_path):
|
|
346
|
+
runner = _runner()
|
|
347
|
+
with runner.isolated_filesystem(temp_dir=tmp_path):
|
|
348
|
+
Path("backend/src").mkdir(parents=True, exist_ok=True)
|
|
349
|
+
Path("backend/VERSION").write_text("1.0.0\n", encoding="utf-8")
|
|
350
|
+
Path("backend/src/version.ts").write_text(
|
|
351
|
+
'export const VERSION = "1.1.0"\n',
|
|
352
|
+
encoding="utf-8",
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
result = runner.invoke(
|
|
356
|
+
cli,
|
|
357
|
+
["version", "init", "--scan", "--review"],
|
|
358
|
+
catch_exceptions=False,
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
assert result.exit_code == 0, result.output
|
|
362
|
+
assert "Scan review" in result.output
|
|
363
|
+
assert "backend: chosen version 1.0.0 (conflict)" in result.output
|
|
364
|
+
assert "backend/VERSION (plain) current: 1.0.0" in result.output
|
|
365
|
+
assert "backend/src/version.ts (regex) current: 1.1.0 (conflict)" in result.output
|
|
366
|
+
assert "Review only - manifest not written" in result.output
|
|
367
|
+
assert not Path(".redeploy/version.yaml").exists()
|
|
368
|
+
|
|
369
|
+
def test_init_scan_interactive_can_reject_source_and_write_manifest(self, tmp_path):
|
|
370
|
+
runner = _runner()
|
|
371
|
+
with runner.isolated_filesystem(temp_dir=tmp_path):
|
|
372
|
+
Path("backend/src").mkdir(parents=True, exist_ok=True)
|
|
373
|
+
Path("backend/VERSION").write_text("1.0.0\n", encoding="utf-8")
|
|
374
|
+
Path("backend/src/version.ts").write_text(
|
|
375
|
+
'export const VERSION = "1.1.0"\n',
|
|
376
|
+
encoding="utf-8",
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
result = runner.invoke(
|
|
380
|
+
cli,
|
|
381
|
+
["version", "init", "--scan", "--interactive"],
|
|
382
|
+
input="\nn\n\n",
|
|
383
|
+
catch_exceptions=False,
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
assert result.exit_code == 0, result.output
|
|
387
|
+
assert "Interactive scan review" in result.output
|
|
388
|
+
assert "Keep backend/VERSION (plain) current=1.0.0?" in result.output
|
|
389
|
+
assert "Keep backend/src/version.ts (regex) current=1.1.0?" in result.output
|
|
390
|
+
assert "Write manifest to .redeploy/version.yaml?" in result.output
|
|
391
|
+
|
|
392
|
+
manifest = yaml.safe_load(Path(".redeploy/version.yaml").read_text(encoding="utf-8"))
|
|
393
|
+
assert manifest["version"]["packages"]["backend"]["version"] == "1.0.0"
|
|
394
|
+
assert manifest["version"]["packages"]["backend"]["sources"] == [
|
|
395
|
+
{"path": "backend/VERSION", "format": "plain", "optional": False}
|
|
396
|
+
]
|
|
397
|
+
|
|
398
|
+
def test_init_interactive_requires_scan(self, tmp_path):
|
|
399
|
+
runner = _runner()
|
|
400
|
+
with runner.isolated_filesystem(temp_dir=tmp_path):
|
|
401
|
+
result = runner.invoke(
|
|
402
|
+
cli,
|
|
403
|
+
["version", "init", "--interactive"],
|
|
404
|
+
catch_exceptions=False,
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
assert result.exit_code == 1, result.output
|
|
408
|
+
assert "--review, --interactive and --exclude require --scan" in result.output
|
|
409
|
+
|
|
410
|
+
def test_init_scan_exclude_omits_detected_source_from_manifest(self, tmp_path):
|
|
411
|
+
runner = _runner()
|
|
412
|
+
with runner.isolated_filesystem(temp_dir=tmp_path):
|
|
413
|
+
Path("backend/src").mkdir(parents=True, exist_ok=True)
|
|
414
|
+
Path("backend/VERSION").write_text("1.0.0\n", encoding="utf-8")
|
|
415
|
+
Path("backend/src/version.ts").write_text(
|
|
416
|
+
'export const VERSION = "1.1.0"\n',
|
|
417
|
+
encoding="utf-8",
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
result = runner.invoke(
|
|
421
|
+
cli,
|
|
422
|
+
[
|
|
423
|
+
"version",
|
|
424
|
+
"init",
|
|
425
|
+
"--scan",
|
|
426
|
+
"--exclude",
|
|
427
|
+
"backend/src/version.ts",
|
|
428
|
+
],
|
|
429
|
+
catch_exceptions=False,
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
assert result.exit_code == 0, result.output
|
|
433
|
+
manifest = yaml.safe_load(Path(".redeploy/version.yaml").read_text(encoding="utf-8"))
|
|
434
|
+
assert manifest["version"]["packages"]["backend"]["version"] == "1.0.0"
|
|
435
|
+
assert manifest["version"]["packages"]["backend"]["sources"] == [
|
|
436
|
+
{"path": "backend/VERSION", "format": "plain", "optional": False}
|
|
437
|
+
]
|
|
438
|
+
assert "Version conflict in backend" not in result.output
|
|
439
|
+
|
|
440
|
+
|
|
241
441
|
class TestVersionVerify:
|
|
242
442
|
def test_verify_all_packages_succeeds_when_in_sync(self, tmp_path):
|
|
243
443
|
runner = _runner()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: redeploy
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.8
|
|
4
4
|
Summary: Infrastructure migration toolkit: detect → plan → apply
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -26,17 +26,17 @@ Requires-Dist: pfix>=0.1.60; extra == "dev"
|
|
|
26
26
|
|
|
27
27
|
## AI Cost Tracking
|
|
28
28
|
|
|
29
|
-
    
|
|
30
|
+
  
|
|
31
31
|
|
|
32
|
-
- 🤖 **LLM usage:** $
|
|
33
|
-
- 👤 **Human dev:** ~$
|
|
32
|
+
- 🤖 **LLM usage:** $4.0500 (27 commits)
|
|
33
|
+
- 👤 **Human dev:** ~$773 (7.7h @ $100/h, 30min dedup)
|
|
34
34
|
|
|
35
35
|
Generated on 2026-04-20 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
|
|
36
36
|
|
|
37
37
|
---
|
|
38
38
|
|
|
39
|
-
    
|
|
40
40
|
|
|
41
41
|
Infrastructure migration and device deploy toolkit — VPS, Raspberry Pi kiosk, Podman Quadlet, k3s.
|
|
42
42
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|