Coverage for src / rhiza / models / _git / snapshot.py: 100%
43 statements
« prev ^ index » next coverage.py v7.14.0, created at 2026-07-16 12:51 +0000
« prev ^ index » next coverage.py v7.14.0, created at 2026-07-16 12:51 +0000
1"""Snapshot preparation helpers: expand, exclude, remap, and copy files."""
3import os
4import shutil
5from pathlib import Path
7from loguru import logger
10def _expand_paths(base_dir: Path, paths: list[str]) -> list[Path]:
11 """Expand file/directory paths relative to *base_dir* into individual files.
13 Args:
14 base_dir: Root directory to resolve against.
15 paths: Relative path strings.
17 Returns:
18 Flat list of file paths.
19 """
20 all_files: list[Path] = []
21 for p in paths:
22 full = base_dir / p
23 if full.is_file():
24 all_files.append(full)
25 elif full.is_dir():
26 all_files.extend(
27 Path(dirpath) / fname
28 for dirpath, _, filenames in os.walk(full, followlinks=True)
29 for fname in filenames
30 )
31 else:
32 logger.debug(f"Path not found in template repository: {p}")
33 return all_files
36def _excluded_set(base_dir: Path, excluded_paths: list[str]) -> set[str]:
37 """Build a set of relative path strings that should be excluded.
39 Args:
40 base_dir: Root of the template clone.
41 excluded_paths: User-configured exclude list.
43 Returns:
44 Set of relative path strings (always includes rhiza internals).
45 """
46 result: set[str] = set()
47 for f in _expand_paths(base_dir, excluded_paths):
48 result.add(str(f.relative_to(base_dir)))
49 result.add(".rhiza/template.yml")
50 return result
53def _remap_path(source: str, path_map: dict[str, str]) -> str:
54 """Translate *source* to its destination path using *path_map*.
56 Supports both exact file matches and directory-prefix matches. A prefix
57 match is triggered when a key ends with ``/`` or when *source* starts with
58 ``<key>/``.
60 Args:
61 source: Source-relative path from the template clone.
62 path_map: Mapping of source path → destination path.
64 Returns:
65 The destination path, or *source* unchanged when no mapping applies.
66 """
67 if source in path_map:
68 return path_map[source]
69 for src, dest in path_map.items():
70 src_prefix = src.rstrip("/") + "/"
71 if source.startswith(src_prefix):
72 suffix = source[len(src_prefix) :]
73 if dest.rstrip("/"):
74 return dest.rstrip("/") + "/" + suffix
75 return suffix
76 return source
79def _prepare_snapshot(
80 clone_dir: Path,
81 include_paths: list[str],
82 excludes: set[str],
83 snapshot_dir: Path,
84 path_map: dict[str, str] | None = None,
85) -> list[Path]:
86 """Copy included (non-excluded) files from a clone into a snapshot directory.
88 When *path_map* is provided, files are written at their destination paths
89 (rather than their source paths) so that downstream diffs and merges operate
90 on the correct target locations.
92 Args:
93 clone_dir: Root of the template clone.
94 include_paths: Source paths to include.
95 excludes: Set of relative source paths to exclude.
96 snapshot_dir: Destination directory for the snapshot.
97 path_map: Optional source→destination path mapping. Keys may be exact
98 file paths or directory prefixes.
100 Returns:
101 List of relative destination file paths that were copied.
102 """
103 effective_map = path_map or {}
104 template_files: list[Path] = []
105 for f in _expand_paths(clone_dir, include_paths):
106 rel_source = str(f.relative_to(clone_dir))
107 if rel_source not in excludes:
108 rel_dest = _remap_path(rel_source, effective_map)
109 dst = snapshot_dir / rel_dest
110 dst.parent.mkdir(parents=True, exist_ok=True)
111 shutil.copy2(f, dst)
112 template_files.append(Path(rel_dest))
113 return template_files