"""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))