diff --git a/src/repocat/cli/clean.py b/src/repocat/cli/clean.py index 084e97e..2bca133 100644 --- a/src/repocat/cli/clean.py +++ b/src/repocat/cli/clean.py @@ -1,65 +1,29 @@ -import subprocess -from typer import Typer, Option, Argument -from pathlib import Path -import shutil -from datetime import datetime, timedelta +from typer import Typer, Option from rich.console import Console from rich.table import Table -import os +import shutil from repocat.config import load_config +from repocat.models.catalog import RepoCatalogState app = Typer() console = Console() -def is_git_dirty(path: Path) -> bool: - try: - result = subprocess.run( - ["git", "status", "--porcelain"], - cwd=path, - stdout=subprocess.PIPE, - stderr=subprocess.DEVNULL, - text=True, - ) - return bool(result.stdout.strip()) - except Exception: - return False - -def is_git_unpushed(path: Path) -> bool: - try: - # Check if upstream exists - subprocess.run(["git", "rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"], - cwd=path, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True) - - result = subprocess.run( - ["git", "log", "@{u}..HEAD", "--oneline"], - cwd=path, - stdout=subprocess.PIPE, - stderr=subprocess.DEVNULL, - text=True, - ) - return bool(result.stdout.strip()) - except Exception: - return False - -def get_dir_size(path: Path) -> int: - """Returns size in bytes.""" - return sum(f.stat().st_size for f in path.rglob("*") if f.is_file()) def complete_categories(incomplete: str): config = load_config() return [c.name for c in config.categories if c.name.startswith(incomplete)] + @app.command("volatile") def clean_volatile( dry_run: bool = Option(False, "--dry-run", help="Nur anzeigen, was gelöscht würde"), category: list[str] = Option(None, "--category", "-c", help="Nur bestimmte Kategorien reinigen", autocompletion=complete_categories), ): """ - Löscht **bedingungslos** alle Repositories aus `volatile`-Kategorien (z. B. `scratches`). + Löscht **bedingungslos** alle Repositories aus `volatile`-Kategorien (z.\u200bB. `scratches`). """ - config = load_config() - base = config.base_dir + catalog = RepoCatalogState.from_config(load_config()) table = Table(title="Clean – Volatile (Zerstörung)") table.add_column("Kategorie") @@ -70,37 +34,31 @@ def clean_volatile( deleted = 0 skipped = 0 - for cat in config.categories: - if not cat.volatile: + for cat in catalog.categories: + if not cat.config.volatile: continue - if category and cat.name not in category: + if category and cat.config.name not in category: continue - path = base / cat.subdir - if not path.exists(): - continue - - for repo in sorted(path.iterdir()): - if not repo.is_dir(): - continue - - size_mb = get_dir_size(repo) / 1024 / 1024 + for repo in cat.repos: + size_mb = repo.size_mb if dry_run: action = "[yellow]Würde löschen[/]" else: try: - shutil.rmtree(repo) + shutil.rmtree(repo.path) action = "[red]Gelöscht[/]" deleted += 1 except Exception as e: action = f"[red]Fehler: {e}[/]" - table.add_row(cat.name, repo.name, f"{size_mb:.1f}", action) + 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") + @app.command() def run( dry_run: bool = Option(False, "--dry-run", help="Nur anzeigen, was gelöscht würde"), @@ -109,11 +67,7 @@ def run( """ Löscht alte oder volatile Repositories aus den konfigurierten Kategorien. """ - config = load_config() - now = datetime.now() - base = config.base_dir - - console.print(f"[bold yellow]Base-Verzeichnis:[/] {base}") + catalog = RepoCatalogState.from_config(load_config()) table = Table(title="Clean-Ergebnis") table.add_column("Kategorie") @@ -125,43 +79,27 @@ def run( deleted = 0 skipped = 0 - for cat in config.categories: - if category and cat.name not in category: + for cat in catalog.categories: + if category and cat.config.name not in category: + continue + if cat.config.is_archive: continue - if cat.is_archive: continue - - path = base / cat.subdir - if not path.exists(): - continue - - cutoff = now - timedelta(days=cat.days or 0) - - for repo in sorted(path.iterdir()): - if not repo.is_dir(): - continue - - mtime = datetime.fromtimestamp(repo.stat().st_mtime) - age_days = (now - mtime).days - size_mb = get_dir_size(repo) / 1024 / 1024 + limit_days = cat.config.days or 0 + for repo in cat.repos: should_delete = ( - (cat.volatile) - or (cat.days is not None and mtime < cutoff) + cat.config.volatile + or (cat.config.days is not None and repo.is_expired(limit_days)) ) - # Git-Schutz prüfen - is_git = (repo / ".git").exists() - dirty = is_git and is_git_dirty(repo) - unpushed = is_git and is_git_unpushed(repo) - - if is_git and (dirty or unpushed): - reason = [] - if dirty: - reason.append("uncommitted") - if unpushed: - reason.append("unpushed") - status = "/".join(reason) + if repo.git and repo.git.is_protected: + reasons = [] + if repo.git.dirty: + reasons.append("uncommitted") + if repo.git.unpushed: + reasons.append("unpushed") + status = "/".join(reasons) action = f"[blue]Geschützt ({status})[/]" skipped += 1 else: @@ -170,7 +108,7 @@ def run( action = "[yellow]Würde löschen[/]" else: try: - shutil.rmtree(repo) + shutil.rmtree(repo.path) action = "[red]Gelöscht[/]" deleted += 1 except Exception as e: @@ -179,7 +117,7 @@ def run( action = "[cyan]Keine Aktion[/]" skipped += 1 - table.add_row(cat.name, repo.name, str(age_days), f"{size_mb:.1f}", action) + table.add_row(cat.config.name, repo.name, str(repo.age_days), f"{repo.size_mb:.1f}", action) console.print(table) console.print(f"\n[green]✓ Clean abgeschlossen[/] – {deleted} gelöscht, {skipped} übersprungen")