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

1"""Snapshot preparation helpers: expand, exclude, remap, and copy files.""" 

2 

3import os 

4import shutil 

5from pathlib import Path 

6 

7from loguru import logger 

8 

9 

10def _expand_paths(base_dir: Path, paths: list[str]) -> list[Path]: 

11 """Expand file/directory paths relative to *base_dir* into individual files. 

12 

13 Args: 

14 base_dir: Root directory to resolve against. 

15 paths: Relative path strings. 

16 

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 

34 

35 

36def _excluded_set(base_dir: Path, excluded_paths: list[str]) -> set[str]: 

37 """Build a set of relative path strings that should be excluded. 

38 

39 Args: 

40 base_dir: Root of the template clone. 

41 excluded_paths: User-configured exclude list. 

42 

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 

51 

52 

53def _remap_path(source: str, path_map: dict[str, str]) -> str: 

54 """Translate *source* to its destination path using *path_map*. 

55 

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>/``. 

59 

60 Args: 

61 source: Source-relative path from the template clone. 

62 path_map: Mapping of source path → destination path. 

63 

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 

77 

78 

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. 

87 

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. 

91 

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. 

99 

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