from __future__ import annotations import subprocess from typing import Iterable from . import config, output class GitError(RuntimeError): pass def _run_git(args: Iterable[str], *, capture_output: bool = False, check: bool = True) -> subprocess.CompletedProcess: cmd = ["git", *args] # Dry-run ondersteuning: voer geen echte git-commando's uit, maar log enkel # wat er zou gebeuren en geef een "geslaagde" CompletedProcess terug. if config.CONFIG.dry_run: output.info(f"[DRY-RUN] zou uitvoeren: {' '.join(cmd)}") return subprocess.CompletedProcess( cmd, 0, stdout="" if capture_output else None, stderr="", ) result = subprocess.run( cmd, text=True, capture_output=capture_output, check=False, ) if check and result.returncode != 0: stderr = result.stderr.strip() if result.stderr else "" raise GitError(f"git {' '.join(args)} failed ({result.returncode}): {stderr}") return result def get_current_branch() -> str: result = _run_git(["rev-parse", "--abbrev-ref", "HEAD"], capture_output=True) return (result.stdout or "").strip() def is_clean_working_tree() -> bool: result = _run_git(["status", "--porcelain"], capture_output=True) return (result.stdout or "").strip() == "" def ensure_clean_working_tree() -> None: if not is_clean_working_tree(): raise GitError( "Working tree is niet clean. Commit of stash je wijzigingen voor je deze actie uitvoert." ) def ensure_branch_exists(branch: str) -> None: try: _run_git(["show-ref", "--verify", f"refs/heads/{branch}"], capture_output=True) except GitError as exc: raise GitError(f"Branch '{branch}' bestaat niet lokaal.") from exc def checkout_branch(branch: str) -> None: _run_git(["checkout", branch]) def create_branch(branch: str, base: str) -> None: _run_git(["checkout", base]) _run_git(["checkout", "-b", branch]) def fetch_remote(remote: str) -> None: _run_git(["fetch", remote]) def ensure_not_behind_remote(branch: str, remote: str) -> None: """Controleer of branch niet achterloopt op remote. We gebruiken `git rev-list --left-right --count local...remote` om ahead/behind te bepalen. """ remote_ref = f"{remote}/{branch}" try: result = _run_git( ["rev-list", "--left-right", "--count", f"{branch}...{remote_ref}"], capture_output=True, ) except GitError: # Geen tracking remote; in die gevallen doen we geen check. return output_str = (result.stdout or "").strip() if not output_str: return ahead_str, behind_str = output_str.split() ahead = int(ahead_str) behind = int(behind_str) if behind > 0: raise GitError( f"Branch '{branch}' loopt {behind} commit(s) achter op {remote_ref}. " f"Doe eerst een 'git pull --ff-only' of werk te wijzigingen lokaal bij." ) def list_local_branches_with_prefix(prefix: str) -> list[str]: """Geef een gesorteerde lijst van lokale branches die met het prefix starten.""" result = _run_git( ["for-each-ref", "--format=%(refname:short)", "refs/heads"], capture_output=True, ) lines = (result.stdout or "").splitlines() branches = sorted(b for b in (ln.strip() for ln in lines) if b.startswith(prefix)) return branches