Option C Tools
Loading...
Searching...
No Matches
git Namespace Reference

Classes

class  GitError
class  GitNotFoundError
class  GitNotARepoError
class  GitTimeoutError
class  GitCommandError

Functions

None _dbg (*args, **kwargs)
str _redact_git_url (str text)
list[str] _redact_argv (list[str] argv)
subprocess.CompletedProcess[str] git_run (list[str] argv, *, Path cwd, int timeout=DEFAULT_GIT_TIMEOUT, bool check=True)
bool is_git_repo (Path path)
Path get_repo_root (Path path)
str current_branch (Path repo_root)
bool is_detached_head (Path repo_root)
bool has_uncommitted_changes (Path repo_root)
bool has_untracked_files (Path repo_root)
list[Path] _resolve_and_filter (Iterable[str] entries, Path repo_root, Optional[set[str]] extensions)
list[Path] git_changed_files (Path repo_root, Optional[set[str]] extensions=None)
list[Path] git_staged_files (Path repo_root, Optional[set[str]] extensions=None)
str last_commit_sha (Path repo_root, bool short=False)
str last_commit_message (Path repo_root)
dict[str, str] remote_urls (Path repo_root)
list[str] list_tags (Path repo_root)
list[str] get_commit_log (Path repo_root, str|None since_ref=None, str format_str="%H %s")
list[Path] git_diff_name_only (Path repo_root, str ref, Optional[set[str]] extensions=None)
tuple[int, int] ahead_behind (Path repo_root)
bool branch_exists (Path repo_root, str name)
None create_branch (Path repo_root, str name, str|None start_point=None)
None switch_branch (Path repo_root, str name)
None create_and_switch_branch (Path repo_root, str name, str|None start_point=None)
list[str] list_branches (Path repo_root, *, bool remote=False)
None stage_paths (Path repo_root, list[Path]|list[str] paths, *, bool update=False, bool all_=False)
None unstage_paths (Path repo_root, list[Path]|list[str] paths)
None restore_paths (Path repo_root, list[Path]|list[str] paths, *, bool staged=False)
int stage_paths_interactive (Path repo_root, list[Path]|list[str] paths)

Variables

int DEFAULT_GIT_TIMEOUT = 30
re _CRED_URL_RE

Detailed Description

Purpose
-------
Provide the foundation layer for OCT's Git integration — a pure
subprocess wrapper and a set of read-only state queries. No CLI, no
writes, no hook installation. This module is the sole place where
``subprocess.run`` is allowed to invoke the ``git`` binary; every
higher-level ``oct git`` command (Phase 4B–4E) delegates through
``git_run`` and the helpers below.

Responsibilities
----------------
- Translate raw subprocess errors into a small, typed ``GitError``
  hierarchy so callers never see ``FileNotFoundError`` or
  ``TimeoutExpired``.
- Enforce subprocess discipline: ``shell=False``, argv-only, explicit
  timeout, ``text=True``, ``encoding="utf-8"``, ``errors="replace"``.
- Redact ``user:token@`` segments from any git output that crosses a
  diagnostics or exception boundary.
- Provide pure read-only query functions for repo state (branch, HEAD,
  changed files, staged files, remotes, last commit) each accepting an
  explicit ``repo_root``.
- Provide branch mutation primitives (create, switch, create-and-switch)
  used by ``oct git branch`` and ``oct git commit`` auto-branching.
- Provide staging primitives (stage, unstage, interactive patch staging)
  used by ``oct git add``.
- Filter path-escape attempts in ``git_changed_files`` / ``git_staged_files``
  by dropping entries whose resolved target lies outside ``repo_root``
  (OI-405 defense-in-depth).

Diagnostics
-----------
Domain: GIT
Levels:
    L1 — errors: missing git binary, command failure, path escape
    L2 — lifecycle: git_run entry/exit with redacted argv + returncode
    L3 — command: each public query function's entry with arg summary
    L4 — deep trace: parsed output, per-call timings

Contracts
---------
- This module must not import ``click``.
- Every subprocess invocation goes through :func:`git_run`.
- ``is_git_repo`` is the only function that swallows all exceptions and
  returns ``False``; every other public function raises from the
  ``GitError`` hierarchy on failure.
- Credential redaction is applied inside ``git_run`` — callers that
  bubble exception state or log output never see raw credentials.
- Read-only query functions never write to the repo.
- Branch mutation functions (``create_branch``, ``switch_branch``,
  ``create_and_switch_branch``) and staging functions (``stage_paths``,
  ``unstage_paths``, ``stage_paths_interactive``) are the only write
  operations and are clearly documented as such.
- ``stage_paths_interactive`` is the only function that bypasses
  :func:`git_run` because patch-mode staging requires an attached TTY
  for the user to answer hunk prompts. It still uses ``shell=False``
  and validates argv as a list.

Dependencies
------------
- oc_diagnostics (_dbg structured logger; optional at dev time)

Function Documentation

◆ _dbg()

None git._dbg ( * args,
** kwargs )
protected
No-op fallback when ``oc_diagnostics`` is not importable.

``oct.core.git`` is used from unit tests and from the linter,
both of which may run in environments where the sibling
``oc_diagnostics`` package is not on the import path (e.g. when
pytest is invoked from inside ``oct/`` rather than the project
root). The module must remain usable in that case — the tests
below exercise behaviour, not logging.

Definition at line 76 of file git.py.

Here is the caller graph for this function:

◆ _redact_argv()

list[str] git._redact_argv ( list[str] argv)
protected
Return ``argv`` with credentials redacted in each URL-bearing element.

OI-507 — ``git_run`` and ``GitCommandError`` both stringify argv for
``_dbg`` and exception messages. Any ``https://user:token@host/...``
argument would otherwise land in logs and raised exceptions verbatim.
This helper is the single choke point that keeps the redaction
invariant (blueprint §11.2) centralised.

Definition at line 189 of file git.py.

Here is the call graph for this function:
Here is the caller graph for this function:

◆ _redact_git_url()

str git._redact_git_url ( str text)
protected
Redact ``user:token@`` segments from http(s) URLs in git output.

Accepts any string (including multi-line output). Empty strings and
``None`` are returned unchanged. SSH URLs of the form
``git@host:path`` are credential-free (auth is key-based) and are
deliberately *not* matched by the regex — they pass through intact.

The substitution uses ``\\g<scheme>***@`` so the scheme is preserved
and the credential segment is replaced with a stable opaque marker.

Definition at line 172 of file git.py.

Here is the caller graph for this function:

◆ _resolve_and_filter()

list[Path] git._resolve_and_filter ( Iterable[str] entries,
Path repo_root,
Optional[set[str]] extensions )
protected
Resolve each entry against ``repo_root`` and drop path escapes.

Internal helper used by :func:`git_changed_files` and
:func:`git_staged_files`. Every entry is:

1. Joined to ``repo_root`` and ``.resolve()``-ed.
2. Checked via :func:`is_within_project_root` — any path that
   escapes the repo (e.g. via a symlink target) is silently dropped
   with an L1 ``_dbg`` log.
3. If ``extensions`` is given, filtered to those suffixes.

Deduplicates while preserving first-seen order.

Definition at line 421 of file git.py.

Here is the call graph for this function:
Here is the caller graph for this function:

◆ ahead_behind()

tuple[int, int] git.ahead_behind ( Path repo_root)
Return ``(ahead, behind)`` counts relative to the upstream branch.

Uses ``git rev-list --left-right --count HEAD...@{upstream}``.
Returns ``(0, 0)`` if no upstream is configured or on a fresh repo.

Definition at line 678 of file git.py.

Here is the call graph for this function:

◆ branch_exists()

bool git.branch_exists ( Path repo_root,
str name )
Return True if a local branch *name* exists.

Uses ``git rev-parse --verify refs/heads/<name>`` with
``check=False`` so it never raises on a missing branch.

Definition at line 711 of file git.py.

Here is the call graph for this function:

◆ create_and_switch_branch()

None git.create_and_switch_branch ( Path repo_root,
str name,
str | None start_point = None )
Atomically create and switch to a new branch *name*.

Equivalent to ``git checkout -b <name> [start_point]``. The index
and working tree are preserved — staged changes carry over to the
new branch.

Raises :class:`GitCommandError` if the branch already exists.

Definition at line 764 of file git.py.

Here is the call graph for this function:

◆ create_branch()

None git.create_branch ( Path repo_root,
str name,
str | None start_point = None )
Create a new branch *name* pointing at *start_point* (default HEAD).

Does **not** switch to the new branch — use :func:`switch_branch` or
:func:`create_and_switch_branch` for that.

Raises :class:`GitCommandError` if the branch already exists or the
start-point is invalid.

Definition at line 729 of file git.py.

Here is the call graph for this function:

◆ current_branch()

str git.current_branch ( Path repo_root)
Return the current branch name, or ``"HEAD"`` if detached.

Callers that need to distinguish "on a branch" from "detached"
should use :func:`is_detached_head` — this function returns the
literal string ``"HEAD"`` in the detached case, mirroring git's own
``--abbrev-ref`` output.

Definition at line 364 of file git.py.

Here is the call graph for this function:
Here is the caller graph for this function:

◆ get_commit_log()

list[str] git.get_commit_log ( Path repo_root,
str | None since_ref = None,
str format_str = "%H %s" )
Return git log lines from HEAD back to *since_ref* (exclusive).

Each line is formatted according to *format_str* (passed to
``git log --pretty=<format_str>``).  If *since_ref* is ``None``,
the entire history is returned.  Returns an empty list on fresh
repos with no commits.

Definition at line 624 of file git.py.

Here is the call graph for this function:

◆ get_repo_root()

Path git.get_repo_root ( Path path)
Return the absolute path of the top-level git repo containing ``path``.

Raises :class:`GitNotARepoError` if ``path`` is not inside a repo.

Definition at line 344 of file git.py.

Here is the call graph for this function:

◆ git_changed_files()

list[Path] git.git_changed_files ( Path repo_root,
Optional[set[str]] extensions = None )
Return absolute paths of files changed since the last commit.

The result is the union of:

- Tracked files with staged changes (``git diff --cached --name-only``).
- Tracked files with unstaged changes (``git diff --name-only``).
- Untracked files honouring .gitignore (``git ls-files --others --exclude-standard``).

If ``extensions`` is supplied (e.g. ``{".py"}``), only files with
those suffixes are returned. Paths whose resolved target lies
outside ``repo_root`` are silently dropped (defense-in-depth against
symlink-escape attacks; each drop is logged at L1).

Definition at line 464 of file git.py.

Here is the call graph for this function:

◆ git_diff_name_only()

list[Path] git.git_diff_name_only ( Path repo_root,
str ref,
Optional[set[str]] extensions = None )
Return absolute paths of files changed between *ref* and HEAD.

Uses ``git diff --name-only <ref>`` and applies the same
resolve-and-filter pipeline as :func:`git_changed_files` to drop
path-escape attempts and optionally filter by extension.

Definition at line 651 of file git.py.

Here is the call graph for this function:

◆ git_run()

subprocess.CompletedProcess[str] git.git_run ( list[str] argv,
* ,
Path cwd,
int timeout = DEFAULT_GIT_TIMEOUT,
bool check = True )
Run a git command with the project's subprocess discipline.

Parameters
----------
argv
    Git sub-arguments excluding the leading ``git``. For example,
    ``["rev-parse", "--show-toplevel"]``. Must be a list; joined
    strings are rejected to prevent shell injection.
cwd
    Working directory for the git invocation. Must exist.
timeout
    Maximum seconds to wait before the subprocess is killed and
    :class:`GitTimeoutError` is raised. Defaults to
    :data:`DEFAULT_GIT_TIMEOUT`.
check
    When ``True`` (default), a non-zero exit code raises
    :class:`GitCommandError`. When ``False``, the completed process
    is returned with its ``returncode`` intact so callers can
    inspect it (used by :func:`is_git_repo` to avoid exception
    control flow).

Returns
-------
``subprocess.CompletedProcess[str]``
    A completed process whose ``stdout`` and ``stderr`` have already
    been passed through :func:`_redact_git_url`.

Raises
------
GitNotFoundError
    If the ``git`` binary is not on ``PATH``.
GitTimeoutError
    If the subprocess exceeds ``timeout`` seconds.
GitCommandError
    If ``check`` is True and the command exits non-zero.

Definition at line 206 of file git.py.

Here is the call graph for this function:
Here is the caller graph for this function:

◆ git_staged_files()

list[Path] git.git_staged_files ( Path repo_root,
Optional[set[str]] extensions = None )
Return absolute paths of files staged for the next commit.

Same contract as :func:`git_changed_files` but scoped to the index
only — untracked and unstaged-but-modified files are excluded.

Definition at line 496 of file git.py.

Here is the call graph for this function:

◆ has_uncommitted_changes()

bool git.has_uncommitted_changes ( Path repo_root)
Return True if the working tree has staged or unstaged changes.

Untracked files are ignored — use :func:`has_untracked_files` for
that. This function checks only tracked files via
``git status --porcelain -- uno``.

Definition at line 390 of file git.py.

Here is the call graph for this function:

◆ has_untracked_files()

bool git.has_untracked_files ( Path repo_root)
Return True if there are any untracked files not ignored by .gitignore.

Definition at line 408 of file git.py.

Here is the call graph for this function:

◆ is_detached_head()

bool git.is_detached_head ( Path repo_root)
Return True if HEAD is detached (not pointing at any branch).

Definition at line 383 of file git.py.

Here is the call graph for this function:

◆ is_git_repo()

bool git.is_git_repo ( Path path)
Return True if ``path`` is inside a git working tree.

This is the only public function in the module that swallows every
exception — ``GitNotFoundError``, ``GitCommandError``, ``OSError``,
and anything else all collapse to ``False``. Use it as a cheap
predicate before calling any other query function.

Definition at line 319 of file git.py.

Here is the call graph for this function:

◆ last_commit_message()

str git.last_commit_message ( Path repo_root)
Return the subject + body of HEAD, or the empty string on fresh repos.

Uses ``git log -1 --pretty=%B`` so both the subject and the body are
returned together (trailing newline stripped).

Definition at line 547 of file git.py.

Here is the call graph for this function:

◆ last_commit_sha()

str git.last_commit_sha ( Path repo_root,
bool short = False )
Return the SHA of HEAD, or the empty string on a fresh repo.

A fresh repo is one where ``git rev-parse HEAD`` fails because no
commits exist yet. Any other failure still propagates as
:class:`GitCommandError`.

Definition at line 517 of file git.py.

Here is the call graph for this function:

◆ list_branches()

list[str] git.list_branches ( Path repo_root,
* ,
bool remote = False )
Return a list of local (or remote) branch names.

Uses ``git branch --format=%(refname:short)`` for clean output
without the ``*`` marker or whitespace prefix.

Definition at line 786 of file git.py.

Here is the call graph for this function:

◆ list_tags()

list[str] git.list_tags ( Path repo_root)
Return tag names sorted by commit date (newest first).

Uses ``git tag --sort=-creatordate``. Returns an empty list if no
tags exist or if the repo has no commits yet.

Definition at line 603 of file git.py.

Here is the call graph for this function:

◆ remote_urls()

dict[str, str] git.remote_urls ( Path repo_root)
Return a ``{remote_name: url}`` map, with credentials redacted.

Uses ``git remote -v`` and collapses the fetch + push variants into
a single entry per remote (preferring ``fetch`` if both are
present). Returns an empty dict if no remotes are configured.

Definition at line 576 of file git.py.

Here is the call graph for this function:

◆ restore_paths()

None git.restore_paths ( Path repo_root,
list[Path] | list[str] paths,
* ,
bool staged = False )
Restore *paths* from the index (working tree) or HEAD (staged).

L3 / Phase 4G-followup. Equivalent to:

- ``staged=False``: ``git restore <paths>`` — drop unstaged
  working-tree edits, restoring from the index.
- ``staged=True``: ``git restore --staged <paths>`` — unstage
  *paths* without touching the working tree.

Raises
------
GitCommandError
    If git rejects the operation.

Definition at line 880 of file git.py.

Here is the call graph for this function:

◆ stage_paths()

None git.stage_paths ( Path repo_root,
list[Path] | list[str] paths,
* ,
bool update = False,
bool all_ = False )
Stage *paths* for the next commit (whole-file staging).

Equivalent to ``git add [<paths>...]`` with optional ``-u`` (update
tracked only) or ``-A`` (all changes including untracked).

Parameters
----------
repo_root
    Git repository root.
paths
    Paths to stage. May be empty when ``update`` or ``all_`` is set.
update
    Pass ``-u`` so only already-tracked files are staged.
all_
    Pass ``-A`` so untracked files are also staged.

Raises
------
GitCommandError
    If git rejects the operation (e.g. path outside repo).

Definition at line 803 of file git.py.

Here is the call graph for this function:

◆ stage_paths_interactive()

int git.stage_paths_interactive ( Path repo_root,
list[Path] | list[str] paths )
Run ``git add -p`` so the user can choose hunks interactively.

This is the only public function in this module that bypasses
:func:`git_run`. Patch-mode staging requires an attached TTY so the
user can answer hunk prompts (``y/n/s/...``); ``git_run`` always
captures stdout/stderr which would break that interaction. The
bypass still uses ``shell=False`` and validates argv.

Parameters
----------
repo_root
    Git repository root.
paths
    Optional list of paths to scope the patch session to. When
    empty, ``git`` walks every modified tracked file.

Returns
-------
int
    Process exit code from ``git add -p``. ``0`` on success.

Raises
------
GitNotFoundError
    If the ``git`` executable is not on PATH.

Definition at line 918 of file git.py.

Here is the call graph for this function:

◆ switch_branch()

None git.switch_branch ( Path repo_root,
str name )
Switch the working tree to an existing branch *name*.

Uses ``git checkout`` for maximum git-version compatibility.
Raises :class:`GitCommandError` if the branch does not exist or
the checkout fails (e.g. uncommitted changes would be overwritten).

Definition at line 751 of file git.py.

Here is the call graph for this function:

◆ unstage_paths()

None git.unstage_paths ( Path repo_root,
list[Path] | list[str] paths )
Remove *paths* from the index (unstage), leaving working tree intact.

Equivalent to ``git reset HEAD -- <paths>``. Provided for symmetry
with :func:`stage_paths`; not currently exposed via CLI but used in
tests and reserved for a future ``oct git unstage`` command.

Raises
------
GitCommandError
    If git rejects the operation.

Definition at line 854 of file git.py.

Here is the call graph for this function:

Variable Documentation

◆ _CRED_URL_RE

re git._CRED_URL_RE
protected
Initial value:
1= re.compile(
2 r"(?P<scheme>https?://)[^/@\s]+@",
3)

Definition at line 108 of file git.py.

◆ DEFAULT_GIT_TIMEOUT

int git.DEFAULT_GIT_TIMEOUT = 30

Definition at line 101 of file git.py.