|
Option C Tools
|
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 |
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)
|
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.
|
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.
|
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.
|
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.
| tuple[int, int] git.ahead_behind | ( | Path | repo_root | ) |
| bool git.branch_exists | ( | Path | repo_root, |
| str | name ) |
| 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.
| 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.
| 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.
| 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.
| Path git.get_repo_root | ( | Path | path | ) |
| 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.
| 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.
| 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.
| list[Path] git.git_staged_files | ( | Path | repo_root, |
| Optional[set[str]] | extensions = None ) |
| bool git.has_uncommitted_changes | ( | Path | repo_root | ) |
| bool git.has_untracked_files | ( | Path | repo_root | ) |
| bool git.is_detached_head | ( | Path | repo_root | ) |
| 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.
| str git.last_commit_message | ( | Path | repo_root | ) |
| str git.last_commit_sha | ( | Path | repo_root, |
| bool | short = False ) |
| list[str] git.list_branches | ( | Path | repo_root, |
| * | , | ||
| bool | remote = False ) |
| list[str] git.list_tags | ( | Path | repo_root | ) |
| 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.
| 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.
| 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.
| 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.
| 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.
| 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.
|
protected |