Documentation
¶
Overview ¶
Package gitutil provides shared git safety primitives used by both the daemon (pull/fetch) and CLI (push/commit) code paths.
Index ¶
- Constants
- func FetchHeadAge(repoPath string) (time.Duration, bool)
- func GitHTTPTimeoutFlags() []string
- func HasLockFiles(gitDir string) []string
- func HasMissingLFSObjects(ctx context.Context, repoPath string) bool
- func IsGitRepo(path string) bool
- func IsLFSPushError(output string) bool
- func IsRebaseInProgress(repoPath string) bool
- func IsSafeForGitOps(repoPath string) error
- func PushWithRetry(ctx context.Context, repoPath string, opts PushOpts) error
- func RemoveStaleLockFiles(gitDir string) (removed []string, errs []error)
- func RepairMissingLFSObjects(ctx context.Context, repoPath string) (int, error)
- func ResolveRebaseAcceptTheirs(ctx context.Context, repoPath string, safePrefixes []string, ...) error
- func RunGit(ctx context.Context, repoPath string, args ...string) (string, error)
- func SanitizeOutput(output string) string
- func StripLFSConfig(repoPath string)
- type GitRunner
- type PushOpts
- type RealRunner
Constants ¶
const MinFetchHeadAge = 30 * time.Second
MinFetchHeadAge is the minimum age of FETCH_HEAD before we'll fetch again. Prevents redundant fetches if another process fetched recently.
const StaleLockAge = 5 * time.Minute
StaleLockAge is how old a git lock file must be before we consider it abandoned. Git operations normally hold locks for milliseconds to a few seconds, but a slow git pull --rebase on a large repo over a poor network can hold index.lock for several minutes. 5 minutes is conservative enough to cover legitimate operations while still recovering from crashed processes.
Variables ¶
This section is empty.
Functions ¶
func FetchHeadAge ¶
FetchHeadAge returns how long ago FETCH_HEAD was last modified. Returns (0, false) if FETCH_HEAD doesn't exist or can't be read.
func GitHTTPTimeoutFlags ¶ added in v0.6.0
func GitHTTPTimeoutFlags() []string
GitHTTPTimeoutFlags returns git config flags that bound DNS/TCP/TLS connection time and detect stalled transfers. Without these, git inherits the OS DNS resolver timeout (~13 min on macOS) which blocks background operations.
- http.connectTimeout=10: fail DNS+TCP+TLS within 10s
- http.lowSpeedLimit=1000: minimum bytes/sec during transfer
- http.lowSpeedTime=15: abort if below lowSpeedLimit for 15s
func HasLockFiles ¶
HasLockFiles checks .git/ for stale lock files that block git operations. Returns the names of lock files found (empty slice = safe to proceed).
func HasMissingLFSObjects ¶ added in v0.5.0
HasMissingLFSObjects returns true if the repo has LFS-tracked files whose backing objects are missing locally.
func IsGitRepo ¶ added in v0.6.0
IsGitRepo checks whether path is the root of a valid git repository. It reads .git/HEAD, which works for both regular repos (where .git is a directory) and worktrees (where .git is a file pointing to the real git dir). A readable HEAD is the most reliable lightweight check — it catches partial clones and corrupt repos that a simple os.Stat(".git") would miss.
func IsLFSPushError ¶ added in v0.5.0
IsLFSPushError returns true if the error output indicates a push failure caused by missing LFS objects (either local pre-push hook or server-side). "failed to store" alone is NOT sufficient — macOS Keychain errors like "failed to store: -25300" (errSecItemNotFound) would false-positive.
func IsRebaseInProgress ¶
IsRebaseInProgress checks whether the repo is stuck in a broken rebase state. Returns true if .git/rebase-merge or .git/rebase-apply exists.
func IsSafeForGitOps ¶
IsSafeForGitOps combines lock file and rebase state checks into a single pre-flight check. Returns nil if safe to proceed, or an error describing why the repo is blocked.
func PushWithRetry ¶ added in v0.6.0
PushWithRetry pushes a git repo to its remote with pre-flight checks, retry, conflict resolution, and backoff.
SAFETY: Force push (--force, --force-with-lease) is banned. All push conflicts are resolved via pull --rebase. Our git remotes reject force pushes server-side, so any force push attempt would fail anyway.
Pre-flight: lock/rebase safety, LFS config cleanup, optional LFS repair, optional credential refresh.
Retry loop: up to MaxRetries attempts with linear backoff (1s, 2s, 3s...). On non-fast-forward rejection: pulls with --rebase --autostash, optionally auto-resolves conflicts for paths in AutoResolvePrefixes.
func RemoveStaleLockFiles ¶ added in v0.6.0
RemoveStaleLockFiles removes git lock files older than StaleLockAge. Safe to call at daemon startup or before pull operations — only removes files that no running git process could still be holding. Returns the names of files removed and any removal errors encountered.
func RepairMissingLFSObjects ¶ added in v0.5.0
RepairMissingLFSObjects detects LFS pointer files whose backing objects are missing locally (lost during GC reclone, interrupted push, etc.) and replaces them with empty content so future pushes aren't blocked by the remote's pre-receive hook rejecting missing LFS objects.
The repair:
- Detects missing LFS objects via `git lfs ls-files`
- Replaces each orphaned pointer with empty content
- Commits the fix
- Sets lfs.allowincompletepush=true so the local pre-push hook doesn't block (the server may still reject — caller should retry via rebase)
Returns the number of repaired files and any error. Safe to call on repos without LFS — returns (0, nil) immediately.
func ResolveRebaseAcceptTheirs ¶ added in v0.5.0
func ResolveRebaseAcceptTheirs(ctx context.Context, repoPath string, safePrefixes []string, denyPrefixes ...[]string) error
ResolveRebaseAcceptTheirs attempts to resolve a rebase conflict by accepting the incoming version of all conflicted files, but ONLY if every conflicted file is under one of the given safe prefixes and NOT under any deny prefix.
This is safe for data directories (like data/) where the content is derived from an external source and the next sync cycle will re-fetch the latest version anyway. Last-write-wins is the correct strategy.
The denyPrefixes parameter is optional — pass nil for no exclusions. Deny prefixes carve out exceptions from the safe set: e.g., safePrefixes ["data/"] with denyPrefixes ["data/proprietary/"] means data/github/prs.json is safe but data/proprietary/keys.json is not.
Returns nil if the rebase was successfully continued after resolution. Returns an error if any conflicted file fails the safety check (the rebase is NOT aborted — caller should abort if needed).
func RunGit ¶
RunGit executes a git command with context for timeout/cancellation. Output is auto-sanitized to remove credentials. Use repoPath="" for commands that don't need -C.
func SanitizeOutput ¶
SanitizeOutput removes credentials and harmless noise from git command output.
func StripLFSConfig ¶
func StripLFSConfig(repoPath string)
StripLFSConfig removes lfs.repositoryformatversion from local git config. This config is set by git-lfs when filter.lfs.required=true is global, but it causes HTTP 403 on push to GitLab when the server-side ALB doesn't expect LFS-aware clients. Safe to call on any repo — no-op if not set.
Types ¶
type GitRunner ¶ added in v0.6.0
type GitRunner interface {
RunGit(ctx context.Context, repoPath string, args ...string) (string, error)
}
GitRunner abstracts git command execution for testability.
func DefaultRunner ¶ added in v0.6.0
func DefaultRunner() GitRunner
DefaultRunner returns the production GitRunner.
type PushOpts ¶ added in v0.6.0
type PushOpts struct {
// AutoResolvePrefixes lists path prefixes where accept-theirs conflict
// resolution is safe (e.g., "data/github/", "data/murmurs/").
// Empty means no auto-resolve — rebase failures abort immediately.
AutoResolvePrefixes []string
// AutoResolveDenyPrefixes lists path prefixes excluded from auto-resolution.
// These carve out exceptions from AutoResolvePrefixes using most-specific-wins
// semantics — e.g., deny "data/proprietary/" while allowing "data/".
AutoResolveDenyPrefixes []string
// RepairLFS runs RepairMissingLFSObjects before pushing.
RepairLFS bool
// PrePush is called before the push loop starts (after lock/LFS checks).
// Use for credential refresh or other caller-specific setup.
// Non-nil errors are logged as warnings but do not prevent the push attempt.
PrePush func(repoPath string) error
// MaxRetries is the number of push attempts. Zero means use default (3).
// To attempt exactly once with no retries, set to 1.
MaxRetries int
// OpTimeout is the timeout per git operation (default 60s).
OpTimeout time.Duration
// Logger for push diagnostics (defaults to slog.Default).
Logger *slog.Logger
}
PushOpts configures push behavior for PushWithRetry.
type RealRunner ¶ added in v0.6.0
type RealRunner struct{}
RealRunner executes git commands via os/exec (production implementation).