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:
@@ -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")
|
||||||
|
Reference in New Issue
Block a user