feat(cli): add initial repository management commands

- Introduce CLI using Typer for repository management
- Add commands for archiving, cleaning, moving, reviewing, and status
- Implement configuration loading and default structure setup
- Include utilities for Git status checks and directory size calculation
- Lay groundwork for extensible repository operations

Signed-off-by: Max P. <Mail@MPassarello.de>
This commit is contained in:
2025-04-30 12:01:34 +02:00
parent d1e246c868
commit ef2bac4e88
12 changed files with 702 additions and 0 deletions

102
src/repocat/cli/review.py Normal file
View File

@@ -0,0 +1,102 @@
from typer import Typer, confirm, prompt
from rich.console import Console
from rich.table import Table
from pathlib import Path
import shutil
from datetime import datetime
import typer
from rich.prompt import Prompt, Confirm
from repocat.cli.move import move_command
from repocat.config import load_config
from repocat.models.config import RepoCategory
from repocat.cli.archive import archive_repo
from repocat.cli.clean import get_dir_size, is_git_dirty, is_git_unpushed
app = Typer()
console = Console()
@app.command("run")
def review():
"""
Führt interaktiv durch alle Repositories im Review-Ordner.
"""
config = load_config()
base = config.base_dir
archive_cat = next((c for c in config.categories if c.is_archive), None)
if not archive_cat:
console.print("[red]Keine Archiv-Kategorie in der Konfiguration gefunden.[/]")
raise typer.Exit(1)
archive_path = base / archive_cat.subdir
archive_path.mkdir(parents=True, exist_ok=True)
review_cat = next((c for c in config.categories if c.name == "review"), None)
if not review_cat:
console.print("[red]Kein 'review'-Eintrag in der Konfiguration gefunden.[/]")
raise typer.Exit(1)
review_path = base / review_cat.subdir
if not review_path.exists():
console.print(f"[yellow]Hinweis:[/] Kein review-Ordner vorhanden unter {review_path}")
return
repos = [p for p in review_path.iterdir() if p.is_dir()]
if not repos:
console.print("[green]✓ Keine Repositories im Review-Ordner.[/]")
return
for repo in sorted(repos):
console.rule(f"[bold cyan]{repo.name}")
age_days = (datetime.now() - datetime.fromtimestamp(repo.stat().st_mtime)).days
size_mb = get_dir_size(repo) / 1024 / 1024
status = []
if is_git_dirty(repo):
status.append("uncommitted")
if is_git_unpushed(repo):
status.append("unpushed")
if not status:
status.append("clean")
console.print(f"[bold]Alter:[/] {age_days} Tage")
console.print(f"[bold]Größe:[/] {size_mb:.1f} MB")
console.print(f"[bold]Git:[/] {', '.join(status)}")
while True:
action = Prompt.ask(
"Aktion \\[m]ove / \\[a]rchive / \\[d]elete / \\[s]kip / \\[q]uit",
choices=["m", "a", "d", "s", "q"],
show_choices=False
)
if action in ("s", "skip", ""):
break
elif action in ("q", "quit"):
console.print("[bold yellow]Abgebrochen.[/]")
raise typer.Exit()
elif action in ("d", "delete"):
if Confirm.ask(f"Bist du sicher, dass du [red]{repo.name}[/] löschen willst?"):
shutil.rmtree(repo)
console.print(f"[red]✓ Gelöscht:[/] {repo.name}")
break
elif action in ("m", "move"):
target_categories = [c.name for c in config.categories if c.name != "review" and not c.is_archive]
target = Prompt.ask("Zielkategorie", choices=target_categories)
move_command(source=f"review/{repo.name}", target=target)
break
elif action in ("a", "archive"):
try:
archive_file = archive_repo(repo, archive_path, dry_run=False)
console.print(f"[green]✓ Archiviert:[/] {archive_file.name}")
break
except Exception as e:
console.print(f"[red]Fehler beim Archivieren:[/] {e}")
else:
console.print("[yellow]Ungültige Eingabe. Gültig sind: m, d, s, q[/]")
console.print("[green]✓ Review abgeschlossen.[/]")