GitCli behaviour (fnord v0.9.39)

View Source

Facade for direct git CLI calls.

Covers repo classification (is_git_repo?/0, worktree_root/0), branch reporting (current_branch/0, default_branch/1), tree enumeration for indexing (ls_tree/2, show_blob/3), gitignore resolution (ignored_files/1), and formatted user-facing messages (git_info/0).

Note: default_branch/1 resolves the project's indexing branch with a strict fallback chain (origin/HEAD → main → master → nil). For the looser worktree-root resolution that falls back to the current branch, see GitCli.Worktree.default_base_branch/1.

This module is the git-subprocess boundary: every public function dispatches through impl/0, resolved via the :git_cli Globals key and defaulting to GitCli.Default (the real System.cmd wrapper). Unlike the lower transport seams, tests do NOT point this key at a mock by default - the real implementation stays in place, and tests that need to script git state opt in per test (see Fnord.TestCase.mock_git_cli/0). GitCli.Default routes its own calls to public siblings back through this facade so a test double intercepts nested calls, not just top-level entry points.

Summary

Types

Parsed metadata for a single commit, as reported by git show. committed_at is the raw unix-epoch string git emits for %at.

Parsed git show --numstat output: the list of changed paths and the per-file addition/deletion counts. Binary files report 0/0.

Functions

Returns the short upstream tracking ref for branch at root (for example origin/feature-parent), or nil when no upstream is configured or git cannot resolve it.

Returns parsed metadata for a single commit. Errors on unparseable output - the most common cause is a literal \x1f byte in a subject or body, which collides with the field separator used in the format string.

Returns the changed file list and per-file diffstat counts for a single commit.

Lists every commit SHA reachable from ref, newest first (rev-list order). Used by the commit indexer to enumerate index candidates.

Returns the current branch name for the effective directory, the short SHA prefixed with @ when HEAD is detached, or nil on failure.

Returns the repository's default branch for indexing purposes

Returns git diff --stat output for range at root.

Fetches ref from remote at root. Network-touching: review uses it to make never-checked-out branches reviewable, and nothing else should reach for it casually.

Returns a formatted, user-facing description of the current git context (branch and root), or a note that the project is not under version control.

Returns a map of absolute paths to true for every gitignored file under root. Returns an empty map if root is nil.

Returns true when the effective directory (project root override or cwd) is inside a git working tree.

Returns true when the given path is inside a git working tree.

Returns true when the effective directory is inside a git working tree. We currently treat any such directory as worktree-aware enough for callers using this predicate, including detached HEAD state.

Returns git log --oneline output for range at root.

Lists every blob in branch's tree as {blob_sha, rel_path} pairs. The blob sha is git's content-addressed hash and is stable across clones and checkouts, so it's usable as a freshness key for the indexer.

Returns the merge base of two refs at root. The review ops below return bare :error on git failure rather than {:error, term}: their callers present target-specific context ("failed to resolve branch X") and have no use for raw plumbing output.

Returns the primary clone's root for the repo containing dir, or nil when dir is not inside a git work tree. For a linked worktree this is the root of the clone the worktree was created from (via git rev-parse --git-common-dir); for a primary checkout it is the repo root itself. Project resolution uses this to map a worktree directory back to the configured project root.

Returns the repository toplevel for the effective directory, or nil when not in a repo (or git is not installed).

Returns the repository toplevel for the given directory, or {:error, :not_a_repo} when the path is not inside a git working tree. Unlike repo_root/0, this ignores the project root override - callers use it to resolve the repo that owns an explicit path (e.g. a stored worktree) regardless of session state.

Returns the content of rel_path as it exists on branch, or an error tuple if git rejects the request (missing file, invalid branch, etc.). Content is returned as a binary; binaries that aren't valid UTF-8 are still returned — callers decide how to handle them.

Returns the raw git status --short --untracked-files=all lines for the repo at root, one entry per changed or untracked file. Used by validation-rule changed-file discovery.

Resolves ref to a commit SHA if it exists locally (rev-parse --verify --quiet ref^{commit}). Never touches the network; pair with fetch_ref/3 to make remote-only refs resolvable via FETCH_HEAD.

Returns the toplevel of the current worktree, or nil when not in one.

Types

commit_meta()

@type commit_meta() :: %{
  sha: String.t(),
  parent_shas: [String.t()],
  author: String.t(),
  committed_at: String.t(),
  subject: String.t(),
  body: String.t()
}

Parsed metadata for a single commit, as reported by git show. committed_at is the raw unix-epoch string git emits for %at.

commit_numstat()

@type commit_numstat() ::
  {[String.t()],
   [
     %{
       file: String.t(),
       additions: non_neg_integer(),
       deletions: non_neg_integer()
     }
   ]}

Parsed git show --numstat output: the list of changed paths and the per-file addition/deletion counts. Binary files report 0/0.

Callbacks

branch_upstream(t, t)

@callback branch_upstream(String.t(), String.t()) :: String.t() | nil

commit_meta(t, t)

@callback commit_meta(String.t(), String.t()) :: {:ok, commit_meta()} | {:error, term()}

commit_numstat(t, t)

@callback commit_numstat(String.t(), String.t()) ::
  {:ok, commit_numstat()} | {:error, term()}

commit_shas(t, t)

@callback commit_shas(String.t(), String.t()) :: {:ok, [String.t()]} | {:error, term()}

current_branch()

@callback current_branch() :: String.t() | nil

default_branch(arg1)

@callback default_branch(String.t() | nil) :: String.t() | nil

diff_stat(t, t)

@callback diff_stat(String.t(), String.t()) :: {:ok, String.t()} | :error

fetch_ref(t, t, t)

@callback fetch_ref(String.t(), String.t(), String.t()) :: {:ok, String.t()} | :error

git_info()

@callback git_info() :: String.t()

ignored_files(arg1)

@callback ignored_files(String.t() | nil) :: map()

is_git_repo?()

@callback is_git_repo?() :: boolean()

is_git_repo_at?(arg1)

@callback is_git_repo_at?(String.t() | nil) :: boolean()

is_worktree?()

@callback is_worktree?() :: boolean()

log_oneline(t, t)

@callback log_oneline(String.t(), String.t()) :: {:ok, String.t()} | :error

ls_tree(t, t)

@callback ls_tree(String.t(), String.t()) ::
  {:ok, [{String.t(), String.t()}]} | {:error, term()}

merge_base(t, t, t)

@callback merge_base(String.t(), String.t(), String.t()) :: {:ok, String.t()} | :error

primary_root_at(t)

@callback primary_root_at(String.t()) :: String.t() | nil

repo_root()

@callback repo_root() :: String.t() | nil

repo_root_at(t)

@callback repo_root_at(String.t()) :: {:ok, String.t()} | {:error, :not_a_repo}

show_blob(t, t, t)

@callback show_blob(String.t(), String.t(), String.t()) ::
  {:ok, binary()} | {:error, term()}

status_short(t)

@callback status_short(String.t()) :: {:ok, [String.t()]} | {:error, term()}

verify_commit(t, t)

@callback verify_commit(String.t(), String.t()) :: {:ok, String.t()} | :error

worktree_root()

@callback worktree_root() :: String.t() | nil

Functions

branch_upstream(root, branch)

@spec branch_upstream(String.t(), String.t()) :: String.t() | nil

Returns the short upstream tracking ref for branch at root (for example origin/feature-parent), or nil when no upstream is configured or git cannot resolve it.

commit_meta(root, sha)

@spec commit_meta(String.t(), String.t()) :: {:ok, commit_meta()} | {:error, term()}

Returns parsed metadata for a single commit. Errors on unparseable output - the most common cause is a literal \x1f byte in a subject or body, which collides with the field separator used in the format string.

commit_numstat(root, sha)

@spec commit_numstat(String.t(), String.t()) ::
  {:ok, commit_numstat()} | {:error, term()}

Returns the changed file list and per-file diffstat counts for a single commit.

commit_shas(root, ref)

@spec commit_shas(String.t(), String.t()) :: {:ok, [String.t()]} | {:error, term()}

Lists every commit SHA reachable from ref, newest first (rev-list order). Used by the commit indexer to enumerate index candidates.

current_branch()

@spec current_branch() :: String.t() | nil

Returns the current branch name for the effective directory, the short SHA prefixed with @ when HEAD is detached, or nil on failure.

default_branch(root)

@spec default_branch(String.t() | nil) :: String.t() | nil

Returns the repository's default branch for indexing purposes:

  1. origin/HEAD - the remote's declared default (usually main).
  2. Local main or master, in that order.
  3. nil - do not silently fall back to the current branch, since that would make fnord index on a feature branch index the feature branch rather than the project's canonical source.

Callers fall back to filesystem-mode indexing when this returns nil, so the user still gets their working tree indexed; they just won't get default-branch semantics.

diff_stat(root, range)

@spec diff_stat(String.t(), String.t()) :: {:ok, String.t()} | :error

Returns git diff --stat output for range at root.

fetch_ref(root, remote, ref)

@spec fetch_ref(String.t(), String.t(), String.t()) :: {:ok, String.t()} | :error

Fetches ref from remote at root. Network-touching: review uses it to make never-checked-out branches reviewable, and nothing else should reach for it casually.

git_info()

@spec git_info() :: String.t()

Returns a formatted, user-facing description of the current git context (branch and root), or a note that the project is not under version control.

ignored_files(root)

@spec ignored_files(String.t() | nil) :: map()

Returns a map of absolute paths to true for every gitignored file under root. Returns an empty map if root is nil.

impl()

@spec impl() :: module()

is_git_repo?()

@spec is_git_repo?() :: boolean()

Returns true when the effective directory (project root override or cwd) is inside a git working tree.

is_git_repo_at?(path)

@spec is_git_repo_at?(String.t() | nil) :: boolean()

Returns true when the given path is inside a git working tree.

is_worktree?()

@spec is_worktree?() :: boolean()

Returns true when the effective directory is inside a git working tree. We currently treat any such directory as worktree-aware enough for callers using this predicate, including detached HEAD state.

log_oneline(root, range)

@spec log_oneline(String.t(), String.t()) :: {:ok, String.t()} | :error

Returns git log --oneline output for range at root.

ls_tree(root, branch)

@spec ls_tree(String.t(), String.t()) ::
  {:ok, [{String.t(), String.t()}]} | {:error, term()}

Lists every blob in branch's tree as {blob_sha, rel_path} pairs. The blob sha is git's content-addressed hash and is stable across clones and checkouts, so it's usable as a freshness key for the indexer.

merge_base(root, ref_a, ref_b)

@spec merge_base(String.t(), String.t(), String.t()) :: {:ok, String.t()} | :error

Returns the merge base of two refs at root. The review ops below return bare :error on git failure rather than {:error, term}: their callers present target-specific context ("failed to resolve branch X") and have no use for raw plumbing output.

primary_root_at(dir)

@spec primary_root_at(String.t()) :: String.t() | nil

Returns the primary clone's root for the repo containing dir, or nil when dir is not inside a git work tree. For a linked worktree this is the root of the clone the worktree was created from (via git rev-parse --git-common-dir); for a primary checkout it is the repo root itself. Project resolution uses this to map a worktree directory back to the configured project root.

repo_root()

@spec repo_root() :: String.t() | nil

Returns the repository toplevel for the effective directory, or nil when not in a repo (or git is not installed).

repo_root_at(path)

@spec repo_root_at(String.t()) :: {:ok, String.t()} | {:error, :not_a_repo}

Returns the repository toplevel for the given directory, or {:error, :not_a_repo} when the path is not inside a git working tree. Unlike repo_root/0, this ignores the project root override - callers use it to resolve the repo that owns an explicit path (e.g. a stored worktree) regardless of session state.

show_blob(root, branch, rel_path)

@spec show_blob(String.t(), String.t(), String.t()) ::
  {:ok, binary()} | {:error, term()}

Returns the content of rel_path as it exists on branch, or an error tuple if git rejects the request (missing file, invalid branch, etc.). Content is returned as a binary; binaries that aren't valid UTF-8 are still returned — callers decide how to handle them.

status_short(root)

@spec status_short(String.t()) :: {:ok, [String.t()]} | {:error, term()}

Returns the raw git status --short --untracked-files=all lines for the repo at root, one entry per changed or untracked file. Used by validation-rule changed-file discovery.

verify_commit(root, ref)

@spec verify_commit(String.t(), String.t()) :: {:ok, String.t()} | :error

Resolves ref to a commit SHA if it exists locally (rev-parse --verify --quiet ref^{commit}). Never touches the network; pair with fetch_ref/3 to make remote-only refs resolvable via FETCH_HEAD.

worktree_root()

@spec worktree_root() :: String.t() | nil

Returns the toplevel of the current worktree, or nil when not in one.