Coverage for src / rhiza / commands / uninstall.py: 100%
90 statements
« prev ^ index » next coverage.py v7.13.1, created at 2025-12-29 01:59 +0000
« prev ^ index » next coverage.py v7.13.1, created at 2025-12-29 01:59 +0000
1"""Command for uninstalling Rhiza template files from a repository.
3This module implements the `uninstall` command. It reads the `.rhiza/history`
4file and removes all files that were previously materialized by Rhiza templates.
5This provides a clean way to remove all template-managed files from a project.
6"""
8import sys
9from pathlib import Path
11from loguru import logger
14def uninstall(target: Path, force: bool) -> None:
15 """Uninstall Rhiza templates from the target repository.
17 Reads the `.rhiza/history` file and removes all files listed in it.
18 This effectively removes all files that were materialized by Rhiza.
20 Args:
21 target (Path): Path to the target repository.
22 force (bool): If True, skip confirmation prompt and proceed with deletion.
23 """
24 # Resolve to absolute path to avoid any ambiguity
25 target = target.resolve()
27 logger.info(f"Target repository: {target}")
29 # Check for history file in new location only
30 history_file = target / ".rhiza" / "history"
32 if not history_file.exists():
33 logger.warning(f"No history file found at: {history_file.relative_to(target)}")
34 logger.info("Nothing to uninstall. This repository may not have Rhiza templates materialized.")
35 logger.info("If you haven't migrated yet, run 'rhiza migrate' first.")
36 return
38 # Read the history file
39 logger.debug(f"Reading history file: {history_file.relative_to(target)}")
40 files_to_remove: list[Path] = []
42 with history_file.open("r", encoding="utf-8") as f:
43 for line in f:
44 line = line.strip()
45 # Skip comments and empty lines
46 if line and not line.startswith("#"):
47 file_path = Path(line)
48 files_to_remove.append(file_path)
50 if not files_to_remove:
51 logger.warning("History file is empty (only contains comments)")
52 logger.info("Nothing to uninstall.")
53 return
55 logger.info(f"Found {len(files_to_remove)} file(s) to remove")
57 # Show confirmation prompt unless --force is used
58 if not force:
59 logger.warning("This will remove the following files from your repository:")
60 for file_path in sorted(files_to_remove):
61 full_path = target / file_path
62 if full_path.exists():
63 logger.warning(f" - {file_path}")
64 else:
65 logger.debug(f" - {file_path} (already deleted)")
67 # Prompt for confirmation
68 try:
69 response = input("\nAre you sure you want to proceed? [y/N]: ").strip().lower()
70 if response not in ("y", "yes"):
71 logger.info("Uninstall cancelled by user")
72 return
73 except (KeyboardInterrupt, EOFError):
74 logger.info("\nUninstall cancelled by user")
75 return
77 # Remove files
78 logger.info("Removing files...")
79 removed_count = 0
80 skipped_count = 0
81 error_count = 0
83 for file_path in sorted(files_to_remove):
84 full_path = target / file_path
86 if not full_path.exists():
87 logger.debug(f"[SKIP] {file_path} (already deleted)")
88 skipped_count += 1
89 continue
91 try:
92 full_path.unlink()
93 logger.success(f"[DEL] {file_path}")
94 removed_count += 1
95 except Exception as e:
96 logger.error(f"Failed to delete {file_path}: {e}")
97 error_count += 1
99 # Clean up empty directories
100 logger.debug("Cleaning up empty directories...")
101 empty_dirs_removed = 0
102 for file_path in sorted(files_to_remove, reverse=True):
103 full_path = target / file_path
104 parent = full_path.parent
106 # Try to remove parent directories if they're empty
107 # Walk up the directory tree
108 while parent != target and parent.exists():
109 try:
110 # Only remove if directory is empty
111 if parent.is_dir() and not any(parent.iterdir()):
112 parent.rmdir()
113 logger.debug(f"[DEL] {parent.relative_to(target)}/ (empty directory)")
114 empty_dirs_removed += 1
115 parent = parent.parent
116 else:
117 break
118 except Exception:
119 # Directory not empty or other error, stop walking up
120 break
122 # Remove history file itself
123 try:
124 history_file.unlink()
125 logger.success(f"[DEL] {history_file.relative_to(target)}")
126 removed_count += 1
127 except Exception as e:
128 logger.error(f"Failed to delete {history_file.relative_to(target)}: {e}")
129 error_count += 1
131 # Summary
132 logger.info("\nUninstall summary:")
133 logger.info(f" Files removed: {removed_count}")
134 if skipped_count > 0:
135 logger.info(f" Files skipped (already deleted): {skipped_count}")
136 if empty_dirs_removed > 0:
137 logger.info(f" Empty directories removed: {empty_dirs_removed}")
138 if error_count > 0:
139 logger.error(f" Errors encountered: {error_count}")
140 sys.exit(1)
142 logger.success("Rhiza templates uninstalled successfully")
143 logger.info(
144 "Next steps:\n"
145 " Review changes:\n"
146 " git status\n"
147 " git diff\n\n"
148 " Commit:\n"
149 " git add .\n"
150 ' git commit -m "chore: remove rhiza templates"'
151 )