8Provide the foundation layer for OCT's Git integration — a pure
9subprocess wrapper and a set of read-only state queries. No CLI, no
10writes, no hook installation. This module is the sole place where
11``subprocess.run`` is allowed to invoke the ``git`` binary; every
12higher-level ``oct git`` command (Phase 4B–4E) delegates through
13``git_run`` and the helpers below.
17- Translate raw subprocess errors into a small, typed ``GitError``
18 hierarchy so callers never see ``FileNotFoundError`` or
20- Enforce subprocess discipline: ``shell=False``, argv-only, explicit
21 timeout, ``text=True``, ``encoding="utf-8"``, ``errors="replace"``.
22- Redact ``user:token@`` segments from any git output that crosses a
23 diagnostics or exception boundary.
24- Provide pure read-only query functions for repo state (branch, HEAD,
25 changed files, staged files, remotes, last commit) each accepting an
26 explicit ``repo_root``.
27- Provide branch mutation primitives (create, switch, create-and-switch)
28 used by ``oct git branch`` and ``oct git commit`` auto-branching.
29- Provide staging primitives (stage, unstage, interactive patch staging)
30 used by ``oct git add``.
31- Filter path-escape attempts in ``git_changed_files`` / ``git_staged_files``
32 by dropping entries whose resolved target lies outside ``repo_root``
33 (OI-405 defense-in-depth).
39 L1 — errors: missing git binary, command failure, path escape
40 L2 — lifecycle: git_run entry/exit with redacted argv + returncode
41 L3 — command: each public query function's entry with arg summary
42 L4 — deep trace: parsed output, per-call timings
46- This module must not import ``click``.
47- Every subprocess invocation goes through :func:`git_run`.
48- ``is_git_repo`` is the only function that swallows all exceptions and
49 returns ``False``; every other public function raises from the
50 ``GitError`` hierarchy on failure.
51- Credential redaction is applied inside ``git_run`` — callers that
52 bubble exception state or log output never see raw credentials.
53- Read-only query functions never write to the repo.
54- Branch mutation functions (``create_branch``, ``switch_branch``,
55 ``create_and_switch_branch``) and staging functions (``stage_paths``,
56 ``unstage_paths``, ``stage_paths_interactive``) are the only write
57 operations and are clearly documented as such.
58- ``stage_paths_interactive`` is the only function that bypasses
59 :func:`git_run` because patch-mode staging requires an attached TTY
60 for the user to answer hunk prompts. It still uses ``shell=False``
61 and validates argv as a list.
65- oc_diagnostics (_dbg structured logger; optional at dev time)
70from pathlib
import Path
71from typing
import Iterable, Optional
74 from oc_diagnostics
import _dbg
as _real_dbg
76 def _dbg(*args, **kwargs) -> None:
77 _real_dbg(*args, **kwargs)
79 def _dbg(*args, **kwargs) -> None:
80 """No-op fallback when ``oc_diagnostics`` is not importable.
82 ``oct.core.git`` is used from unit tests and from the linter,
83 both of which may run in environments where the sibling
84 ``oc_diagnostics`` package is not on the import path (e.g. when
85 pytest is invoked from inside ``oct/`` rather than the project
86 root). The module must remain usable in that case — the tests
87 below exercise behaviour, not logging.
91from oct.core.project_root
import is_within_project_root
101DEFAULT_GIT_TIMEOUT: int = 30
108_CRED_URL_RE: re.Pattern[str] = re.compile(
109 r"(?P<scheme>https?://)[^/@\s]+@",
119 """Base class for every error raised by :mod:`oct.core.git`.
121 Callers that want a catch-all should catch ``GitError``; the
122 specific subclasses exist to distinguish "missing binary" from
123 "timeout" from "command failed" so that higher-level CLI commands
124 can render tailored error messages.
128class GitNotFoundError(GitError):
129 """Raised when the ``git`` binary is not found on ``PATH``."""
133 """Raised when a target path is not inside a git working tree."""
137 """Raised when a git subprocess exceeds its configured timeout."""
141 """Raised when ``git`` exits with a non-zero status.
143 Exposes ``returncode``, ``stdout``, and ``stderr`` attributes. Both
144 ``stdout`` and ``stderr`` are already credential-redacted.
161 f
"git command failed (exit {returncode}): "
162 f
"{' '.join(self.argv)!r}\nstderr: {stderr.strip() or '<empty>'}"
173 """Redact ``user:token@`` segments from http(s) URLs in git output.
175 Accepts any string (including multi-line output). Empty strings and
176 ``None`` are returned unchanged. SSH URLs of the form
177 ``git@host:path`` are credential-free (auth is key-based) and are
178 deliberately *not* matched by the regex — they pass through intact.
180 The substitution uses ``\\g<scheme>***@`` so the scheme is preserved
181 and the credential segment is replaced with a stable opaque marker.
183 func =
"_redact_git_url"
186 return _CRED_URL_RE.sub(
r"\g<scheme>***@", text)
190 """Return ``argv`` with credentials redacted in each URL-bearing element.
192 OI-507 — ``git_run`` and ``GitCommandError`` both stringify argv for
193 ``_dbg`` and exception messages. Any ``https://user:token@host/...``
194 argument would otherwise land in logs and raised exceptions verbatim.
195 This helper is the single choke point that keeps the redaction
196 invariant (blueprint §11.2) centralised.
210 timeout: int = DEFAULT_GIT_TIMEOUT,
212) -> subprocess.CompletedProcess[str]:
213 """Run a git command with the project's subprocess discipline.
218 Git sub-arguments excluding the leading ``git``. For example,
219 ``["rev-parse", "--show-toplevel"]``. Must be a list; joined
220 strings are rejected to prevent shell injection.
222 Working directory for the git invocation. Must exist.
224 Maximum seconds to wait before the subprocess is killed and
225 :class:`GitTimeoutError` is raised. Defaults to
226 :data:`DEFAULT_GIT_TIMEOUT`.
228 When ``True`` (default), a non-zero exit code raises
229 :class:`GitCommandError`. When ``False``, the completed process
230 is returned with its ``returncode`` intact so callers can
231 inspect it (used by :func:`is_git_repo` to avoid exception
236 ``subprocess.CompletedProcess[str]``
237 A completed process whose ``stdout`` and ``stderr`` have already
238 been passed through :func:`_redact_git_url`.
243 If the ``git`` binary is not on ``PATH``.
245 If the subprocess exceeds ``timeout`` seconds.
247 If ``check`` is True and the command exits non-zero.
250 if not isinstance(argv, list):
251 raise GitError(f
"argv must be a list, got {type(argv).__name__}")
252 full_argv = [
"git", *argv]
254 _dbg(
"GIT", func, f
"argv={_redact_argv(full_argv)!r} cwd={cwd}", 2)
256 proc = subprocess.run(
266 except FileNotFoundError
as exc:
267 _dbg(
"GIT", func, f
"git binary not found: {exc}", 1, override=
True)
269 "The 'git' executable was not found on PATH. "
270 "Install git or adjust PATH."
272 except subprocess.TimeoutExpired
as exc:
278 f
"git timed out after {timeout}s: {' '.join(redacted_argv)!r}",
282 f
"git command timed out after {timeout}s: {' '.join(redacted_argv)!r}"
289 proc.stdout = redacted_out
290 proc.stderr = redacted_err
294 f
"returncode={proc.returncode} stdout_len={len(redacted_out)} "
295 f
"stderr_len={len(redacted_err)}",
299 if check
and proc.returncode != 0:
302 f
"non-zero exit: {proc.returncode} stderr={redacted_err.strip()!r}",
307 returncode=proc.returncode,
320 """Return True if ``path`` is inside a git working tree.
322 This is the only public function in the module that swallows every
323 exception — ``GitNotFoundError``, ``GitCommandError``, ``OSError``,
324 and anything else all collapse to ``False``. Use it as a cheap
325 predicate before calling any other query function.
328 _dbg(
"GIT", func, f
"path={path}", 3)
331 [
"rev-parse",
"--is-inside-work-tree"],
339 result = proc.returncode == 0
and proc.stdout.strip() ==
"true"
340 _dbg(
"GIT", func, f
"result={result}", 4)
345 """Return the absolute path of the top-level git repo containing ``path``.
347 Raises :class:`GitNotARepoError` if ``path`` is not inside a repo.
349 func =
"get_repo_root"
350 _dbg(
"GIT", func, f
"path={path}", 3)
352 [
"rev-parse",
"--show-toplevel"],
356 if proc.returncode != 0:
357 _dbg(
"GIT", func, f
"not a repo: {path}", 1, override=
True)
359 root = Path(proc.stdout.strip()).resolve()
360 _dbg(
"GIT", func, f
"root={root}", 4)
365 """Return the current branch name, or ``"HEAD"`` if detached.
367 Callers that need to distinguish "on a branch" from "detached"
368 should use :func:`is_detached_head` — this function returns the
369 literal string ``"HEAD"`` in the detached case, mirroring git's own
370 ``--abbrev-ref`` output.
372 func =
"current_branch"
373 _dbg(
"GIT", func, f
"repo_root={repo_root}", 3)
375 [
"rev-parse",
"--abbrev-ref",
"HEAD"],
378 branch = proc.stdout.strip()
379 _dbg(
"GIT", func, f
"branch={branch!r}", 4)
384 """Return True if HEAD is detached (not pointing at any branch)."""
385 func =
"is_detached_head"
386 _dbg(
"GIT", func, f
"repo_root={repo_root}", 3)
391 """Return True if the working tree has staged or unstaged changes.
393 Untracked files are ignored — use :func:`has_untracked_files` for
394 that. This function checks only tracked files via
395 ``git status --porcelain -- uno``.
397 func =
"has_uncommitted_changes"
398 _dbg(
"GIT", func, f
"repo_root={repo_root}", 3)
400 [
"status",
"--porcelain",
"--untracked-files=no"],
403 result = bool(proc.stdout.strip())
404 _dbg(
"GIT", func, f
"result={result}", 4)
409 """Return True if there are any untracked files not ignored by .gitignore."""
410 func =
"has_untracked_files"
411 _dbg(
"GIT", func, f
"repo_root={repo_root}", 3)
413 [
"ls-files",
"--others",
"--exclude-standard"],
416 result = bool(proc.stdout.strip())
417 _dbg(
"GIT", func, f
"result={result}", 4)
422 entries: Iterable[str],
424 extensions: Optional[set[str]],
426 """Resolve each entry against ``repo_root`` and drop path escapes.
428 Internal helper used by :func:`git_changed_files` and
429 :func:`git_staged_files`. Every entry is:
431 1. Joined to ``repo_root`` and ``.resolve()``-ed.
432 2. Checked via :func:`is_within_project_root` — any path that
433 escapes the repo (e.g. via a symlink target) is silently dropped
434 with an L1 ``_dbg`` log.
435 3. If ``extensions`` is given, filtered to those suffixes.
437 Deduplicates while preserving first-seen order.
439 func =
"_resolve_and_filter"
440 repo_root_resolved = Path(repo_root).resolve()
441 result: list[Path] = []
442 seen: set[Path] = set()
447 candidate = (repo_root_resolved / rel).resolve()
448 if not is_within_project_root(candidate, repo_root_resolved):
451 f
"dropping path-escape candidate: {rel!r} -> {candidate}",
455 if extensions
is not None and candidate.suffix
not in extensions:
457 if candidate
in seen:
460 result.append(candidate)
466 extensions: Optional[set[str]] =
None,
468 """Return absolute paths of files changed since the last commit.
470 The result is the union of:
472 - Tracked files with staged changes (``git diff --cached --name-only``).
473 - Tracked files with unstaged changes (``git diff --name-only``).
474 - Untracked files honouring .gitignore (``git ls-files --others --exclude-standard``).
476 If ``extensions`` is supplied (e.g. ``{".py"}``), only files with
477 those suffixes are returned. Paths whose resolved target lies
478 outside ``repo_root`` are silently dropped (defense-in-depth against
479 symlink-escape attacks; each drop is logged at L1).
481 func =
"git_changed_files"
482 _dbg(
"GIT", func, f
"repo_root={repo_root} extensions={extensions}", 3)
483 entries: list[str] = []
485 [
"diff",
"--cached",
"--name-only",
"--diff-filter=ACMR"],
486 [
"diff",
"--name-only",
"--diff-filter=ACMR"],
487 [
"ls-files",
"--others",
"--exclude-standard"],
489 proc =
git_run(argv, cwd=Path(repo_root))
490 entries.extend(proc.stdout.splitlines())
492 _dbg(
"GIT", func, f
"returning {len(result)} path(s)", 4)
498 extensions: Optional[set[str]] =
None,
500 """Return absolute paths of files staged for the next commit.
502 Same contract as :func:`git_changed_files` but scoped to the index
503 only — untracked and unstaged-but-modified files are excluded.
505 func =
"git_staged_files"
506 _dbg(
"GIT", func, f
"repo_root={repo_root} extensions={extensions}", 3)
508 [
"diff",
"--cached",
"--name-only",
"--diff-filter=ACMR"],
511 entries = proc.stdout.splitlines()
513 _dbg(
"GIT", func, f
"returning {len(result)} path(s)", 4)
518 """Return the SHA of HEAD, or the empty string on a fresh repo.
520 A fresh repo is one where ``git rev-parse HEAD`` fails because no
521 commits exist yet. Any other failure still propagates as
522 :class:`GitCommandError`.
524 func =
"last_commit_sha"
525 _dbg(
"GIT", func, f
"repo_root={repo_root} short={short}", 3)
528 argv.append(
"--short")
530 proc =
git_run(argv, cwd=Path(repo_root), check=
False)
531 if proc.returncode != 0:
533 if "unknown revision" in proc.stderr
or "bad revision" in proc.stderr:
534 _dbg(
"GIT", func,
"no commits in repo", 4)
538 returncode=proc.returncode,
542 sha = proc.stdout.strip()
543 _dbg(
"GIT", func, f
"sha={sha}", 4)
548 """Return the subject + body of HEAD, or the empty string on fresh repos.
550 Uses ``git log -1 --pretty=%B`` so both the subject and the body are
551 returned together (trailing newline stripped).
553 func =
"last_commit_message"
554 _dbg(
"GIT", func, f
"repo_root={repo_root}", 3)
556 [
"log",
"-1",
"--pretty=%B"],
560 if proc.returncode != 0:
561 if "does not have any commits" in proc.stderr
or \
562 "bad default revision" in proc.stderr:
563 _dbg(
"GIT", func,
"no commits in repo", 4)
566 [
"git",
"log",
"-1",
"--pretty=%B"],
567 returncode=proc.returncode,
571 message = proc.stdout.rstrip(
"\n")
572 _dbg(
"GIT", func, f
"message_len={len(message)}", 4)
577 """Return a ``{remote_name: url}`` map, with credentials redacted.
579 Uses ``git remote -v`` and collapses the fetch + push variants into
580 a single entry per remote (preferring ``fetch`` if both are
581 present). Returns an empty dict if no remotes are configured.
584 _dbg(
"GIT", func, f
"repo_root={repo_root}", 3)
585 proc =
git_run([
"remote",
"-v"], cwd=Path(repo_root))
586 remotes: dict[str, str] = {}
587 for line
in proc.stdout.splitlines():
595 name, url = parts[0], parts[1]
597 if name
not in remotes:
599 _dbg(
"GIT", func, f
"remotes={list(remotes.keys())}", 4)
604 """Return tag names sorted by commit date (newest first).
606 Uses ``git tag --sort=-creatordate``. Returns an empty list if no
607 tags exist or if the repo has no commits yet.
610 _dbg(
"GIT", func, f
"repo_root={repo_root}", 3)
612 [
"tag",
"--sort=-creatordate"],
616 if proc.returncode != 0:
617 _dbg(
"GIT", func,
"no tags or git error", 4)
619 tags = [t.strip()
for t
in proc.stdout.splitlines()
if t.strip()]
620 _dbg(
"GIT", func, f
"returning {len(tags)} tag(s)", 4)
626 since_ref: str |
None =
None,
627 format_str: str =
"%H %s",
629 """Return git log lines from HEAD back to *since_ref* (exclusive).
631 Each line is formatted according to *format_str* (passed to
632 ``git log --pretty=<format_str>``). If *since_ref* is ``None``,
633 the entire history is returned. Returns an empty list on fresh
634 repos with no commits.
636 func =
"get_commit_log"
637 _dbg(
"GIT", func, f
"repo_root={repo_root} since_ref={since_ref!r}", 3)
638 argv = [
"log", f
"--pretty={format_str}"]
639 if since_ref
is not None:
640 argv.append(f
"{since_ref}..HEAD")
641 proc =
git_run(argv, cwd=Path(repo_root), check=
False)
642 if proc.returncode != 0:
644 _dbg(
"GIT", func,
"no log output", 4)
646 lines = [ln
for ln
in proc.stdout.splitlines()
if ln.strip()]
647 _dbg(
"GIT", func, f
"returning {len(lines)} line(s)", 4)
654 extensions: Optional[set[str]] =
None,
656 """Return absolute paths of files changed between *ref* and HEAD.
658 Uses ``git diff --name-only <ref>`` and applies the same
659 resolve-and-filter pipeline as :func:`git_changed_files` to drop
660 path-escape attempts and optionally filter by extension.
662 func =
"git_diff_name_only"
663 _dbg(
"GIT", func, f
"repo_root={repo_root} ref={ref!r}", 3)
665 [
"diff",
"--name-only", ref],
669 if proc.returncode != 0:
670 _dbg(
"GIT", func,
"diff failed — returning empty", 4)
672 entries = proc.stdout.splitlines()
674 _dbg(
"GIT", func, f
"returning {len(result)} path(s)", 4)
679 """Return ``(ahead, behind)`` counts relative to the upstream branch.
681 Uses ``git rev-list --left-right --count HEAD...@{upstream}``.
682 Returns ``(0, 0)`` if no upstream is configured or on a fresh repo.
684 func =
"ahead_behind"
685 _dbg(
"GIT", func, f
"repo_root={repo_root}", 3)
687 [
"rev-list",
"--left-right",
"--count",
"HEAD...@{upstream}"],
691 if proc.returncode != 0:
692 _dbg(
"GIT", func,
"no upstream or error — returning (0, 0)", 4)
694 parts = proc.stdout.strip().split()
698 ahead = int(parts[0])
699 behind = int(parts[1])
702 _dbg(
"GIT", func, f
"ahead={ahead} behind={behind}", 4)
712 """Return True if a local branch *name* exists.
714 Uses ``git rev-parse --verify refs/heads/<name>`` with
715 ``check=False`` so it never raises on a missing branch.
717 func =
"branch_exists"
718 _dbg(
"GIT", func, f
"repo_root={repo_root} name={name!r}", 3)
720 [
"rev-parse",
"--verify", f
"refs/heads/{name}"],
724 result = proc.returncode == 0
725 _dbg(
"GIT", func, f
"result={result}", 4)
732 start_point: str |
None =
None,
734 """Create a new branch *name* pointing at *start_point* (default HEAD).
736 Does **not** switch to the new branch — use :func:`switch_branch` or
737 :func:`create_and_switch_branch` for that.
739 Raises :class:`GitCommandError` if the branch already exists or the
740 start-point is invalid.
742 func =
"create_branch"
743 _dbg(
"GIT", func, f
"name={name!r} start_point={start_point!r}", 3)
744 argv = [
"branch", name]
746 argv.append(start_point)
747 git_run(argv, cwd=Path(repo_root))
748 _dbg(
"GIT", func, f
"created branch {name!r}", 4)
752 """Switch the working tree to an existing branch *name*.
754 Uses ``git checkout`` for maximum git-version compatibility.
755 Raises :class:`GitCommandError` if the branch does not exist or
756 the checkout fails (e.g. uncommitted changes would be overwritten).
758 func =
"switch_branch"
759 _dbg(
"GIT", func, f
"name={name!r}", 3)
760 git_run([
"checkout", name], cwd=Path(repo_root))
761 _dbg(
"GIT", func, f
"switched to {name!r}", 4)
767 start_point: str |
None =
None,
769 """Atomically create and switch to a new branch *name*.
771 Equivalent to ``git checkout -b <name> [start_point]``. The index
772 and working tree are preserved — staged changes carry over to the
775 Raises :class:`GitCommandError` if the branch already exists.
777 func =
"create_and_switch_branch"
778 _dbg(
"GIT", func, f
"name={name!r} start_point={start_point!r}", 3)
779 argv = [
"checkout",
"-b", name]
781 argv.append(start_point)
782 git_run(argv, cwd=Path(repo_root))
783 _dbg(
"GIT", func, f
"created and switched to {name!r}", 4)
787 """Return a list of local (or remote) branch names.
789 Uses ``git branch --format=%(refname:short)`` for clean output
790 without the ``*`` marker or whitespace prefix.
792 func =
"list_branches"
793 _dbg(
"GIT", func, f
"repo_root={repo_root} remote={remote}", 3)
794 argv = [
"branch",
"--format=%(refname:short)"]
797 proc =
git_run(argv, cwd=Path(repo_root))
798 branches = [b.strip()
for b
in proc.stdout.splitlines()
if b.strip()]
799 _dbg(
"GIT", func, f
"returning {len(branches)} branch(es)", 4)
805 paths: list[Path] | list[str],
807 update: bool =
False,
810 """Stage *paths* for the next commit (whole-file staging).
812 Equivalent to ``git add [<paths>...]`` with optional ``-u`` (update
813 tracked only) or ``-A`` (all changes including untracked).
820 Paths to stage. May be empty when ``update`` or ``all_`` is set.
822 Pass ``-u`` so only already-tracked files are staged.
824 Pass ``-A`` so untracked files are also staged.
829 If git rejects the operation (e.g. path outside repo).
834 f
"repo_root={repo_root} paths={len(paths)} update={update} all={all_}",
837 argv: list[str] = [
"add"]
844 argv.extend(str(p)
for p
in paths)
845 elif not (update
or all_):
848 _dbg(
"GIT", func,
"no paths and no -u/-A; nothing to stage", 4)
850 git_run(argv, cwd=Path(repo_root))
851 _dbg(
"GIT", func,
"stage complete", 4)
856 paths: list[Path] | list[str],
858 """Remove *paths* from the index (unstage), leaving working tree intact.
860 Equivalent to ``git reset HEAD -- <paths>``. Provided for symmetry
861 with :func:`stage_paths`; not currently exposed via CLI but used in
862 tests and reserved for a future ``oct git unstage`` command.
867 If git rejects the operation.
869 func =
"unstage_paths"
870 _dbg(
"GIT", func, f
"repo_root={repo_root} paths={len(paths)}", 3)
872 _dbg(
"GIT", func,
"no paths; nothing to unstage", 4)
874 argv: list[str] = [
"reset",
"HEAD",
"--"]
875 argv.extend(str(p)
for p
in paths)
876 git_run(argv, cwd=Path(repo_root))
877 _dbg(
"GIT", func,
"unstage complete", 4)
882 paths: list[Path] | list[str],
884 staged: bool =
False,
886 """Restore *paths* from the index (working tree) or HEAD (staged).
888 L3 / Phase 4G-followup. Equivalent to:
890 - ``staged=False``: ``git restore <paths>`` — drop unstaged
891 working-tree edits, restoring from the index.
892 - ``staged=True``: ``git restore --staged <paths>`` — unstage
893 *paths* without touching the working tree.
898 If git rejects the operation.
900 func =
"restore_paths"
903 f
"repo_root={repo_root} paths={len(paths)} staged={staged}",
907 _dbg(
"GIT", func,
"no paths; nothing to restore", 4)
909 argv: list[str] = [
"restore"]
911 argv.append(
"--staged")
913 argv.extend(str(p)
for p
in paths)
914 git_run(argv, cwd=Path(repo_root))
915 _dbg(
"GIT", func,
"restore complete", 4)
920 paths: list[Path] | list[str],
922 """Run ``git add -p`` so the user can choose hunks interactively.
924 This is the only public function in this module that bypasses
925 :func:`git_run`. Patch-mode staging requires an attached TTY so the
926 user can answer hunk prompts (``y/n/s/...``); ``git_run`` always
927 captures stdout/stderr which would break that interaction. The
928 bypass still uses ``shell=False`` and validates argv.
935 Optional list of paths to scope the patch session to. When
936 empty, ``git`` walks every modified tracked file.
941 Process exit code from ``git add -p``. ``0`` on success.
946 If the ``git`` executable is not on PATH.
948 func =
"stage_paths_interactive"
949 _dbg(
"GIT", func, f
"repo_root={repo_root} paths={len(paths)}", 3)
950 argv: list[str] = [
"git",
"add",
"-p"]
953 argv.extend(str(p)
for p
in paths)
954 if not isinstance(argv, list):
956 f
"argv must be a list, got {type(argv).__name__}",
959 proc = subprocess.run(
965 except FileNotFoundError
as exc:
966 _dbg(
"GIT", func, f
"git binary not found: {exc}", 1, override=
True)
968 "The 'git' executable was not found on PATH. "
969 "Install git or adjust PATH."
971 _dbg(
"GIT", func, f
"interactive stage exit={proc.returncode}", 4)
972 return proc.returncode
None __init__(self, list[str] argv, int returncode, str stdout, str stderr)
tuple[int, int] ahead_behind(Path repo_root)
None stage_paths(Path repo_root, list[Path]|list[str] paths, *, bool update=False, bool all_=False)
bool is_detached_head(Path repo_root)
None restore_paths(Path repo_root, list[Path]|list[str] paths, *, bool staged=False)
bool branch_exists(Path repo_root, str name)
list[str] _redact_argv(list[str] argv)
None unstage_paths(Path repo_root, list[Path]|list[str] paths)
str _redact_git_url(str text)
list[str] list_branches(Path repo_root, *, bool remote=False)
bool is_git_repo(Path path)
list[Path] git_changed_files(Path repo_root, Optional[set[str]] extensions=None)
bool has_uncommitted_changes(Path repo_root)
dict[str, str] remote_urls(Path repo_root)
str current_branch(Path repo_root)
Path get_repo_root(Path path)
None _dbg(*args, **kwargs)
subprocess.CompletedProcess[str] git_run(list[str] argv, *, Path cwd, int timeout=DEFAULT_GIT_TIMEOUT, bool check=True)
list[Path] git_staged_files(Path repo_root, Optional[set[str]] extensions=None)
list[Path] git_diff_name_only(Path repo_root, str ref, Optional[set[str]] extensions=None)
list[str] list_tags(Path repo_root)
None switch_branch(Path repo_root, str name)
int stage_paths_interactive(Path repo_root, list[Path]|list[str] paths)
None create_branch(Path repo_root, str name, str|None start_point=None)
str last_commit_sha(Path repo_root, bool short=False)
list[str] get_commit_log(Path repo_root, str|None since_ref=None, str format_str="%H %s")
str last_commit_message(Path repo_root)
None create_and_switch_branch(Path repo_root, str name, str|None start_point=None)
bool has_untracked_files(Path repo_root)
list[Path] _resolve_and_filter(Iterable[str] entries, Path repo_root, Optional[set[str]] extensions)