refactor(cli): simplify repository cleaning logic

- Replaced direct filesystem and Git operations with catalog-based API
- Streamlined handling of repository properties and deletion logic
- Improved readability and maintainability by reducing redundancy
This commit is contained in:
2025-05-11 14:38:12 +02:00
parent d3ee7ee63d
commit 458d965062

View File

@@ -1,65 +1,29 @@
import subprocess from typer import Typer, Option
from typer import Typer, Option, Argument
from pathlib import Path
import shutil
from datetime import datetime, timedelta
from rich.console import Console from rich.console import Console
from rich.table import Table from rich.table import Table
import os import shutil
from repocat.config import load_config from repocat.config import load_config
from repocat.models.catalog import RepoCatalogState
app = Typer() app = Typer()
console = Console() 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): def complete_categories(incomplete: str):
config = load_config() config = load_config()
return [c.name for c in config.categories if c.name.startswith(incomplete)] return [c.name for c in config.categories if c.name.startswith(incomplete)]
@app.command("volatile") @app.command("volatile")
def clean_volatile( def clean_volatile(
dry_run: bool = Option(False, "--dry-run", help="Nur anzeigen, was gelöscht würde"), 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), 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() catalog = RepoCatalogState.from_config(load_config())
base = config.base_dir
table = Table(title="Clean – Volatile (Zerstörung)") table = Table(title="Clean – Volatile (Zerstörung)")
table.add_column("Kategorie") table.add_column("Kategorie")
@@ -70,37 +34,31 @@ def clean_volatile(
deleted = 0 deleted = 0
skipped = 0 skipped = 0
for cat in config.categories: for cat in catalog.categories:
if not cat.volatile: if not cat.config.volatile:
continue continue
if category and cat.name not in category: if category and cat.config.name not in category:
continue continue
path = base / cat.subdir for repo in cat.repos:
if not path.exists(): size_mb = repo.size_mb
continue
for repo in sorted(path.iterdir()):
if not repo.is_dir():
continue
size_mb = get_dir_size(repo) / 1024 / 1024
if dry_run: if dry_run:
action = "[yellow]Würde löschen[/]" action = "[yellow]Würde löschen[/]"
else: else:
try: try:
shutil.rmtree(repo) shutil.rmtree(repo.path)
action = "[red]Gelöscht[/]" action = "[red]Gelöscht[/]"
deleted += 1 deleted += 1
except Exception as e: except Exception as e:
action = f"[red]Fehler: {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(table)
console.print(f"\n[green]✓ Volatile-Clean abgeschlossen[/] – {deleted} gelöscht, {skipped} übersprungen") console.print(f"\n[green]✓ Volatile-Clean abgeschlossen[/] – {deleted} gelöscht, {skipped} übersprungen")
@app.command() @app.command()
def run( def run(
dry_run: bool = Option(False, "--dry-run", help="Nur anzeigen, was gelöscht würde"), 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. Löscht alte oder volatile Repositories aus den konfigurierten Kategorien.
""" """
config = load_config() catalog = RepoCatalogState.from_config(load_config())
now = datetime.now()
base = config.base_dir
console.print(f"[bold yellow]Base-Verzeichnis:[/] {base}")
table = Table(title="Clean-Ergebnis") table = Table(title="Clean-Ergebnis")
table.add_column("Kategorie") table.add_column("Kategorie")
@@ -125,43 +79,27 @@ def run(
deleted = 0 deleted = 0
skipped = 0 skipped = 0
for cat in config.categories: for cat in catalog.categories:
if category and cat.name not in category: if category and cat.config.name not in category:
continue
if cat.config.is_archive:
continue continue
if cat.is_archive: continue limit_days = cat.config.days or 0
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
for repo in cat.repos:
should_delete = ( should_delete = (
(cat.volatile) cat.config.volatile
or (cat.days is not None and mtime < cutoff) or (cat.config.days is not None and repo.is_expired(limit_days))
) )
# Git-Schutz prüfen if repo.git and repo.git.is_protected:
is_git = (repo / ".git").exists() reasons = []
dirty = is_git and is_git_dirty(repo) if repo.git.dirty:
unpushed = is_git and is_git_unpushed(repo) reasons.append("uncommitted")
if repo.git.unpushed:
if is_git and (dirty or unpushed): reasons.append("unpushed")
reason = [] status = "/".join(reasons)
if dirty:
reason.append("uncommitted")
if unpushed:
reason.append("unpushed")
status = "/".join(reason)
action = f"[blue]Geschützt ({status})[/]" action = f"[blue]Geschützt ({status})[/]"
skipped += 1 skipped += 1
else: else:
@@ -170,7 +108,7 @@ def run(
action = "[yellow]Würde löschen[/]" action = "[yellow]Würde löschen[/]"
else: else:
try: try:
shutil.rmtree(repo) shutil.rmtree(repo.path)
action = "[red]Gelöscht[/]" action = "[red]Gelöscht[/]"
deleted += 1 deleted += 1
except Exception as e: except Exception as e:
@@ -179,7 +117,7 @@ def run(
action = "[cyan]Keine Aktion[/]" action = "[cyan]Keine Aktion[/]"
skipped += 1 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(table)
console.print(f"\n[green]✓ Clean abgeschlossen[/] – {deleted} gelöscht, {skipped} übersprungen") console.print(f"\n[green]✓ Clean abgeschlossen[/] – {deleted} gelöscht, {skipped} übersprungen")