# scripts/lib/git_ops.py """Narrow git wrapper: subprocess calls, no third-party library.""" from __future__ import annotations import subprocess from pathlib import Path from typing import Iterable def _run(args: list[str], *, cwd: Path | None = None, check: bool = True) -> subprocess.CompletedProcess: return subprocess.run( ["git", *args], cwd=cwd, check=check, capture_output=True, text=True ) def current_commit() -> str: return _run(["rev-parse", "HEAD"]).stdout.strip() def worktree_clean(*, allowlist: Iterable[str] | None = None) -> bool: """True if `git status --porcelain=v1` is empty after filtering allowlisted paths. Allowlist entries match either an exact path or a directory prefix (`/...`). Limitations: - Renames (`R old -> new` lines) are not parsed; a rename of an allowlisted path will be reported as unclean. Callers in this project don't rename tracked paths during /refresh, so this is acceptable. - Paths containing spaces or non-ASCII characters may be quoted by git (`core.quotepath` default), breaking exact-match comparison. Project-controlled paths (index/, derived/, surfaces/, STATE.json) are ASCII without spaces. """ out = _run(["status", "--porcelain=v1"]).stdout if not out.strip(): return True if allowlist is None: return False allowed = set(allowlist) for line in out.splitlines(): path = line[3:].strip() if path not in allowed and not any(path.startswith(p + "/") for p in allowed): return False return True def pull() -> None: _run(["pull", "--ff-only"]) def push() -> None: _run(["push"]) def add_and_commit(paths: list[str], message: str) -> None: _run(["add", "--", *paths]) _run(["commit", "-m", message]) def add_and_commit_if_changes(paths: list[str], message: str) -> bool: """Stage paths and commit only if there are staged changes. Returns True if a commit was made.""" _run(["add", "--", *paths]) # `git diff --cached --quiet` exits 0 if no staged changes, 1 if there are staged changes. result = _run(["diff", "--cached", "--quiet", "--", *paths], check=False) if result.returncode == 0: return False _run(["commit", "-m", message]) return True