rhiza 0.8.3__py3-none-any.whl → 0.8.4__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.
rhiza/commands/migrate.py CHANGED
@@ -13,30 +13,15 @@ from loguru import logger
13
13
  from rhiza.models import RhizaTemplate
14
14
 
15
15
 
16
- def migrate(target: Path) -> None:
17
- """Migrate project to use the new .rhiza folder structure.
18
-
19
- This command performs the following actions:
20
- 1. Creates the `.rhiza/` directory in the project root
21
- 2. Moves template.yml from `.github/rhiza/` or `.github/` to `.rhiza/template.yml`
22
- 3. Moves `.rhiza.history` to `.rhiza/history` if it exists
23
- 4. Provides instructions for next steps
24
-
25
- The `.rhiza/` folder will contain:
26
- - `template.yml` - Template configuration (replaces `.github/rhiza/template.yml`)
27
- - `history` - List of files managed by Rhiza templates (replaces `.rhiza.history`)
28
- - Future: Additional state, cache, or metadata files
16
+ def _create_rhiza_directory(target: Path) -> Path:
17
+ """Create .rhiza directory if it doesn't exist.
29
18
 
30
19
  Args:
31
- target (Path): Path to the target repository.
32
- """
33
- # Resolve to absolute path
34
- target = target.resolve()
35
-
36
- logger.info(f"Migrating Rhiza structure in: {target}")
37
- logger.info("This will create the .rhiza folder and migrate configuration files")
20
+ target: Target repository path.
38
21
 
39
- # Create .rhiza directory
22
+ Returns:
23
+ Path to .rhiza directory.
24
+ """
40
25
  rhiza_dir = target / ".rhiza"
41
26
  if not rhiza_dir.exists():
42
27
  logger.info(f"Creating .rhiza directory at: {rhiza_dir.relative_to(target)}")
@@ -44,21 +29,30 @@ def migrate(target: Path) -> None:
44
29
  logger.success(f"✓ Created {rhiza_dir.relative_to(target)}")
45
30
  else:
46
31
  logger.debug(f".rhiza directory already exists at: {rhiza_dir.relative_to(target)}")
32
+ return rhiza_dir
47
33
 
48
- # Track what was migrated for summary
49
- migrations_performed = []
50
34
 
51
- # Migrate template.yml from .github to .rhiza if it exists
35
+ def _migrate_template_file(target: Path, rhiza_dir: Path) -> tuple[bool, list[str]]:
36
+ """Migrate template.yml from .github to .rhiza.
37
+
38
+ Args:
39
+ target: Target repository path.
40
+ rhiza_dir: Path to .rhiza directory.
41
+
42
+ Returns:
43
+ Tuple of (migration_performed, migrations_list).
44
+ """
52
45
  github_dir = target / ".github"
53
46
  new_template_file = rhiza_dir / "template.yml"
54
47
 
55
- # Check possible locations for template.yml in .github
56
48
  possible_template_locations = [
57
49
  github_dir / "rhiza" / "template.yml",
58
50
  github_dir / "template.yml",
59
51
  ]
60
52
 
53
+ migrations_performed = []
61
54
  template_migrated = False
55
+
62
56
  for old_template_file in possible_template_locations:
63
57
  if old_template_file.exists():
64
58
  if new_template_file.exists():
@@ -68,8 +62,6 @@ def migrate(target: Path) -> None:
68
62
  else:
69
63
  logger.info(f"Found template.yml at: {old_template_file.relative_to(target)}")
70
64
  logger.info(f"Moving to new location: {new_template_file.relative_to(target)}")
71
-
72
- # Move the template file to new location (not copy)
73
65
  shutil.move(str(old_template_file), str(new_template_file))
74
66
  logger.success("✓ Moved template.yml to .rhiza/template.yml")
75
67
  migrations_performed.append("Moved template.yml to .rhiza/template.yml")
@@ -83,26 +75,42 @@ def migrate(target: Path) -> None:
83
75
  logger.warning("No existing template.yml file found in .github")
84
76
  logger.info("You may need to run 'rhiza init' to create a template configuration")
85
77
 
86
- # Ensure the .rhiza folder is included in template.yml include list (if template exists)
87
- template_file = new_template_file
88
- if template_file.exists():
89
- # Load existing template configuration
90
- template = RhizaTemplate.from_yaml(template_file)
91
- template_include = template.include or []
92
- if ".rhiza" not in template_include:
93
- logger.warning("The .rhiza folder is not included in your template.yml")
94
- template_include.append(".rhiza")
95
- logger.info("The .rhiza folder is added to your template.yml to ensure it's included in your repository")
96
-
97
- # Save the updated template.yml
98
- template.include = template_include
99
- template.to_yaml(template_file)
100
- else:
78
+ return template_migrated or new_template_file.exists(), migrations_performed
79
+
80
+
81
+ def _ensure_rhiza_in_include(template_file: Path) -> None:
82
+ """Ensure .rhiza folder is in template.yml include list.
83
+
84
+ Args:
85
+ template_file: Path to template.yml file.
86
+ """
87
+ if not template_file.exists():
101
88
  logger.debug("No template.yml present in .rhiza; skipping include update")
89
+ return
90
+
91
+ template = RhizaTemplate.from_yaml(template_file)
92
+ template_include = template.include or []
93
+ if ".rhiza" not in template_include:
94
+ logger.warning("The .rhiza folder is not included in your template.yml")
95
+ template_include.append(".rhiza")
96
+ logger.info("The .rhiza folder is added to your template.yml to ensure it's included in your repository")
97
+ template.include = template_include
98
+ template.to_yaml(template_file)
99
+
100
+
101
+ def _migrate_history_file(target: Path, rhiza_dir: Path) -> list[str]:
102
+ """Migrate .rhiza.history to .rhiza/history.
102
103
 
103
- # Migrate .rhiza.history to .rhiza/history if it exists
104
+ Args:
105
+ target: Target repository path.
106
+ rhiza_dir: Path to .rhiza directory.
107
+
108
+ Returns:
109
+ List of migrations performed.
110
+ """
104
111
  old_history_file = target / ".rhiza.history"
105
112
  new_history_file = rhiza_dir / "history"
113
+ migrations_performed = []
106
114
 
107
115
  if old_history_file.exists():
108
116
  if new_history_file.exists():
@@ -112,8 +120,6 @@ def migrate(target: Path) -> None:
112
120
  else:
113
121
  logger.info("Found existing .rhiza.history file")
114
122
  logger.info(f"Moving to new location: {new_history_file.relative_to(target)}")
115
-
116
- # Move the history file to new location
117
123
  shutil.move(str(old_history_file), str(new_history_file))
118
124
  logger.success("✓ Moved history file to .rhiza/history")
119
125
  migrations_performed.append("Moved history tracking to .rhiza/history")
@@ -123,7 +129,15 @@ def migrate(target: Path) -> None:
123
129
  else:
124
130
  logger.debug("No existing .rhiza.history file to migrate")
125
131
 
126
- # Summary
132
+ return migrations_performed
133
+
134
+
135
+ def _print_migration_summary(migrations_performed: list[str]) -> None:
136
+ """Print migration summary.
137
+
138
+ Args:
139
+ migrations_performed: List of migrations performed.
140
+ """
127
141
  logger.success("✓ Migration completed successfully")
128
142
 
129
143
  if migrations_performed:
@@ -145,3 +159,42 @@ def migrate(target: Path) -> None:
145
159
  " git add .\n"
146
160
  ' git commit -m "chore: migrate to .rhiza folder structure"\n'
147
161
  )
162
+
163
+
164
+ def migrate(target: Path) -> None:
165
+ """Migrate project to use the new .rhiza folder structure.
166
+
167
+ This command performs the following actions:
168
+ 1. Creates the `.rhiza/` directory in the project root
169
+ 2. Moves template.yml from `.github/rhiza/` or `.github/` to `.rhiza/template.yml`
170
+ 3. Moves `.rhiza.history` to `.rhiza/history` if it exists
171
+ 4. Provides instructions for next steps
172
+
173
+ The `.rhiza/` folder will contain:
174
+ - `template.yml` - Template configuration (replaces `.github/rhiza/template.yml`)
175
+ - `history` - List of files managed by Rhiza templates (replaces `.rhiza.history`)
176
+ - Future: Additional state, cache, or metadata files
177
+
178
+ Args:
179
+ target (Path): Path to the target repository.
180
+ """
181
+ target = target.resolve()
182
+ logger.info(f"Migrating Rhiza structure in: {target}")
183
+ logger.info("This will create the .rhiza folder and migrate configuration files")
184
+
185
+ # Create .rhiza directory
186
+ rhiza_dir = _create_rhiza_directory(target)
187
+
188
+ # Migrate template file
189
+ template_exists, template_migrations = _migrate_template_file(target, rhiza_dir)
190
+
191
+ # Ensure .rhiza is in include list
192
+ if template_exists:
193
+ _ensure_rhiza_in_include(rhiza_dir / "template.yml")
194
+
195
+ # Migrate history file
196
+ history_migrations = _migrate_history_file(target, rhiza_dir)
197
+
198
+ # Print summary
199
+ all_migrations = template_migrations + history_migrations
200
+ _print_migration_summary(all_migrations)
@@ -11,70 +11,69 @@ from pathlib import Path
11
11
  from loguru import logger
12
12
 
13
13
 
14
- def uninstall(target: Path, force: bool) -> None:
15
- """Uninstall Rhiza templates from the target repository.
16
-
17
- Reads the `.rhiza/history` file and removes all files listed in it.
18
- This effectively removes all files that were materialized by Rhiza.
14
+ def _read_history_file(history_file: Path, target: Path) -> list[Path]:
15
+ """Read history file and return list of files to remove.
19
16
 
20
17
  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()
26
-
27
- logger.info(f"Target repository: {target}")
28
-
29
- # Check for history file in new location only
30
- history_file = target / ".rhiza" / "history"
18
+ history_file: Path to history file.
19
+ target: Target repository path.
31
20
 
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
37
-
38
- # Read the history file
21
+ Returns:
22
+ List of file paths to remove.
23
+ """
39
24
  logger.debug(f"Reading history file: {history_file.relative_to(target)}")
40
25
  files_to_remove: list[Path] = []
41
26
 
42
27
  with history_file.open("r", encoding="utf-8") as f:
43
28
  for line in f:
44
29
  line = line.strip()
45
- # Skip comments and empty lines
46
30
  if line and not line.startswith("#"):
47
31
  file_path = Path(line)
48
32
  files_to_remove.append(file_path)
49
33
 
50
- if not files_to_remove:
51
- logger.warning("History file is empty (only contains comments)")
52
- logger.info("Nothing to uninstall.")
53
- return
34
+ return files_to_remove
54
35
 
55
- logger.info(f"Found {len(files_to_remove)} file(s) to remove")
56
36
 
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)")
66
-
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
37
+ def _confirm_uninstall(files_to_remove: list[Path], target: Path) -> bool:
38
+ """Show confirmation prompt and get user response.
76
39
 
77
- # Remove files
40
+ Args:
41
+ files_to_remove: List of files to remove.
42
+ target: Target repository path.
43
+
44
+ Returns:
45
+ True if user confirmed, False otherwise.
46
+ """
47
+ logger.warning("This will remove the following files from your repository:")
48
+ for file_path in sorted(files_to_remove):
49
+ full_path = target / file_path
50
+ if full_path.exists():
51
+ logger.warning(f" - {file_path}")
52
+ else:
53
+ logger.debug(f" - {file_path} (already deleted)")
54
+
55
+ try:
56
+ response = input("\nAre you sure you want to proceed? [y/N]: ").strip().lower()
57
+ if response not in ("y", "yes"):
58
+ logger.info("Uninstall cancelled by user")
59
+ return False
60
+ except (KeyboardInterrupt, EOFError):
61
+ logger.info("\nUninstall cancelled by user")
62
+ return False
63
+
64
+ return True
65
+
66
+
67
+ def _remove_files(files_to_remove: list[Path], target: Path) -> tuple[int, int, int]:
68
+ """Remove files from repository.
69
+
70
+ Args:
71
+ files_to_remove: List of files to remove.
72
+ target: Target repository path.
73
+
74
+ Returns:
75
+ Tuple of (removed_count, skipped_count, error_count).
76
+ """
78
77
  logger.info("Removing files...")
79
78
  removed_count = 0
80
79
  skipped_count = 0
@@ -96,18 +95,28 @@ def uninstall(target: Path, force: bool) -> None:
96
95
  logger.error(f"Failed to delete {file_path}: {e}")
97
96
  error_count += 1
98
97
 
99
- # Clean up empty directories
98
+ return removed_count, skipped_count, error_count
99
+
100
+
101
+ def _cleanup_empty_directories(files_to_remove: list[Path], target: Path) -> int:
102
+ """Clean up empty directories after file removal.
103
+
104
+ Args:
105
+ files_to_remove: List of files that were removed.
106
+ target: Target repository path.
107
+
108
+ Returns:
109
+ Number of empty directories removed.
110
+ """
100
111
  logger.debug("Cleaning up empty directories...")
101
112
  empty_dirs_removed = 0
113
+
102
114
  for file_path in sorted(files_to_remove, reverse=True):
103
115
  full_path = target / file_path
104
116
  parent = full_path.parent
105
117
 
106
- # Try to remove parent directories if they're empty
107
- # Walk up the directory tree
108
118
  while parent != target and parent.exists():
109
119
  try:
110
- # Only remove if directory is empty
111
120
  if parent.is_dir() and not any(parent.iterdir()):
112
121
  parent.rmdir()
113
122
  logger.debug(f"[DEL] {parent.relative_to(target)}/ (empty directory)")
@@ -116,19 +125,39 @@ def uninstall(target: Path, force: bool) -> None:
116
125
  else:
117
126
  break
118
127
  except Exception:
119
- # Directory not empty or other error, stop walking up
120
128
  break
121
129
 
122
- # Remove history file itself
130
+ return empty_dirs_removed
131
+
132
+
133
+ def _remove_history_file(history_file: Path, target: Path) -> tuple[int, int]:
134
+ """Remove the history file itself.
135
+
136
+ Args:
137
+ history_file: Path to history file.
138
+ target: Target repository path.
139
+
140
+ Returns:
141
+ Tuple of (removed_count, error_count).
142
+ """
123
143
  try:
124
144
  history_file.unlink()
125
145
  logger.success(f"[DEL] {history_file.relative_to(target)}")
126
- removed_count += 1
146
+ return 1, 0
127
147
  except Exception as e:
128
148
  logger.error(f"Failed to delete {history_file.relative_to(target)}: {e}")
129
- error_count += 1
149
+ return 0, 1
150
+
151
+
152
+ def _print_summary(removed_count: int, skipped_count: int, empty_dirs_removed: int, error_count: int) -> None:
153
+ """Print uninstall summary.
130
154
 
131
- # Summary
155
+ Args:
156
+ removed_count: Number of files removed.
157
+ skipped_count: Number of files skipped.
158
+ empty_dirs_removed: Number of empty directories removed.
159
+ error_count: Number of errors encountered.
160
+ """
132
161
  logger.info("\nUninstall summary:")
133
162
  logger.info(f" Files removed: {removed_count}")
134
163
  if skipped_count > 0:
@@ -139,6 +168,56 @@ def uninstall(target: Path, force: bool) -> None:
139
168
  logger.error(f" Errors encountered: {error_count}")
140
169
  sys.exit(1)
141
170
 
171
+
172
+ def uninstall(target: Path, force: bool) -> None:
173
+ """Uninstall Rhiza templates from the target repository.
174
+
175
+ Reads the `.rhiza/history` file and removes all files listed in it.
176
+ This effectively removes all files that were materialized by Rhiza.
177
+
178
+ Args:
179
+ target (Path): Path to the target repository.
180
+ force (bool): If True, skip confirmation prompt and proceed with deletion.
181
+ """
182
+ target = target.resolve()
183
+ logger.info(f"Target repository: {target}")
184
+
185
+ # Check for history file
186
+ history_file = target / ".rhiza" / "history"
187
+ if not history_file.exists():
188
+ logger.warning(f"No history file found at: {history_file.relative_to(target)}")
189
+ logger.info("Nothing to uninstall. This repository may not have Rhiza templates materialized.")
190
+ logger.info("If you haven't migrated yet, run 'rhiza migrate' first.")
191
+ return
192
+
193
+ # Read history file
194
+ files_to_remove = _read_history_file(history_file, target)
195
+ if not files_to_remove:
196
+ logger.warning("History file is empty (only contains comments)")
197
+ logger.info("Nothing to uninstall.")
198
+ return
199
+
200
+ logger.info(f"Found {len(files_to_remove)} file(s) to remove")
201
+
202
+ # Confirm uninstall unless force is used
203
+ if not force:
204
+ if not _confirm_uninstall(files_to_remove, target):
205
+ return
206
+
207
+ # Remove files
208
+ removed_count, skipped_count, error_count = _remove_files(files_to_remove, target)
209
+
210
+ # Clean up empty directories
211
+ empty_dirs_removed = _cleanup_empty_directories(files_to_remove, target)
212
+
213
+ # Remove history file
214
+ history_removed, history_error = _remove_history_file(history_file, target)
215
+ removed_count += history_removed
216
+ error_count += history_error
217
+
218
+ # Print summary
219
+ _print_summary(removed_count, skipped_count, empty_dirs_removed, error_count)
220
+
142
221
  logger.success("Rhiza templates uninstalled successfully")
143
222
  logger.info(
144
223
  "Next steps:\n"