This commit is contained in:
Team3
2026-07-22 16:12:23 +02:00
commit f07ef9653d
851 changed files with 501480 additions and 0 deletions

53
backend/repo.py Normal file
View File

@@ -0,0 +1,53 @@
"""git-Helfer (nackte CLI, keine Bibliothek): Anker-Stände und Diff fürs Re-Scannen."""
import subprocess
from pathlib import Path
def _git(wurzel: Path, *args: str) -> str:
res = subprocess.run(["git", "-C", str(wurzel), *args],
capture_output=True, text=True, timeout=30)
if res.returncode != 0:
raise RuntimeError(f"git {' '.join(args)}: {res.stderr.strip() or res.stdout.strip()}")
return res.stdout
def ist_repo(wurzel: Path) -> bool:
try:
_git(wurzel, "rev-parse", "--git-dir")
return True
except (RuntimeError, FileNotFoundError):
return False
def head_hash(wurzel: Path) -> str:
return _git(wurzel, "rev-parse", "--short=12", "HEAD").strip()
def working_tree_sauber(wurzel: Path) -> bool:
"""False bei uncommitteten Änderungen (inkl. untracked) — Scan/Board arbeiten nur
auf sauberem Tree, sonst vermischen sich Stände. AUSNAHME `.planer/`: die eigenen
Artefakte (z. B. aus einem Teillauf) dürfen den Folgelauf nicht blockieren."""
for zeile in _git(wurzel, "status", "--porcelain").splitlines():
pfad = zeile[3:].split(" -> ")[-1].strip().strip('"')
if not pfad.startswith(".planer/"):
return False
return True
def geaenderte_dateien(wurzel: Path, alt: str, neu: str = "HEAD") -> tuple[set[str], set[str]]:
"""(geändert_oder_neu, gelöscht) zwischen `alt` und `neu`, `.planer/` ausgefiltert.
Renames (R) = alt gelöscht + neu geändert (v1: kein Anker-Matching über Renames)."""
geaendert: set[str] = set()
geloescht: set[str] = set()
for zeile in _git(wurzel, "diff", "--name-status", f"{alt}..{neu}").splitlines():
teile = zeile.split("\t")
status = teile[0]
if status.startswith("R") and len(teile) == 3:
geloescht.add(teile[1])
geaendert.add(teile[2])
elif status == "D":
geloescht.add(teile[1])
elif len(teile) >= 2:
geaendert.add(teile[1])
kein_planer = lambda s: not s.startswith(".planer/") # noqa: E731
return set(filter(kein_planer, geaendert)), set(filter(kein_planer, geloescht))