gitrepo

package
v0.11.5 Latest Latest
Warning

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

Go to latest
Published: Oct 14, 2026 License: MIT Imports: 31 Imported by: 0

Documentation

Index

Constants

View Source
const PathClassificationBudget = 5 * time.Second

PathClassificationBudget bounds each git call that classifies touched paths (IgnoredPaths, GitlinkPaths) on agent-hook paths. It matches WorktreeContentHashBudget, the per-call budget of the sibling hash-object step, and stays well inside StatusWalkBudget: these are pathspec-limited lookups, so a call that runs this long is wedged (a hung filesystem or a stuck index lock), not slow. The caller's context still applies on top.

View Source
const StatusWalkBudget = 20 * time.Second

StatusWalkBudget bounds the wall-clock time of one worktree status read on the agent-hook capture paths — the go-git walk in StatusWithBudget and the first-checkpoint `git status` subprocess in the checkpoint store. Agents cancel a hook at its timeout (for Claude Code, see claudecode's captureHookTimeoutSec and checkpointHookTimeoutSec), but cancelling has not reliably ended the hook PROCESS — an unbounded walk over a pathological worktree (e.g. a stray `git init` in $HOME with no .gitignore) has been observed grinding for hours at gigabytes of RSS after the agent gave up. 20s is chosen as far beyond any healthy repository (a warm walk over a large working set finishes in seconds) while leaving the rest of the hook's timeout for the remaining capture path — transcript copy, tree building, state writes — so the hook still degrades gracefully and exits instead of being orphaned. Hook timeouts must stay above this budget.

View Source
const WorktreeContentHashBudget = 5 * time.Second

WorktreeContentHashBudget bounds native Git clean-filter processing on the post-commit hook path. A broken or hanging custom filter must not leave the hook process blocked indefinitely.

Variables

View Source
var (
	// ErrRefCASConflict means the ref no longer has the expected old value.
	ErrRefCASConflict = errors.New("git reference changed during compare-and-swap")
	// ErrRefLocked means native Git could not acquire the ref lock.
	ErrRefLocked = errors.New("git reference lock is unavailable")
	// ErrRefSymbolic means the requested CAS target is a symbolic reference.
	ErrRefSymbolic = errors.New("git reference is symbolic")
)
View Source
var ErrStatusBudgetExceeded = errors.New("worktree status walk exceeded time budget")

ErrStatusBudgetExceeded reports that a worktree status walk was abandoned because it exceeded StatusWalkBudget. Capture is fail-open: hook-path callers must treat this like any other status failure — warn and continue with transcript-derived data — never fail the hook.

View Source
var ErrWorktreeMetadataNotFound = errors.New("worktree Git metadata not found")

ErrWorktreeMetadataNotFound reports that an explicit worktree root has no .git entry. Repository opening uses it to distinguish a possible bare repository from broken worktree metadata.

Functions

func CommitAtReference added in v0.11.4

func CommitAtReference(ctx context.Context, repo *git.Repository, name plumbing.ReferenceName) (*object.Commit, error)

CommitAtReference reads an exact reference and peels annotated tags to a commit. Unlike ResolveRevision, it does not accept revision expressions or abbreviations. Missing refs, missing objects, and non-commit targets remain errors; callers decide whether their workflow treats those as absence. Replace refs require native interpretation and are returned as unsupported objects so migrated callers can use their native compatibility path.

func CompareAndSwapRef added in v0.10.6

func CompareAndSwapRef(
	ctx context.Context,
	repoRoot string,
	refName plumbing.ReferenceName,
	newHash, expectedHash plumbing.Hash,
) error

CompareAndSwapRef atomically updates a direct ref through native Git and rejects symbolic refs. ZeroHash as expectedHash requires that refName does not exist.

func EnvWithoutRepoOverrides added in v0.10.3

func EnvWithoutRepoOverrides() []string

EnvWithoutRepoOverrides returns the current environment minus git's repo-selector variables (GIT_DIR, GIT_COMMON_DIR, GIT_WORK_TREE, GIT_INDEX_FILE), so a git subprocess resolves its repository from cmd.Dir as the call site intends.

Use this for git subprocesses that must resolve their target independently of the enclosing hook, using cmd.Dir or `-C`. Inheriting these variables makes the child silently operate on the hook's repository instead: `git -C <other> rev-parse` reports the hook's repo, and an index-touching command reads and writes whatever GIT_INDEX_FILE names.

Commands inspecting the commit being prepared must retain Git's temporary GIT_INDEX_FILE rather than use this helper.

Deliberately not applied to user-invoked commands that operate on the current directory (`entire status`, `entire doctor`, `entire review`): there a GIT_DIR the user exported in their own shell is an instruction, not contamination.

func GitlinkPaths added in v0.11.5

func GitlinkPaths(ctx context.Context, repoRoot string, paths []string) (map[string]struct{}, error)

GitlinkPaths returns the subset of paths (relative to repoRoot) that are submodule gitlinks (mode 160000) in HEAD's tree or in the index, so a submodule added but not yet committed counts too, and so does one whose removal is staged (it cannot be committed as a file either).

Both sides are pathspec-limited with literal pathspecs, so a name containing glob characters matches only itself, and both are bounded by what differs rather than by repository size, because callers include ancestor directories (a monorepo's "packages") in paths:

  • HEAD: `git ls-tree -z HEAD -- <paths>`, which without -r prints only the named entries, never a directory's subtree.
  • The index: `git diff-index --cached --raw -z --no-renames <base> -- <paths>`, where base is HEAD, or the empty tree when HEAD is unborn. It prints only index entries that differ from base under the named paths, so an unchanged directory costs nothing. (`git ls-files -s` would list the directory's entire subtree on every call.) --ignore-submodules=none keeps a `submodule.<name>.ignore` setting in .gitmodules or config from hiding a staged gitlink. A record whose destination mode is 160000 is a gitlink staged but not in HEAD; one whose source mode is 160000 and destination 000000 is a gitlink whose removal is staged. A gitlink present unchanged in both is found by the HEAD side.

The call is bounded by PathClassificationBudget on top of the caller's context. On error the returned set holds what was found so far; callers keep the remaining paths (fail open).

func HashWorktreeFiles added in v0.11.0

func HashWorktreeFiles(
	ctx context.Context,
	worktreeRoot string,
	paths []string,
) (map[string]plumbing.Hash, error)

HashWorktreeFiles returns the Git blob hash of each regular working-tree file after applying Git's path-specific clean filters. Git hash-object follows symlinks, so callers comparing Git symlink blobs must handle those separately. Large path sets are split across commands so an oversized argv cannot fail before Git inspects anything.

The paths must be repository-relative. They are file operands, not pathspecs, so names containing pathspec magic are interpreted literally. The returned map can contain successful hashes alongside a non-nil error; callers can retain filter-aware results and degrade only the files Git could not hash.

func IgnoredPaths added in v0.11.5

func IgnoredPaths(ctx context.Context, worktreeRoot string, worktree *os.Root, paths []string) (ignored, refused map[string]struct{}, err error)

IgnoredPaths returns the subset of paths (relative to worktreeRoot) that git's ignore rules exclude, from `git check-ignore -z --stdin`.

Tracked paths are never reported (no --no-index): a tracked file that happens to match an ignore pattern can still be committed.

git refuses some paths outright and then fails the whole batch with exit 128, after printing only some answers: a path inside a submodule ("Pathspec is in submodule") or beneath a symlinked directory ("beyond a symbolic link"). Neither can be committed through the superproject's worktree, and both are returned in refused for the caller to drop:

  • A path with a symlinked ancestor directory is found in process first (symlinkedAncestorPaths, no-follow Lstat through worktree, the caller's anchor root for worktreeRoot, one stat per distinct ancestor) and never sent to git, so the common case costs no extra process. A nil worktree skips this layer; gitrepo cannot open the anchor itself (worktreedir's tests import gitrepo).
  • If a batch is still refused, a probe with a harmless path establishes that git can answer at all, and the batch is then bisected: halves are asked recursively and only a single-path batch that git refuses lands in refused, so k refused paths among n cost O(k log n) processes rather than n. If the probe fails too, git cannot answer here and an error is returned with nothing refused, so callers keep the paths rather than drop them on a systemic failure.

The call is bounded by PathClassificationBudget on top of the caller's context, so a wedged check-ignore cannot consume a hook's whole budget.

func ListWorktreePaths added in v0.11.5

func ListWorktreePaths(ctx context.Context, worktreeRoot string) ([]string, error)

ListWorktreePaths returns the worktrees git has registered for the repository worktreeRoot belongs to (ParseWorktreePaths), via `git worktree list --porcelain` run with -C worktreeRoot.

func NativeReadSelectorEnvVars added in v0.11.4

func NativeReadSelectorEnvVars() []string

NativeReadSelectorEnvVars returns every variable ReadsNeedNativeGit treats as selecting a store go-git would not open. It is the single source for that list; test isolation derives its own list from it rather than copying it.

func OpenCurrent

func OpenCurrent(ctx context.Context) (*git.Repository, error)

OpenCurrent opens the current git worktree with object alternates enabled. The caller owns the returned repository and must close it.

A worktree root that will not resolve is an error, not a reason to open the current directory. This used to fall back to ".", which is a different repository whenever the two disagree, and go-git disagrees with git in exactly the cases that made the resolution fail:

  • git exports GIT_DIR and GIT_WORK_TREE to the hooks it runs, and paths.WorktreeRoot honours them. OpenPath(".") cannot see them, so a hook running for repo A whose git lookup failed opened repo B, the unrelated repository it happened to be sitting in. Verified.
  • git refuses a repository whose ownership fails its safe.directory check, and refuses a .git it cannot parse. go-git applies neither check, so the fallback opened repositories the user's own git declines to touch.

func OpenPath

func OpenPath(repoRoot string) (*git.Repository, error)

OpenPath opens a git repository with object alternates enabled. The caller owns the returned repository and must close it.

func ParseWorktreePaths added in v0.11.5

func ParseWorktreePaths(porcelain string) []string

ParseWorktreePaths returns the path of every non-bare worktree in `git worktree list --porcelain` output, detached ones included, exactly as git printed it and in output order. A bare repository's block is skipped: it has no working tree and so no .entire.

func PathStatuses added in v0.11.5

func PathStatuses(ctx context.Context, repoRoot string, paths []string) (map[string]string, error)

PathStatuses returns the porcelain v1 status code ("XY", e.g. " M", "A ", " D", "??") of each of paths (relative to repoRoot) that `git status` reports, from pathspec-limited `git --no-optional-locks status --porcelain=v1 -z --untracked-files=all --no-renames` calls with literal pathspecs. A path absent from the result matches both the index and HEAD. Bounded by PathClassificationBudget on top of the caller's context.

git status applies the same index-aware conversion rules `git add` does (a file already committed with CRLF is not normalized by text=auto or core.autocrlf), which `git hash-object` does not. That is why "does the worktree still differ from what was committed" is asked of status rather than answered by hashing the worktree file.

func PathsInHEAD added in v0.11.5

func PathsInHEAD(ctx context.Context, repoRoot string, paths []string) (map[string]struct{}, error)

PathsInHEAD returns the subset of paths (relative to repoRoot) that HEAD's tree has, from pathspec-limited `git ls-tree -z --name-only HEAD` calls with literal pathspecs. An unborn HEAD has no paths. Bounded by PathClassificationBudget on top of the caller's context.

func PathsStagedAsNew added in v0.11.5

func PathsStagedAsNew(ctx context.Context, repoRoot string, paths []string) (map[string]struct{}, error)

PathsStagedAsNew returns the subset of paths (relative to repoRoot) whose blob is staged as a new file: in the index with content and absent from HEAD (the empty tree when HEAD is unborn), so the next commit adds it. It comes from pathspec-limited `git diff-index --cached --name-only -z --diff-filter=A --no-renames --ita-invisible-in-index <base>` calls with literal pathspecs; diff-index, unlike `git diff`, never refreshes the index.

Index membership alone (`ls-files --cached`) is not enough: an intent-to-add entry (`git add -N`) is listed there but holds no blob and is never committed. diff-index would report it as added (with the empty blob, which a genuinely staged empty file shares), so --ita-invisible-in-index hides it, as `git diff --cached` and `git commit` do. Bounded by PathClassificationBudget on top of the caller's context.

func ReadsNeedNativeGit added in v0.11.4

func ReadsNeedNativeGit(ctx context.Context) bool

ReadsNeedNativeGit reports whether a local ref or commit read must use native Git instead of go-git. It keeps explicit store selectors (NativeReadSelectorEnvVars) and CLI-backed reference stores on the native path, and ENTIRE_NATIVE_GIT_READS forces native for every caller. Detect reftable before opening a repository: even opening its adapter performs a native reference lookup. Failed discovery also stays native, never guessing a CWD repository. A GIT_DIR naming the discovered Git directory selects the store go-git would open anyway; Git exports exactly that to hooks in linked worktrees.

Caller contract: the gate covers ref and object reads only. Callers must not read the index, attributes, or pathspecs on the go-git path; GIT_INDEX_FILE, GIT_ATTR_* and pathspec variables are deliberately not consulted. The guard test in read_guard_test.go lists the approved call sites.

func ResolveCommonGitPath added in v0.10.3

func ResolveCommonGitPath(dotGitPath string) (string, error)

ResolveCommonGitPath resolves the shared Git directory for a resolved .git directory. An empty result means the repository has no commondir file.

func ResolveDotGitPath added in v0.10.3

func ResolveDotGitPath(repoRoot string) (string, error)

ResolveDotGitPath resolves the .git entry for a worktree without opening the repository. Callers that need Git metadata should use this shared resolver.

func SetStatusBudgetBreachedForTesting added in v0.10.3

func SetStatusBudgetBreachedForTesting(breached bool)

SetStatusBudgetBreachedForTesting overrides the process-local breach latch so tests can exercise budget-breach degradation without a slow walk.

func Status added in v0.10.0

func Status(_ context.Context, repo *git.Repository) (git.Status, error)

Status is the single entry point for reading go-git worktree status; the forbidigo rule in .golangci.yaml enforces this and names this signature, so the context parameter is part of that contract and is where cancellation or a perf span would attach.

Worktree.Status() walks the worktree, so its cost scales with working-set size rather than with the size of the change being inspected — it is the most expensive git read on the hook paths. Avoid calling it more than once per hook.

func StatusWithBudget added in v0.10.3

func StatusWithBudget(ctx context.Context, repo *git.Repository) (git.Status, error)

StatusWithBudget is Status bounded by StatusWalkBudget, for agent-hook capture paths. go-git's Worktree.Status is not context-cancellable, so on breach the walk goroutine is abandoned — it dies with the short-lived hook process, which is the point — and the returned error wraps ErrStatusBudgetExceeded so callers' warn-and-continue degrade paths apply. Paths where a user is actively waiting on a command (review via review_target.go, and `session adopt` via detectFileChangesUnbounded) keep calling Status directly.

Types

type WorktreeBranch added in v0.11.5

type WorktreeBranch struct {
	// Path is the worktree path exactly as git printed it.
	Path string
	// Branch is the short branch name (refs/heads/ stripped).
	Branch string
}

WorktreeBranch is one worktree from `git worktree list --porcelain` that has a branch checked out.

func ParseWorktreeBranches added in v0.11.5

func ParseWorktreeBranches(porcelain string) []WorktreeBranch

ParseWorktreeBranches returns the worktrees in `git worktree list --porcelain` output that have a branch checked out, in output order.

Each worktree is a block beginning with a `worktree <path>` line and separated by a blank line; a `branch <ref>` line only appears for non-detached worktrees. The path is reset at each block boundary and a branch line is only paired with a worktree line from the same block, so a detached worktree (no branch line) can never pair a branch with a stale path or report an empty one.

type WorktreeMetadata added in v0.10.6

type WorktreeMetadata struct {
	GitDir     string
	CommonDir  string
	WorktreeID string
}

WorktreeMetadata contains Git directory facts for one explicit worktree root. Paths are cleaned and absolute but retain their lexical spelling.

func ResolveWorktreeMetadata added in v0.10.6

func ResolveWorktreeMetadata(worktreeRoot string) (WorktreeMetadata, error)

ResolveWorktreeMetadata resolves filesystem metadata for an explicit worktree root. It does not discover a root, inspect Git environment variables, run Git, cache results, or decide caller policy.

Jump to

Keyboard shortcuts

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