On this page
Package git wraps os/exec calls to the git binary and is the sole interface through which safegit interacts with git plumbing commands.
#internal/git
#internal/git
Package git wraps os/exec calls to the git binary and is the sole interface through which safegit interacts with git plumbing commands. All functions shell out to git and return structured results; no other package may invoke git directly.
#AttrUnspecified
const AttrUnspecified = "unspecified"AttrUnspecified is what check-attr answers for a path no attributes file says anything about. CheckAttr passes it through rather than dropping the entry, because "the path was asked about and nothing was set" and "the path was never asked about" are different facts.
#StyleMerge
const StyleMerge ConflictStyle = "merge"StyleMerge is git's default: the two sides separated by "=======".
#StyleDiff3
const StyleDiff3 ConflictStyle = "diff3"StyleDiff3 adds the merge base between them, under "|||||||".
#StyleZdiff3
const StyleZdiff3 ConflictStyle = "zdiff3"StyleZdiff3 is diff3 with lines common to both sides hoisted out of the conflicted region.
#ZeroSHA
const ZeroSHA = "0000000000000000000000000000000000000000"ZeroSHA is git's "this object must not exist" convention: the all-zero object name. Passed to update-ref as the expected old value it means "create only" -- git refuses with "reference already exists" when the ref is already there.
#ZeroMode
const ZeroMode = "000000"ZeroMode is how git's raw diff format spells the mode of a side that is not there: the addition's source, the deletion's destination.
#ErrDetachedHead
var ErrDetachedHead = fmt.Errorf("HEAD is detached (not on a branch); check out a branch first or use --branch")ErrDetachedHead is returned when HEAD is not on a branch.
#ErrNoExpectedValue
var ErrNoExpectedValue = errors.New("update-ref requires an expected old value; pass git.ZeroSHA to require that the ref does not exist yet")ErrNoExpectedValue is returned by UpdateRef and DeleteRef when the caller supplies no expected old value.
It used to mean "omit the old-value argument", which is git's spelling of an UNCONDITIONAL write: the ref moved to whatever the caller computed no matter what another process had done to it in the meantime. That is precisely the compare-and-swap safegit exists to provide, so the empty string is now a refusal rather than a mode. A caller that means "this ref must not exist yet" says so with ZeroSHA.
#UnmergedEntry
type UnmergedEntry structUnmergedEntry is one unmerged index entry: a path at one of the three merge stages. A conflicted path has up to three of them (1 = the merge base, 2 = ours, 3 = theirs), and a stage is ABSENT when the path did not exist on that side -- an add/add conflict has no stage 1, a delete/modify conflict has no stage 2 or no stage 3.
#ConflictStyle
type ConflictStyle stringConflictStyle is git's merge.conflictStyle vocabulary: which shape git writes a conflicted region in.
#MergeFileOptions
type MergeFileOptions structMergeFileOptions shapes one merge-file reconstruction.
#CommitIdentity
type CommitIdentity structCommitIdentity pins the author and committer a commit is written with, timestamps included. A caller that has no identity to impose passes nil to CommitTree and gets git's own configured identity and the current time, which is what every ordinary commit wants; the rewrite paths, which must reproduce an existing commit's identity exactly, pass one.
#AuthorInfo
type AuthorInfo structAuthorInfo holds the name, email, and raw git date for an author or committer.
#CommitInfo
type CommitInfo structCommitInfo holds the parsed contents of a git commit object.
#TreeEntry
type TreeEntry structTreeEntry represents an entry from git ls-tree (blob, tree, or other object).
#ChangedPath
type ChangedPath structChangedPath is one entry of a recursive raw diff between two trees.
#ObjectEntry
type ObjectEntry structObjectEntry holds one object read from a git cat-file --batch stream.
#ObjectIterator
type ObjectIterator structObjectIterator streams objects from a long-running git cat-file process.
#CommitMessage
type CommitMessage structCommitMessage is one commit and the whole message it carries.
#IndexStage0
type IndexStage0 structIndexStage0 is one already-decided resolution applied to an index: every slot the path currently occupies is replaced by a single stage-0 entry naming Mode and SHA.
An empty Mode removes the path from the index entirely instead, which is what resolving a conflict by deleting the path means.
#MergeTreeResult
type MergeTreeResult structMergeTreeResult is what one git merge-tree --write-tree computed.
#UnmergedStages
func UnmergedStages(ctx context.Context, indexPath string) ([]UnmergedEntry, error)UnmergedStages lists the unmerged entries of an index, in git's own order.
indexPath names the index to read; an empty indexPath reads the repository's shared index. The listing is NUL-delimited, so a path holding a newline, a quote or a non-UTF-8 byte arrives exactly as it is stored.
#IndexEntries
func IndexEntries(ctx context.Context, indexPath string) ([]UnmergedEntry, error)IndexEntries lists EVERY entry of an index, at every stage, in git's own order. A quiet index answers entirely at stage 0; a conflicted one carries the unmerged path's stages 1/2/3 as well.
It exists for the marker verification, which has to read the content a conclusion is about to commit for paths that are NOT conflicted -- a path the operator resolved with git add before running the conclusion carries no stages at all, and is exactly where a forgotten marker hides.
#IndexPathsChangedFrom
func IndexPathsChangedFrom(ctx context.Context, treeish string) ([]string, error)IndexPathsChangedFrom lists the repo-relative paths whose entry in the shared index differs from the given tree-ish, unmerged paths included.
It is how the marker verification decides what a conclusion is about to RECORD: a path whose index entry already matches the first parent is not something the commit changes, and cannot introduce anything into it. The listing is NUL-delimited, so no path is C-quoted into something that names no file.
#AbbrevSHA
func AbbrevSHA(ctx context.Context, rev string) (string, error)AbbrevSHA returns the abbreviated object name git itself would print for a revision, honoring core.abbrev exactly as git's own conflict-marker labels do (git names the merge base on a diff3 marker line by this abbreviation, so a reconstruction that abbreviates differently is not byte-identical).
#CheckAttr
func CheckAttr(ctx context.Context, attrSource string, attrs []string, paths []string) (map[string]map[string]string, error)CheckAttr answers what the attributes files say about paths.
attrSource, when non-empty, is a tree-ish whose .gitattributes files are read INSTEAD of the working tree's (git's --attr-source). That is the whole reason this wrapper exists: during a conflicted merge the working tree's .gitattributes may itself be conflicted -- marker-laden and meaningless -- so an attribute that decides how safegit treats the conflict has to be read from a committed tree, where it necessarily predates the conflict.
The result is keyed by path, then by attribute name. A path git answers for is always present in the map; an attribute git says nothing about carries AttrUnspecified. A set-but-valueless attribute reads "set", an unset one "unset", exactly as git spells them.
Paths travel on stdin, so a path that looks like an option or holds a special byte is never re-interpreted.
#ParseConflictStyle
func ParseConflictStyle(value string) (ConflictStyle, error)ParseConflictStyle reads a merge.conflictStyle configuration value. An empty value is git's own default. An unrecognized value is an error rather than a silent fall back to the default: safegit would otherwise reconstruct a conflict in a shape git never wrote, and compare it against the real file.
#MergeFile
func MergeFile(ctx context.Context, ours, base, theirs []byte, opts MergeFileOptions) (merged []byte, conflicted bool, err error)MergeFile runs git's three-way file merge over three blob contents and returns the merged result, plus whether the merge conflicted.
This is how safegit reproduces the conflict-marked file git itself wrote into the working tree: given the index's stage 1/2/3 blobs and the attributes that were in force, merge-file emits the same bytes, because it is the same engine.
The three sides are written into a throwaway directory and merge-file is run with -p, so the result comes back on stdout and nothing in the repository or the working tree is touched. A missing side (an add/add conflict has no base) is passed as an empty file, which is what git's own merge does.
A conflicted merge is a NORMAL return, not an error: merge-file's exit status is the number of conflicts it left, and only a negative status (255 in practice) means it failed.
#StripComments
func StripComments(ctx context.Context, message string) (string, error)StripComments removes comment lines from a commit message the way git does when it commits one: it runs git stripspace --strip-comments, so the repository's own core.commentChar (or core.commentString) decides what a comment is, and blank-line collapsing matches git's.
The conclusion commands need it because the MERGE_MSG git leaves behind carries the "# Conflicts:" block, which is a comment in the draft and must not reach the commit object.
#ConfigGet
func ConfigGet(ctx context.Context, key string) (value string, set bool, err error)ConfigGet reads one git configuration value. set reports whether the key is configured at all: git exits 1 with no output for an absent key, which is an answer rather than a failure, and a caller that needs a default applies its own.
#WithDir
func WithDir(ctx context.Context, gitDir, workTree string) context.ContextWithDir returns a context that carries git directory overrides. All git functions that receive this context will automatically set GIT_DIR, GIT_WORK_TREE, and cmd.Dir on the subprocess, targeting the specified repo regardless of the process's current working directory.
The override itself lives in internal/gitexec, the one place that builds a git subprocess; this is the plumbing interface's spelling of it.
#WithRoot
func WithRoot(ctx context.Context, root string) context.ContextWithRoot returns a context carrying the repository-root working-directory pin. See gitexec.WithRoot for what the pin is for.
#Version
func Version(ctx context.Context) (gitversion.Version, error)Version returns the version of the git binary safegit is running against, parsed. It is the one place a caller asks; a feature with a version floor compares this against its floor via gitversion.Require.
#Run
func Run(ctx context.Context, args ...string) (stdout, stderr string, err error)Run executes a git command and returns stdout, stderr, and any error.
#RunWithEnv
func RunWithEnv(ctx context.Context, env []string, args ...string) (stdout, stderr string, err error)RunWithEnv executes a git command with additional environment variables.
#RunWithEnvStdin
func RunWithEnvStdin(ctx context.Context, env []string, stdin []byte, args ...string) (stdout, stderr string, err error)RunWithEnvStdin executes a git command with environment variables and stdin data.
#AnchorRoot
func AnchorRoot(ctx context.Context) (string, error)AnchorRoot returns the directory that repo-relative paths reported by git in this context resolve against.
Pinning the git SUBPROCESS working directory does not change how Go resolves a relative path: an os.Lstat, os.ReadFile or os.WriteFile on a path git just listed still resolves against the PROCESS working directory. From a subdirectory the two disagree, and the file the syscall reaches is not the file git named -- which is how a protection that reads a git listing and then touches the filesystem silently protects nothing. Every filesystem syscall that consumes a git-listed path goes through Anchor(AnchorRoot(ctx), path).
The order is most-specific-first: a context targeting another repository anchors at that repository's work tree, a pinned context at the pin, and an unpinned context at whatever the repository root is from here.
#Anchor
func Anchor(root, repoRelative string) stringAnchor joins a repo-relative path onto root. An absolute path is returned unchanged: a caller that already resolved a path must not have it re-rooted.
#RepoRoot
func RepoRoot(ctx context.Context) (string, error)RepoRoot returns the absolute path to the repository root.
#GitDir
func GitDir(ctx context.Context) (string, error)GitDir returns the ABSOLUTE path of the repository's git directory.
Absolute because every consumer joins a state-file name onto it -- MERGE_HEAD, index, safegit/ -- and then reaches that path with a Go filesystem call, which resolves a relative path against the PROCESS working directory. Plain rev-parse --git-dir answers .git whenever git ran at the top of the work tree, and safegit's own context pins every git subprocess to the repository root, so from a subdirectory that answer names
It is git's own canonicalized answer (--absolute-git-dir) rather than a filepath.Abs of the relative one, for the same reason ObjectsDir and HooksDir ask git: a linked worktree, a redirected git directory and a GIT_DIR override all break any join a caller could do itself.
#ObjectsDir
func ObjectsDir(ctx context.Context) (string, error)ObjectsDir returns the ABSOLUTE path of the repository's object store.
Absolute because the answer is used as a GIT_ALTERNATE_OBJECT_DIRECTORIES entry, which git resolves against whatever directory the child process runs in -- and safegit's children run in several (the repository root under the pin, another repository entirely at the explicit-directory sites). A relative answer would name a different store depending on who read it.
It is git's own answer rather than a join onto the git dir, so a linked worktree (whose objects live in the common git dir) and a repository whose object store is redirected both report the store git will actually use.
#HooksDir
func HooksDir(ctx context.Context) (string, error)HooksDir returns the ABSOLUTE path of the directory git runs hooks from.
It is git's own answer rather than a join onto the git dir, and it is the one place anything in safegit asks. Two configurations make the join wrong, and both are silent when it is: core.hooksPath redirects the directory entirely, and a LINKED WORKTREE's git dir (.git/worktrees/
#HeadRef
func HeadRef(ctx context.Context) (string, error)HeadRef returns the current branch ref (e.g. "refs/heads/main"). Returns ErrDetachedHead if HEAD is not on a branch.
#RevParse
func RevParse(ctx context.Context, rev string) (string, error)RevParse resolves a revision to a full SHA.
#EmptyTreeSHA
func EmptyTreeSHA(ctx context.Context) (string, error)EmptyTreeSHA is the object name of the tree with no entries, asked of git rather than spelled out.
The two well-known constants (sha1's 4b825dc6... and sha256's 6ef19b41...) are deliberately NOT hardcoded here. Hardcoding them would save one subprocess on paths that are already cold, in exchange for a table that has to be extended by hand the day git gains another hash algorithm -- and the failure then is silent, a name that resolves to nothing in a repository the table does not know. hash-object computes the name from the algorithm the repository actually uses, so it self-adapts.
It is hash-object -t tree on EMPTY STDIN rather than MkTree with no entries, which also yields the empty tree: mktree WRITES the object, which puts it in the class the preview quarantine exists for, while hash-object without -w computes the name and writes nothing at all. Empty stdin rather than /dev/null for the same reason every other hashing helper here takes bytes: safegit's hash-object callers hand git content, never a path (see the comment above HashObjectBytes), and /dev/null is not a path every platform has.
#HeadTreeish
func HeadTreeish(ctx context.Context) (string, error)HeadTreeish names the tree to compare the working tree, the index or a conclusion's first parent against: HEAD where it resolves, and the EMPTY TREE where it does not.
The second case is an UNBORN branch -- the state between git init and the first commit, and the state safegit undo of a root commit leaves behind. There is no HEAD there, and every git command that takes HEAD as a treeish is fatal, which is why the substitution is made here rather than left to each caller: a repository with no commits holds exactly the empty tree, so a diff against it reports precisely what a diff against HEAD reports on a born branch -- every staged addition, and nothing else.
The test is rev-parse --verify --quiet: without --quiet git prints its "ambiguous argument" advice and exits 128, so the cheap question would answer with noise on stderr in the ordinary case this function exists for.
#HeadIsUnborn
func HeadIsUnborn(ctx context.Context) boolHeadIsUnborn reports whether HEAD names a branch that does not exist yet.
It is the cheap question, asked with rev-parse --verify --quiet: without --quiet git prints its "ambiguous argument 'HEAD'" advice and exits 128, so the ordinary case this exists for would answer with noise on stderr.
A bool rather than (bool, error), because every caller is already inside a repository safegit resolved a git directory for, and the only other way this invocation fails is a repository nothing else in the process could read either.
#ReadTree
func ReadTree(ctx context.Context, indexPath, treeish string) errorReadTree populates a temporary index from a treeish (commit/tree SHA or ref).
#WriteTree
func WriteTree(ctx context.Context, indexPath string) (string, error)WriteTree writes the index content as a tree object, returns the tree SHA.
#CommitTree
func CommitTree(ctx context.Context, treeSHA string, parents []string, message string, identity *CommitIdentity) (string, error)CommitTree creates a commit object from a tree SHA and its parents, in the order given, and returns the new commit SHA. An empty parents slice creates a root commit; more than one parent creates a merge commit, which is why the parameter is a slice rather than a single SHA -- a caller that rewrites a merge commit with one parent silently unmerges the branch.
identity, when non-nil, pins the author and committer (see CommitIdentity).
#UpdateRef
func UpdateRef(ctx context.Context, ref, newSHA, oldSHA string) errorUpdateRef atomically updates a ref using compare-and-swap.
oldSHA is the expected current value and is MANDATORY. Pass ZeroSHA to require that the ref does not exist yet, which git enforces by refusing with "reference already exists".
#DeleteRef
func DeleteRef(ctx context.Context, ref, oldSHA string) errorDeleteRef atomically deletes a ref using compare-and-swap.
oldSHA is the expected current value and is MANDATORY, for the same reason it is on UpdateRef: without it git deletes whatever the ref points at now.
#AddFile
func AddFile(ctx context.Context, indexPath, filePath string) errorAddFile stages a file into a custom index.
An empty indexPath stages into the repository's shared index, the same convention UnmergedStages and SetIndexStage0 use.
#RmCached
func RmCached(ctx context.Context, indexPath, filePath string) errorRmCached removes a file or directory from a custom index without touching the working tree.
#DropFromIndex
func DropFromIndex(ctx context.Context, indexPath, repoRelPath string) errorDropFromIndex removes ONE exact path from a custom index, whether or not the file is still on disk and whatever its content is.
It is deliberately not RmCached. git rm --cached is a porcelain safety check as much as a removal: it refuses a path whose indexed content differs from both the working file and HEAD, and it reads HEAD -- the repository's real HEAD, which on a cross-branch operation is not the tree the index was seeded from. Untracking a file that is meant to STAY on disk, usually with content that has moved on since it was committed, is exactly the shape that check refuses. update-index --force-remove states the intent directly: drop this index entry, touch nothing else.
The path is repo-relative; git resolves it against the process working directory, which every safegit git call has pinned to the repository root.
#IsTracked
func IsTracked(ctx context.Context, rev, filePath string) (bool, error)IsTracked checks whether a file is tracked in the given revision's tree. Uses cat-file instead of ls-files because safegit never writes to the main index -- files committed via safegit exist in HEAD but not in .git/index.
The revision is a PARAMETER rather than a hardcoded HEAD because the tree a path must be judged against is the tree the operation is built on, which is not always HEAD: a commit --branch other builds on other's tip, and an amend builds on the tip it replaces. Asking HEAD there decides the request against a tree the operation will never touch. An empty rev means there is no such tree yet (an unborn ref), where nothing is tracked.
#ListSkipWorktreeFiles
func ListSkipWorktreeFiles(ctx context.Context) ([]string, error)ListSkipWorktreeFiles returns the paths of all files with the skip-worktree flag set in the main index. It parses git ls-files -v -z output, selecting records that start with "S " (the skip-worktree indicator).
The NUL-delimited form is what makes the answer usable: without -z git C-quotes any path that is not plain ASCII, and the quoted spelling names no index entry, so restoring the flag afterwards would fail on exactly the paths that most need it.
#ListTrackedIgnoredFiles
func ListTrackedIgnoredFiles(ctx context.Context) ([]string, error)ListTrackedIgnoredFiles returns the paths of all files that are tracked in the index but ignored by .gitignore rules. These are files that were once committed and later gitignored -- read-tree --reset -u would overwrite them, destroying local modifications (e.g., config files with secrets).
#SyncMainIndexWithWorktree
func SyncMainIndexWithWorktree(ctx context.Context, treeish string) ([]string, error)SyncMainIndexWithWorktree updates the main .git/index AND the working tree to match the given treeish. Uses --reset -u, so the working tree must be clean before calling. Needed after history rewrites (scrub) where committed blobs have changed and the working tree must reflect the new content.
Tracked+gitignored files (committed then later gitignored, e.g., config files with secrets) are protected: skip-worktree is set before read-tree so --reset -u does not overwrite them. Pre-existing skip-worktree flags are also preserved.
Returns the list of protected tracked+gitignored paths (empty if none).
ONE substitution is made on the caller's treeish, and its scope is narrow on purpose: a literal "HEAD" on an UNBORN branch becomes the empty tree, because read-tree --reset -u HEAD is fatal there and what the caller means -- put the index and the working tree in step with the committed state -- is the empty tree in a repository that has no commits. It applies to nothing else. An unresolvable treeish that is NOT literal HEAD stays a hard error, and must: substituting the empty tree for a failed resolution generally would read-tree --reset -u every tracked file out of the working tree, which is the opposite of what a caller passing a real SHA (a merge's incoming tip) asked for.
#RunPassthrough
func RunPassthrough(ctx context.Context, args ...string) errorRunPassthrough executes a git command with stdin/stdout/stderr wired to the terminal (os.Stdin, os.Stdout, os.Stderr). It prepends --no-optional-locks like Run, but does not capture output -- suitable for interactive/pager commands.
This is the route for argv the OPERATOR wrote (cherry-pick, revert), so it carries the declared operator-cwd exemption from the repository-root pin: git must resolve the operator's own pathspecs in the operator's own directory. A context-carried WithDir override still applies.
#RunPassthroughWithEnv
func RunPassthroughWithEnv(ctx context.Context, env []string, args ...string) errorRunPassthroughWithEnv is RunPassthrough with extra environment entries, which is what lets a caller point git at an index file of safegit's own choosing (GIT_INDEX_FILE) while keeping safegit's promise never to write the shared one.
GIT_INDEX_FILE is deliberately NOT among the environment entries the boundary refuses (that list is GIT_DIR, GIT_WORK_TREE, GIT_COMMON_DIR and GIT_OBJECT_DIRECTORY): naming an index file does not retarget the repository, and internal/git already reaches every temporary index this way.
Streams, directory semantics and the declared exemption are identical to RunPassthrough's -- both are the same site, and a caller that adds an environment entry must not silently get different terminal or directory behavior.
#RunPassthroughTo
func RunPassthroughTo(ctx context.Context, env []string, stdout io.Writer, args ...string) errorRunPassthroughTo is RunPassthroughWithEnv with the child's STDOUT sink named by the caller.
It exists for machine mode: under --json safegit's stdout carries exactly one document, the framework's envelope, and a passthrough child writing its own progress there would put a second document beside it. The caller passes os.Stderr instead, which is where push already routes git's stdout for the same reason. Stderr and stdin are wired to the terminal either way.
#CommonGitDirOf
func CommonGitDirOf(ctx context.Context, gitDir string) (string, error)CommonGitDirOf returns the common git directory for a given gitDir: the one every worktree of a repository shares. For a normal repository it equals gitDir; for a linked worktree it is the main .git dir. Lock files live there, so that worktrees committing to the same branch serialize correctly.
The repository is an ARGUMENT rather than the process working directory, so the call goes through RunWithGitDir -- the declared explicit-directory exemption from the repository-root pin -- which sets GIT_DIR and runs git in that directory. An absolute gitDir therefore yields an absolute answer; a relative one yields an answer relative to gitDir itself, never to this process's working directory. There is deliberately no working-directory form: one existed, had no callers, and returned an answer whose meaning depended on where the process happened to stand.
#IsIgnored
func IsIgnored(ctx context.Context, filePath string) (bool, error)IsIgnored checks whether a file matches a gitignore rule.
This is git's own question, index included: a path that is in the index is TRACKED, and check-ignore answers "not ignored" for it whatever the ignore rules say. That is the right answer for "may this path be added", which is what the callers of this function ask -- a tracked file matching an ignore pattern must stay committable. A caller asking the other question, whether the ignore rules cover a path at all, wants MatchesIgnoreRules.
#MatchesIgnoreRules
func MatchesIgnoreRules(ctx context.Context, filePath string) (bool, error)MatchesIgnoreRules reports whether the ignore rules cover a path, with the index left out of the question entirely.
--no-index is the whole difference from IsIgnored, and it inverts the answer for exactly the paths that make the question worth asking: one that is still in the index. Plain check-ignore calls such a path not ignored because it is tracked, so asking it "is this path gitignored" about a path that is about to STOP being tracked yields the opposite of the truth. With --no-index only the patterns decide.
Exit 1 with nothing on stderr is check-ignore's "no pattern matches", which is an answer; any other failure is a real one and is returned.
#IsAncestorOf
func IsAncestorOf(ctx context.Context, commitSHA, descendantSHA string) (bool, error)IsAncestorOf checks whether commitSHA is an ancestor of (or equal to) descendantSHA. Uses git merge-base --is-ancestor which exits 0 if true, 1 if false, and other codes on error.
#HaveMergeBase
func HaveMergeBase(ctx context.Context, a, b string) (have, ok bool)HaveMergeBase reports whether two commits share any merge base at all.
The three answers git's own merge-base gives are kept distinct, because only one of them means "these histories are unrelated": exit 0 with a base, exit 1 with none, and anything else -- an unresolvable argument, a broken object store -- which is not an answer to this question. ok is false for that third case, so a caller refuses on a FACT rather than on a failure that could mean anything.
#IsShallowRepository
func IsShallowRepository(ctx context.Context) boolIsShallowRepository reports whether this repository was fetched with a depth limit, so part of its history is simply absent from the object store.
It answers TRUE only on git's own "true": anything else -- "false", or a rev-parse that failed for any reason at all -- is read as not shallow, which is the safe direction for the one thing the answer is used for. It refines a refusal's WORDING, never the refusal itself, so a wrong "false" leaves the ordinary message rather than inventing a shallow one for a full clone.
#FirstParentRange
func FirstParentRange(ctx context.Context, from, to string) ([]string, error)FirstParentRange lists the commits a branch would LOSE by moving from to back to from, newest first. An empty from means the branch would lose everything reachable from to, which is what deleting the ref does.
The walk is FIRST-PARENT, and that is the whole definition rather than a detail of it. A merge commit's second parent is the side that was merged IN: those commits were never made by the branch, and moving the branch back to the merge's first parent does not undo them -- it undoes the merge. Walking every parent would report a whole merged-in branch as commits the move discards, which is a different and untrue statement.
#ConfiguredAuthor
func ConfiguredAuthor(ctx context.Context) (AuthorInfo, error)ConfiguredAuthor is the identity git itself would record as the AUTHOR of a commit created right now: git var GIT_AUTHOR_IDENT, which is git's own resolution of the environment, the repository config and the global config. Asking git is the point -- a reconstruction from config --get user.name would miss the GIT_AUTHOR_* environment and git's own fallbacks, and would therefore be able to disagree with the commit it claims to describe.
The timestamp git includes is dropped: the identity is asked for so a report can name who a commit records, and a time resolved here is not the time the commit will carry.
#ParseCommit
func ParseCommit(ctx context.Context, sha string) (CommitInfo, error)ParseCommit reads and parses a commit object by SHA using git cat-file.
#LsTreeAll
func LsTreeAll(ctx context.Context, treeish string) ([]TreeEntry, error)LsTreeAll returns all blob entries in the given treeish, recursively. Empty trees return an empty slice, not an error.
--full-tree is not optional here. Without it git resolves a tree listing against the process working directory PREFIX: from a subdirectory, ls-tree <root-tree> returns that subdirectory's entries with the prefix stripped, and a caller that rebuilds a tree from the result promotes the subdirectory to the repository root and deletes everything outside it. Pinning the working directory alone does not fix this, because the *WithDir family and any future caller can still run somewhere else; the flag makes the listing repository-rooted no matter where the process stands.
#LsTreeRecursive
func LsTreeRecursive(ctx context.Context, treeish string) ([]TreeEntry, error)LsTreeRecursive returns EVERY entry in the given treeish, recursively: blobs, symlinks and gitlinks (submodule pointers, mode 160000, object type "commit"). LsTreeAll drops everything that is not a blob, which hides exactly the entries a caller that must not cross a submodule boundary needs to see.
ls-tree -r never descends INTO a gitlink, so a submodule's own contents can never appear here -- the gitlink is reported as one entry and the recursion stops there.
#LsTreePathsRecursive
func LsTreePathsRecursive(ctx context.Context, treeish string, paths []string) ([]TreeEntry, error)LsTreePathsRecursive returns the entries a treeish holds at EXACTLY the given repo-relative paths, recursively, and nothing else. A path the tree does not carry is simply absent from the answer, which is how a caller learns the tree does not hold it.
It is the path-limited form of LsTreeRecursive, for a caller that wants a handful of named paths out of a tree rather than all of it. --full-tree makes both the listing and the pathspecs repository-rooted, so the answer does not depend on where the process stands -- the same reason it is mandatory on the two listings above.
An empty path list returns nothing: ls-tree with no pathspec lists the whole tree, which is the opposite of what a caller asking about no paths means.
Every path goes out under the :(literal) pathspec magic, which is not optional: a path is a NAME here, never a pattern. Without it a path beginning with a colon is read as pathspec magic of its own -- :weird.txt matches NOTHING and git exits 0 -- and a caller that reads an absent answer as "the tree does not carry this path" would act on a silent miss. Wildcards in a name are the same class of error in the other direction.
#DiffTree
func DiffTree(ctx context.Context, fromTreeish, toTreeish string) ([]ChangedPath, error)DiffTree lists every path that differs between two trees, recursively.
It is the one place safegit asks git what a tree comparison contains, and the answer is what the commit pipeline reports as "the files in this commit": derived from the objects, never counted from the arguments a caller typed.
GIT'S rename detection is deliberately OFF, and the qualification is the point: what this returns is the RAW delta -- a deletion and an addition, with the modes and blob names on both sides -- which is what a reviewer of the published commit sees and what safegit's own move inference reads (internal/commit/infer_moves.go). That inference pairs a deletion with an addition only where the objects leave one answer possible; a similarity score is an interpretation of CONTENT, and safegit asks git for none, here or anywhere.
An empty fromTreeish means "compare against nothing": every path in the new tree is reported as an addition. That is the root-commit case, and it is spelled this way rather than with git's empty-tree constant so the function carries no assumption about the repository's hash algorithm.
#FilterIgnored
func FilterIgnored(ctx context.Context, paths []string) (map[string]bool, error)FilterIgnored returns the subset of the given repo-relative paths that git's ignore rules exclude, as a set. Directories may be passed too: an ignored directory answers for itself, so a caller walking a tree can stop there instead of asking about every file underneath it.
One check-ignore --stdin invocation answers for the whole batch. git exits 1 when nothing in the batch is ignored, which is an answer and not a failure.
#LsTree
func LsTree(ctx context.Context, treeish string) ([]TreeEntry, error)LsTree returns all entries (blobs and subtrees) at one level of the given treeish, without recursing into subtrees. Each entry includes Mode and ObjectType so callers can distinguish blobs from trees. --full-tree is mandatory for the same reason it is on LsTreeAll: a listing resolved against the working-directory prefix is a listing of the wrong tree.
#HashObjectBytes
func HashObjectBytes(ctx context.Context, data []byte) (string, error)HashObjectBytes returns the blob SHA for in-memory bytes without writing anything to the object store -- the preview counterpart of HashObjectWriteBytes.
#HashObjectBytesAsPath
func HashObjectBytesAsPath(ctx context.Context, rel string, data []byte) (string, error)HashObjectBytesAsPath returns the blob SHA git would record for in-memory bytes if they lived at rel: the same answer as HashObjectBytes, except that git's clean filter and text attributes for rel are applied to the bytes first. It writes nothing.
This is NOT one of the path-taking helpers the comment above rules out. The content still comes from the caller, on stdin; rel is a repo-relative NAME git looks attributes up under and never opens, so nothing here depends on where a file happens to be or on which directory the child runs in. It is what makes a content comparison filter-aware: on a checkout where git converts line endings, the bytes on disk differ from the blob and the file is still clean, and a comparison that hashed them raw would call it changed.
#HashObjectWriteBytes
func HashObjectWriteBytes(ctx context.Context, data []byte) (string, error)HashObjectWriteBytes writes in-memory bytes as a blob to the object store via git hash-object -w --stdin, returning the blob SHA.
#HashObjectWriteTag
func HashObjectWriteTag(ctx context.Context, content []byte) (string, error)HashObjectWriteTag writes in-memory bytes as a TAG object to the object store, returning the tag object SHA. A rewritten annotated tag is a new tag object, so every site that reconstructs one goes through here rather than spelling the -t tag argv again.
#CatFileBlob
func CatFileBlob(ctx context.Context, sha string) ([]byte, error)CatFileBlob reads blob content by SHA via git cat-file -p.
#MkTree
func MkTree(ctx context.Context, entries []TreeEntry) (string, error)MkTree creates a tree object from a slice of TreeEntry values and returns the tree SHA. Each entry must have Mode, ObjectType, SHA, and Path populated. Input is piped to git mktree -z as "ls-tree -z produces, which is where every entry safegit writes back came from.
The -z is not an optimization. Plain mktree input treats a path that starts with a double quote as a C-quoted string, so a repository holding a file whose name begins with one -- a legal name -- makes it refuse with "invalid quoting", and a path containing a backslash would be read as an escape. Under -z every path is taken literally, so the writer round-trips exactly what the reader parsed.
#CatFileBatchAll
func CatFileBatchAll(ctx context.Context) (*ObjectIterator, error)CatFileBatchAll starts a git cat-file --batch-all-objects --batch subprocess and returns an ObjectIterator for streaming the results. The caller must call Close() when done. Respects WithDir context overrides.
#CatFileBatchSHAs
func CatFileBatchSHAs(ctx context.Context, shas []string) (*ObjectIterator, error)CatFileBatchSHAs starts a git cat-file --batch subprocess that reads only the specified SHAs, and returns an ObjectIterator for streaming the results. Unlike CatFileBatchAll (which enumerates all objects), this feeds specific SHAs via stdin using bytes.NewReader to avoid pipe deadlock: if output exceeds the OS pipe buffer (~64KB), git blocks on stdout write while the caller is still writing to stdin. With bytes.NewReader, git reads stdin from memory at its own pace. The caller must call Close() when done.
#RunWithGitDir
func RunWithGitDir(ctx context.Context, gitDir string, workTree string, args ...string) (stdout, stderr string, err error)RunWithGitDir executes a git command against a specific git directory and work tree, rather than relying on cwd-based discovery. Sets GIT_DIR, GIT_WORK_TREE, and cmd.Dir so both git and cwd-relative paths resolve against the target repo.
It is one of the declared explicit-directory exemptions from the repository-root pin: the repository is an argument, not a discovery.
#CatFileBatchAllWithDir
func CatFileBatchAllWithDir(ctx context.Context, gitDir string) (*ObjectIterator, error)CatFileBatchAllWithDir starts a git cat-file --batch-all-objects --batch subprocess targeting a specific git directory. Returns an ObjectIterator for streaming the results. The caller must call Close() when done.
#CatFileBatchSHAsWithDir
func CatFileBatchSHAsWithDir(ctx context.Context, gitDir string, shas []string) (*ObjectIterator, error)CatFileBatchSHAsWithDir starts a git cat-file --batch subprocess targeting a specific git directory, reading only the specified SHAs. Sets GIT_DIR so git resolves objects from the target repo rather than the cwd repo. The caller must call Close() when done.
#SplitNonEmpty
func SplitNonEmpty(s string) []stringSplitNonEmpty splits s by newlines and returns only non-empty lines.
#ForEachRef
func ForEachRef(ctx context.Context, format string, prefixes ...string) ([]string, error)ForEachRef runs git for-each-ref with the given format and optional ref prefixes (e.g. "refs/heads/", "refs/tags/"). Returns one line per ref.
#ReachableMessages
func ReachableMessages(ctx context.Context, rev string) ([]CommitMessage, error)ReachableMessages returns every commit reachable from rev, newest first, with its full message.
The delimiter is a NUL between commits (log -z), which is the only separator a commit message cannot contain: a message holds arbitrary text, blank lines and lines that look like whatever separator one might reach for, so any printable delimiter is a message somebody can write.
#LsRemoteBulk
func LsRemoteBulk(ctx context.Context, remote, pattern string) (map[string]string, error)LsRemoteBulk runs git ls-remote against a remote with a pattern and returns a map of refname to SHA. The output format of git ls-remote is "
#ReconcileMainIndex
func ReconcileMainIndex(ctx context.Context, beforeTip, afterTreeish string) errorReconcileMainIndex rebuilds the shared .git/index after a ref the working tree is on has moved, preserving everything the index holds that the pre-operation tip does not account for.
This is the SINGLE index-reconciliation authority: commit, amend, reword and undo all reconcile through this one function, so "what happens to the shared index when safegit moves a ref" has exactly one answer, and the continue commands that conclude an interrupted operation reconcile the same way.
beforeTip is the commit-ish the index was last reconciled against -- the ref's value BEFORE the operation. Empty means there was none (a root commit), so the whole index counts as delta. afterTreeish is the state to sync to; empty means clear the index entirely (undoing a root commit).
What survives the sync:
- foreign staged work: a stage-0 entry that differs from beforeTip (a staged modification or an addition beforeTip never had), and a path beforeTip has that the index has no slot for at all (a staged deletion, e.g. git rm --cached); - unmerged stage 1/2/3 entries, replayed intact, so a conflict another session is resolving is still a conflict afterwards; - skip-worktree flags, re-set on every flagged path still present at stage 0.
The whole delta is replayed in ONE git update-index --index-info batch. Within that batch an unmerged path is preceded by a zero-mode removal line, because the read-tree wrote a stage-0 entry for it and git refuses to hold stage 0 and a higher stage for the same path at once.
Every failure is HARD. A half-replayed index is a corrupted view of somebody else's staged work; reporting that as a warning and returning success is exactly how staged state disappears silently.
#SetIndexStage0
func SetIndexStage0(ctx context.Context, indexPath string, entries []IndexStage0) errorSetIndexStage0 applies resolutions to the index at indexPath, in one git update-index --index-info batch.
This is how a conflict is resolved in an index without going near the working tree: a conflicted path occupies stages 1, 2 and 3, and writing a stage-0 entry for it is what makes git write-tree accept it. Each path is preceded by a zero-mode removal line, because git will not hold stage 0 and a higher stage for one path at once, and because removing a path the index does not hold is a no-op -- so the same batch expresses both "resolve to this blob" and "remove this path".
indexPath names the index to write, and an empty indexPath writes the repository's shared index -- the same convention UnmergedStages reads by. The shared index has TWO writers, and both hold the worktree operation lock while they write: the conclusion's own reconciliation, through here, and safegit doctor --action fix's repair of an ORPHANED unmerged index, which re-stages the working tree's own content through the effects handle (so a preview records the invocations instead of performing them) and therefore does not come through this function. The lock is what keeps the two from interleaving.
#MergeTree
func MergeTree(ctx context.Context, base, ours, theirs string, extra ...string) (MergeTreeResult, error)MergeTree computes a three-way merge into the object store, touching neither the index nor the working tree.
It is the engine behind the honest --dry-run of merge, cherry-pick and revert: replaying the operation this way gives the REAL answer (clean or conflicted, and which paths) instead of a guess, and the objects it writes go into the preview's quarantine and away with it.
base is the merge base, and passing it is what makes a cherry-pick or a revert expressible as a merge -- the operation's whole difference from a branch merge is which commit stands as the base and which stands as the incoming side. Empty means "let git find the merge base itself", which is the branch-merge case.
extra are further merge-tree options, each its own element, inserted before the two sides. It carries the STRATEGY OPTIONS a previewed command line asked for, so the previewed tree is the one that command line really produces. Their own version floor is newer than this function's and belongs to the caller that knows whether any were asked for -- see previewRefusal.
The version floor is checked here rather than at each caller: --write-tree is git 2.38, and on an older git the flag does not exist at all, so an unchecked call would fail with git's usage text instead of a sentence naming the floor.
#StashApply
func StashApply(ctx context.Context, commit string) (output string, err error)StashApply applies a stash-shaped commit to the working tree and index, the way git stash apply <commit> does.
It returns git's own combined output, so a caller can show the operator what happened, and an error when the apply did not succeed -- most often because the stashed change conflicts with what the working tree now holds. A failed apply is NOT a no-op: git leaves the conflict in the working tree and the index, exactly as it does for a conflicting git stash apply an operator ran themselves.
#StashStore
func StashStore(ctx context.Context, commit, message string) errorStashStore records an already-existing stash-shaped commit as an entry on refs/stash, the way git stash store does, without touching the working tree or the index.
It is the recovery path for an apply that failed: the commit is real either way, but until it is on refs/stash the only name for it is a raw object name in a file that is about to be removed. Stored, it is stash@{0} and every ordinary stash command reaches it.
#RequireFeature
func RequireFeature(ctx context.Context, f gitversion.Feature) errorRequireFeature refuses when the installed git is older than the floor a declared feature carries, and returns nil otherwise.
It is the production entry point to internal/gitversion: a command that is about to run git syntax with a version floor calls this FIRST, so an operator on an older git is told which git feature safegit needs and which version introduced it -- rather than being handed git's own "unknown option" from somewhere in the middle of a conclusion.
The version is read once per process (see above), so a caller may call this on a hot path.
#ObjectIterator.Next
func (it *ObjectIterator) Next() (*ObjectEntry, error)Next reads the next non-tree object from the stream. Trees are silently skipped. Returns io.EOF when the stream ends.
#ObjectIterator.Close
func (it *ObjectIterator) Close() errorClose kills the subprocess if it is still running and waits for it to exit.