gitutil

package
v0.8.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 10, 2026 License: MIT Imports: 6 Imported by: 0

Documentation

Overview

Package gitutil holds the shared, isolated git queries. It is transport-agnostic: no stdout, no os.Exit, no CLI knowledge.

Every git invocation here neutralises global and system config, so a developer's ~/.gitconfig or ~/.gitignore can never change what abcd reports about a repository. Queries fail open — when git is unavailable or the directory is not a repository, nothing is claimed rather than an error raised, so a convention check degrades to "cannot tell" instead of asserting a violation it has no evidence for.

Index

Constants

This section is empty.

Variables

View Source
var ErrNoCheckoutRoot = errors.New("no checkout root")

ErrNoCheckoutRoot is CheckoutRoot's refusal: there is no checkout whose record store the caller's working directory belongs to, so there is no store to address. A surface maps it to an exit code and its own wording; this package never prints.

Functions

func CheckIgnored

func CheckIgnored(root string, candidates []string) map[string]struct{}

CheckIgnored returns the subset of repo-relative candidates that git actually ignores, via a single isolated `git check-ignore -z -v --stdin`. Batching keeps one subprocess for an arbitrary number of paths.

The index is consulted (no `--no-index`), so this mirrors git's real ignore decision: git never ignores a tracked file, even one force-added against a matching pattern — reporting such a path as ignored would invert the answer for callers asking "is this file committed-durable?" or "must this be excluded from the release bundle?".

A negation record (a pattern beginning `!`) un-ignores its path and so does not count as ignored. When git is unavailable or root is not a repository the result is empty (fail open).

func CheckoutRoot added in v0.8.0

func CheckoutRoot(cwd, store string) (string, error)

CheckoutRoot answers the question every front door onto a repository-scoped record store has to ask before it builds a request: which checkout's store does a caller standing in cwd address?

It is the ONE resolution for that question, and it exists because two front doors skipped asking it and reached the same failure independently. The capture verbs handed their working directory to the ledger core as an explicit repo root, so a verb run from a subdirectory addressed a ledger that was not there: a read reported open 0 against a populated checkout, and a write minted a second ledger under the subdirectory and reported success with a repo-relative path that looked ordinary (iss-2609090951291524). `decide` did the same to the decision store, and one directory further out: run outside every repository it exited 0 and laid a full ADR store in whatever plain directory the caller stood in (iss-2609091707224329). The resolution is store-agnostic, so it lives here rather than in either store's package, and `store` — a noun phrase naming what is being addressed, "the issue ledger", "the decision store" — is the ONLY thing that varies between callers.

Three outcomes, and only the first is a root:

  • git names a toplevel: that is the answer, whoever owns the checkout.
  • git will not answer for a repo-SHAPED tree (git absent from PATH, a corrupt .git, an ownership refusal under the isolated env): REFUSED, naming that git could not answer. RepoShapedRoot is read here as a CLASSIFIER and never as a root: it is a marker walk, which accepts any directory merely carrying the name and has neither the shape check nor the ownership gate the rules-root resolver grew (iss-2609090947359464), so returning its answer is the one change that would make that walk live. A store addressed by a guess is the defect this function closes, one directory further out.
  • nothing repo-shaped anywhere above: REFUSED. Laying a store in whatever directory the caller stood in is not a lenient fallback — the records would sit outside any checkout, committed by nothing and read by nothing, which is the same lost trail this resolution exists to prevent. Every store this resolves for is per-repository by definition.

func InRepo

func InRepo(root string) bool

InRepo reports whether root is inside a git working tree. A convention rule uses it to tell "git says this path is not ignored" apart from "git cannot answer" (git absent, or not a repo) — the latter is "cannot tell", never "compliant".

func IsAncestor added in v0.6.8

func IsAncestor(root, ancestor, descendant string) (bool, error)

IsAncestor reports whether ancestor is an ancestor of (or equal to) descendant under root, via `git merge-base --is-ancestor`. git encodes the answer in the exit status — 0 is "yes", 1 is "no", and anything else (128 for a bad object, a missing repository) is a real failure — so a bare Run cannot be used: it folds the informative 1 into a generic error. A caller deriving a release's content commit from the receipts directory needs the three outcomes kept apart: "yes" selects a candidate, "no" skips it, and only a genuine git failure is fatal.

func IsIgnored

func IsIgnored(root, path string) bool

IsIgnored reports whether a single repo-relative path is ignored by git. It is CheckIgnored for the one-path case; prefer CheckIgnored when asking about several paths, to keep it to one subprocess.

func IsolatedEnv

func IsolatedEnv() []string

IsolatedEnv returns the child environment an isolated git command runs with: the parent environment scrubbed of every repo-selection and config-injection variable (GIT_DIR, GIT_WORK_TREE, GIT_CONFIG_*, …), plus the config-file neutralisers. A front door that must run a git command this package does not already wrap — a probe needing --stdin, say — uses this instead of os.Environ() so it inherits the same isolation as Run/RunLimited. Reaching for os.Environ() directly lets an inherited GIT_DIR/GIT_WORK_TREE silently redirect the command at a DIFFERENT repository, which for a secret-hygiene gate is a real vulnerability.

func RepoShaped added in v0.6.7

func RepoShaped(root string) bool

RepoShaped reports whether root sits anywhere inside a tree carrying a .git entry — a directory, or the file a worktree or submodule leaves. It is deliberately cruder than InRepo: its job is to tell "not a repository" apart from "a repository git would not answer for" (git absent from PATH, a corrupt .git, an ownership refusal under the isolated env). Only the first is safe to treat as "nothing is tracked here"; the others can commit, and nothing can say what git would answer.

It walks to the filesystem root, because git does: checking root alone answers "not a repository" for every SUBDIRECTORY of one.

func RepoShapedRoot added in v0.7.1

func RepoShapedRoot(root string) string

RepoShapedRoot is RepoShaped's walk with its answer kept: the nearest directory at or above root carrying a .git entry, or "" when there is none. It is the toplevel a caller can still bound work at when git itself will not name one — the ownership refusal under the isolated env, git absent from PATH, a corrupt .git — where the alternative is treating a real working tree as an unbounded directory.

It is a MARKER, not git's answer: it does not read .git, so it cannot tell a valid repository from a directory that merely holds the name, and for a submodule or linked worktree it reports the tree the .git file sits in (which is the working-tree root a config walk wants). A caller that needs git's own answer must ask git.

func RootCommit added in v0.6.9

func RootCommit(root string) string

RootCommit returns the repository's canonical (first) root-commit SHA, or "" when it cannot be derived — git absent, not a repository, no commits. It is total: a caller keying a store or a marker on the repository's identity gets "" rather than an error, and decides for itself what an unidentified repository means. A repository can have several root commits (an octopus of unrelated histories); the first `rev-list` reports is the canonical one, the same choice the history registry and the lifeboat probe make.

The output is bounded (one object name, so the cap is generous) rather than buffered whole: a hostile repository must not be able to make an identity probe allocate.

func Run

func Run(root string, args ...string) (string, error)

Run executes a read-only git command under root with the developer's global and system config neutralised, returning trimmed stdout. It is the shared primitive for tooling that reads git history (the lifeboat probe's Tier-0 adapters); centralising it keeps every caller on the same isolated environment rather than re-deriving the exec plumbing. An error (git absent, not a repo, a failing subcommand) is returned verbatim so the caller can decide whether "git cannot answer" degrades to empty or is fatal.

func RunCapped

func RunCapped(root string, maxBytes int, args ...string) (string, error)

RunCapped is RunLimited for callers whose answer is only correct if it is COMPLETE. It returns an error rather than a truncated string when git's output exceeds maxBytes.

The distinction is the whole reason both exist. A truncated `git log` is not a shorter history; it is a wrong one, and a caller that cannot tell the two apart will publish the wrong one with no sign that anything was dropped. RunLimited's callers (the lifeboat probe over an archived repository) would genuinely rather have a partial answer than none, and keep that behaviour.

func RunLimited

func RunLimited(root string, maxBytes int, args ...string) (string, error)

RunLimited is Run with a hard cap on how much stdout is buffered. A hostile or archived repository can make a read-only command (a full `git log`) emit arbitrarily much output; the unbounded `Output()` would buffer all of it. The probe uses this so a giant history cannot exhaust memory — output past maxBytes is discarded (the last retained line may be truncated, which degrades a single probe rather than crashing it). On failure the error carries git's bounded stderr — the reason, not just the exit status.

func ScrubbedEnv

func ScrubbedEnv() []string

ScrubbedEnv is IsolatedEnv without the global/system config-file neutralisers: the parent environment with every repo-selection and config-injection variable stripped (GIT_DIR, GIT_WORK_TREE, GIT_INDEX_FILE, GIT_CONFIG_COUNT/PARAMETERS/ KEY_*/VALUE_*, …) but with ~/.gitconfig and the system config still in effect. It is for a git command that MUST honour the developer's real global config — the identity probe reads user.name/user.email to redact the caller's OWN identity, and those overwhelmingly live in global config, so suppressing it (as IsolatedEnv does) would blind the probe and silently stop redacting a real identity leak. Scrubbing still defeats the two attacks that matter here: an inherited GIT_DIR redirecting the probe at a different repository, and an injected GIT_CONFIG_* forging a fake identity that displaces the real one.

func TrackedFiles

func TrackedFiles(root string) ([]string, error)

TrackedFiles returns the repo-relative paths git tracks under root, NUL-safe so a filename with a newline cannot desync the list. Outside anything repo-shaped it returns no files and no error — a scan over committed files then degrades to "nothing to scan" rather than failing. In a repo-shaped tree git cannot answer for, and inside a repo on any other ls-files failure (a corrupt index, say), it returns an error, so a caller cannot mistake "could not read the repository" for "nothing tracked" and report a scanning rule compliant after reading zero files.

Types

This section is empty.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL