From 7f629fd752d7301c6c84d8227918853dd0538ff5 Mon Sep 17 00:00:00 2001 From: "Max P." Date: Thu, 27 Nov 2025 18:21:20 +0100 Subject: [PATCH] feat(clean): add snapshot support for volatile repo cleanup --- src/repocat/cli/clean.py | 89 +++++++++++++++++++++++++++++++----- src/repocat/models/config.py | 2 + 2 files changed, 80 insertions(+), 11 deletions(-) diff --git a/src/repocat/cli/clean.py b/src/repocat/cli/clean.py index 0150145..5cb4cd0 100644 --- a/src/repocat/cli/clean.py +++ b/src/repocat/cli/clean.py @@ -2,6 +2,8 @@ from typer import Typer, Option from rich.console import Console from rich.table import Table import shutil +import os +from datetime import datetime from repocat.config import load_config from repocat.models.catalog import RepoCatalogState @@ -23,7 +25,16 @@ def clean_volatile( """ Löscht **bedingungslos** alle Repositories aus `volatile`-Kategorien (z.B. `scratches`). """ - catalog = RepoCatalogState.from_config(load_config()) + config = load_config() + catalog = RepoCatalogState.from_config(config) + + # Snapshot configuration + use_snapshots = config.defaults.snapshots if config.defaults else False + snapshot_count = config.defaults.snapshot_count if config.defaults else 5 + + snapshot_root = config.base_dir / ".snapshots" + timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") + current_snapshot_dir = snapshot_root / timestamp table = Table(title="Clean – Volatile (Zerstörung)") table.add_column("Kategorie") @@ -33,6 +44,7 @@ def clean_volatile( deleted = 0 skipped = 0 + snapshotted_categories = set() for cat in catalog.categories: if not cat.config.volatile: @@ -40,23 +52,78 @@ def clean_volatile( if category and cat.config.name not in category: continue - for repo in cat.repos: - size_mb = repo.size_mb - + # If snapshots are enabled, we handle the whole category at once + if use_snapshots: + if not cat.path.exists(): + continue + if dry_run: - action = "[yellow]Würde löschen[/]" + action = f"[yellow]Würde verschieben nach .snapshots/{timestamp}[/]" else: try: - shutil.rmtree(repo.path) - action = "[red]Gelöscht[/]" - deleted += 1 + # Create snapshot dir only if we actually have something to move + if not current_snapshot_dir.exists(): + current_snapshot_dir.mkdir(parents=True, exist_ok=True) + + target_dir = current_snapshot_dir / cat.config.subdir + # Ensure parent of target exists (if subdir is nested) + target_dir.parent.mkdir(parents=True, exist_ok=True) + + # Move the whole category folder + # Note: shutil.move(src, dst) where dst does not exist moves src to dst. + # If dst exists, it moves src inside dst. + # We want to rename cat.path to target_dir. + shutil.move(str(cat.path), str(target_dir)) + + # Recreate the empty category folder + cat.path.mkdir(parents=True, exist_ok=True) + + action = f"[green]Verschoben nach .snapshots/{timestamp}[/]" + snapshotted_categories.add(cat.config.name) except Exception as e: - action = f"[red]Fehler: {e}[/]" + action = f"[red]Fehler beim Verschieben: {e}[/]" - table.add_row(cat.config.name, repo.name, f"{size_mb:.1f}", action) + # Add rows for all repos in this category to show what happened to them + if cat.repos: + for repo in cat.repos: + table.add_row(cat.config.name, repo.name, f"{repo.size_mb:.1f}", action) + deleted += 1 # Count as "processed/removed from volatile" + else: + # If no repos but folder existed (and maybe had other files), show a generic row + table.add_row(cat.config.name, "(Alle Dateien)", "-", action) + + else: + # Old behavior: Delete individual repos + for repo in cat.repos: + size_mb = repo.size_mb + + if dry_run: + action = "[yellow]Würde löschen[/]" + else: + try: + shutil.rmtree(repo.path) + action = "[red]Gelöscht[/]" + deleted += 1 + except Exception as e: + action = f"[red]Fehler: {e}[/]" + + table.add_row(cat.config.name, repo.name, f"{size_mb:.1f}", action) console.print(table) - console.print(f"\n[green]✓ Volatile-Clean abgeschlossen[/] – {deleted} gelöscht, {skipped} übersprungen") + + # Prune old snapshots + if use_snapshots and not dry_run and snapshot_root.exists(): + try: + snapshots = sorted([p for p in snapshot_root.iterdir() if p.is_dir()], key=lambda p: p.name) + if len(snapshots) > snapshot_count: + to_delete = snapshots[:-snapshot_count] + for snap in to_delete: + shutil.rmtree(snap) + console.print(f"[dim]Alte Snapshots bereinigt: {len(to_delete)} gelöscht[/]") + except Exception as e: + console.print(f"[red]Fehler beim Bereinigen der Snapshots: {e}[/]") + + console.print(f"\n[green]✓ Volatile-Clean abgeschlossen[/] – {deleted} Repositories verarbeitet") @app.command() diff --git a/src/repocat/models/config.py b/src/repocat/models/config.py index 7b9bd19..94e5ea3 100644 --- a/src/repocat/models/config.py +++ b/src/repocat/models/config.py @@ -13,6 +13,8 @@ class RepoCategory(BaseModel): class RepoCatDefaults(BaseModel): dry_run: bool = True auto_create_directories: bool = True + snapshots: bool = False + snapshot_count: int = 5 class RepoCatConfig(BaseModel): base_dir: Path = Field(default=Path("~/Repositories").expanduser())