Coverage for src/keel/git.py: 100%
41 statements
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-16 18:07 +0000
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-16 18:07 +0000
1"""Thin, fail-soft ``git`` wrappers (argv, no shell).
3These build the exact git command for each backbone operation and run it via the
4injectable ``_run`` seam, so the command construction is unit-tested offline; live
5behaviour is exercised opt-in against a real repo. Each returns a
6:class:`keel.runner.CommandResult` (or a parsed value), never raising.
7"""
9from __future__ import annotations
11from .runner import CommandResult, run_argv
14def fetch(remote: str, ref: str, *, cwd: str | None = None, _run=None) -> CommandResult:
15 return run_argv(["git", "fetch", remote, ref, "--quiet"], cwd=cwd, **_kw(_run))
18def worktree_add(
19 base: str, branch: str, path: str, *, cwd: str | None = None, _run=None
20) -> CommandResult:
21 return run_argv(["git", "worktree", "add", "-b", branch, path, base], cwd=cwd, **_kw(_run))
24def worktree_remove(path: str, *, cwd: str | None = None, _run=None) -> CommandResult:
25 return run_argv(["git", "worktree", "remove", path, "--force"], cwd=cwd, **_kw(_run))
28def worktree_list(*, cwd: str | None = None, _run=None) -> CommandResult:
29 return run_argv(["git", "worktree", "list", "--porcelain"], cwd=cwd, **_kw(_run))
32def current_branch(*, cwd: str | None = None, _run=None) -> str | None:
33 result = run_argv(["git", "rev-parse", "--abbrev-ref", "HEAD"], cwd=cwd, **_kw(_run))
34 return result.output.strip() if result.ok else None
37def list_branches(*, cwd: str | None = None, _run=None) -> CommandResult:
38 """List local + remote branch short names (one per line) as a ``CommandResult``.
40 Returns the raw result (like :func:`worktree_list`) rather than a parsed
41 fail-soft list, so a caller that needs to *distinguish a git error from an
42 empty repo* — e.g. dry-run integrity verification, which must fail closed
43 when it cannot observe — can inspect ``result.ok``. Parsing is the caller's.
44 """
45 return run_argv(
46 ["git", "for-each-ref", "--format=%(refname:short)",
47 "refs/heads", "refs/remotes"],
48 cwd=cwd, **_kw(_run),
49 )
52def rev_parse(ref: str, *, cwd: str | None = None, _run=None) -> str | None:
53 """Resolve ``ref`` to a full commit SHA; ``None`` when it cannot be resolved."""
54 result = run_argv(["git", "rev-parse", "--verify", "--quiet", ref], cwd=cwd, **_kw(_run))
55 output = result.output.strip()
56 return output if result.ok and output else None
59def merge_base(a: str, b: str, *, cwd: str | None = None, _run=None) -> str | None:
60 """Best common ancestor of ``a`` and ``b``; ``None`` when there is none/on error."""
61 result = run_argv(["git", "merge-base", a, b], cwd=cwd, **_kw(_run))
62 output = result.output.strip()
63 return output if result.ok and output else None
66def rev_count(base: str, head: str, *, cwd: str | None = None, _run=None) -> int | None:
67 """Commits in ``base..head`` (how far ``head`` is ahead of ``base``); ``None`` on error."""
68 result = run_argv(
69 ["git", "rev-list", "--count", f"{base}..{head}"], cwd=cwd, **_kw(_run)
70 )
71 if not result.ok:
72 return None
73 output = result.output.strip()
74 if not output.isdigit():
75 return None
76 return int(output)
79def changed_files(base: str, head: str, *, cwd: str | None = None, _run=None) -> list[str]:
80 """Files changed between ``base`` and ``head`` (``base...head``); ``[]`` on error."""
81 result = run_argv(["git", "diff", "--name-only", f"{base}...{head}"], cwd=cwd, **_kw(_run))
82 if not result.ok:
83 return []
84 return [line for line in result.output.splitlines() if line.strip()]
87def diff(base: str, head: str, *, cwd: str | None = None, _run=None) -> str:
88 """The unified diff between ``base`` and ``head`` (``base...head``); ``""`` on error."""
89 result = run_argv(["git", "diff", f"{base}...{head}"], cwd=cwd, **_kw(_run))
90 return result.output if result.ok else ""
93def _kw(_run):
94 """Pass ``_run`` through only when provided (so the default subprocess is used otherwise)."""
95 return {"_run": _run} if _run is not None else {}