Documentation
¶
Overview ¶
Package git drives the git binary — never go-git (AGENTS.md §4.6) — so that a commit made on the user's behalf inherits the user's own signing configuration (gpg.format=ssh, user.signingkey, 1Password's gpg.ssh.program, includeIf) exactly as if the user had typed the command themselves. Every mutating call is scoped to a worktree created under $XDG_CACHE_HOME/hoist/worktrees/<id> from the user's own clone: the worktree shares the clone's .git directory (and therefore its config and its object store) but has its own HEAD, index and working files, so nothing here ever touches the user's own checked-out branch, working tree or index.
Git is the seam pkg/forge and internal/engine are tested against without a network call: ExecGit is exercised against real temporary git repositories (a bare "origin" and a clone of it), never a fake, because the properties this package exists to prove — worktree creation and reuse, signed commits, push and remote observation — are properties of the git binary's actual behavior, not of a model of it.
Index ¶
- Variables
- type Exec
- func (e Exec) Commit(ctx context.Context, worktreeDir, message string, paths []string, ...) (string, error)
- func (e Exec) CommitTime(ctx context.Context, dir, sha string) (time.Time, error)
- func (e Exec) DeleteRemoteBranch(ctx context.Context, cloneDir, remote, branch string) error
- func (e Exec) DiffNameOnly(ctx context.Context, worktreeDir, fromRev, toRev string) ([]string, error)
- func (e Exec) FetchBranch(ctx context.Context, dir, remote, branch string) (string, bool, error)
- func (e Exec) HashObject(ctx context.Context, worktreeDir string, content []byte) (string, error)
- func (e Exec) IsAncestor(ctx context.Context, dir, ancestor, descendant string) (bool, error)
- func (e Exec) IsShallow(ctx context.Context, dir string) (bool, error)
- func (e Exec) Log(ctx context.Context, worktreeDir, revRange string) ([]string, error)
- func (e Exec) LsRemoteBranch(ctx context.Context, cloneDir, remote, branch string) (string, bool, error)
- func (e Exec) LsTreeBlob(ctx context.Context, worktreeDir, rev, path string) (string, bool, error)
- func (e Exec) ObjectExists(ctx context.Context, dir, sha string) (bool, error)
- func (e Exec) Push(ctx context.Context, worktreeDir, remote, branch string) error
- func (e Exec) PushHeadTo(ctx context.Context, worktreeDir, remote, remoteBranch string) error
- func (e Exec) RemoveWorktree(ctx context.Context, cloneDir, worktreeDir string) error
- func (e Exec) RevParse(ctx context.Context, worktreeDir, rev string) (string, bool, error)
- func (e Exec) Worktree(ctx context.Context, cloneDir, worktreeDir, branch, base string) error
- func (e Exec) WorktreeAtRef(ctx context.Context, cloneDir, dir, ref string) error
- func (e Exec) WorktreeBranch(ctx context.Context, cloneDir, worktreeDir string) (string, bool, error)
- type Git
Constants ¶
This section is empty.
Variables ¶
var ErrTimeout = errors.New("git: timed out")
ErrTimeout marks a Commit that did not return within its timeout — most often the interactive 1Password SSH-sign approval never arriving. It is retryable: the caller should leave the worktree alone (no half-applied commit is possible; see Commit's doc comment) and try again, rather than treating this as a crash.
Functions ¶
This section is empty.
Types ¶
type Exec ¶
type Exec struct {
// Bin overrides the git binary; "" means "git" from PATH. Tests never need this — HOME
// and GIT_CONFIG_GLOBAL isolate the git config instead — but it exists for a caller that
// wants an explicit path.
Bin string
}
Exec is the real Git, implemented by shelling out to the git binary found on PATH.
func (Exec) Commit ¶
func (e Exec) Commit(ctx context.Context, worktreeDir, message string, paths []string, timeout time.Duration, onWaiting func()) (string, error)
Commit implements Git.
func (Exec) CommitTime ¶
CommitTime implements Git: sha's committer date (`%cI`, strict ISO 8601 — always includes a UTC offset, so time.Parse(time.RFC3339, ...) is exact, never a local-timezone guess).
func (Exec) DeleteRemoteBranch ¶
DeleteRemoteBranch implements Git. It runs against cloneDir (not a worktree — this promotion may have already removed its own worktree by the time cleanup runs; cloneDir always exists and shares the same remotes). "Already gone" is treated as success, matching RemoveWorktree's own idempotency rule — decided by an authoritative LsRemoteBranch re-check against the remote after the delete errors, never by matching text in git's stderr (the same reasoning FetchBranch's own doc comment gives: git's wording varies across versions and locales, and string-matching it can silently misclassify a real failure as "already gone", or the reverse).
func (Exec) DiffNameOnly ¶
func (e Exec) DiffNameOnly(ctx context.Context, worktreeDir, fromRev, toRev string) ([]string, error)
DiffNameOnly implements Git.
func (Exec) FetchBranch ¶
FetchBranch implements Git: fetches remote's branch and resolves FETCH_HEAD to the sha it just fetched. dir's own checked-out branch, working tree and index are never touched — that is the property callers actually rely on — but this is an ordinary `git fetch <remote> <branch>`, and git's own default fetch refspec (`+refs/heads/*:refs/remotes/<remote>/*`) opportunistically updates dir's remote-tracking ref for branch as a side effect, the same as it would for any other fetch; that update is harmless here since nothing in this package ever trusts a remote-tracking ref as evidence without fetching it fresh first, but it is real and should not be described as "no local ref changes at all". "branch does not exist on remote at all" is reported as ok=false, not an error — the same "not found, not broken" shape LsRemoteBranch and RevParse use. That classification is decided by an authoritative LsRemoteBranch re-check against the remote, never by matching text in git's stderr: git's wording for "no such ref" varies across versions and locales, and pattern-matching it can silently misclassify a real failure as "branch missing" (or the reverse).
func (Exec) HashObject ¶
HashObject implements Git.
func (Exec) IsAncestor ¶
IsAncestor implements Git: `git merge-base --is-ancestor`, which exits 0 when ancestor is an ancestor of (or identical to) descendant, 1 when it is not, and anything else (most commonly 128, "not a valid object name") when either rev could not even be resolved — that last case is reported as a real error, never silently folded into "not an ancestor", since a caller that asked about the wrong sha deserves to know rather than be told a false negative.
func (Exec) IsShallow ¶
IsShallow implements Git: `git rev-parse --is-shallow-repository` prints true or false and exits 0 either way, so any non-zero exit is a real error.
func (Exec) LsRemoteBranch ¶
func (e Exec) LsRemoteBranch(ctx context.Context, cloneDir, remote, branch string) (string, bool, error)
LsRemoteBranch implements Git.
func (Exec) LsTreeBlob ¶
LsTreeBlob implements Git.
func (Exec) ObjectExists ¶
ObjectExists implements Git: `git cat-file -e`, which exits 0 when sha names an object actually present in dir's object database and 1 when it does not (never treated as a failure) — anything else is a genuine error worth surfacing.
func (Exec) PushHeadTo ¶
PushHeadTo implements Git.
func (Exec) RemoveWorktree ¶
RemoveWorktree implements Git. Removing an absent worktree is treated as success: git itself errors on a path it doesn't recognise, which here just means there is nothing to clean up.
func (Exec) WorktreeAtRef ¶
WorktreeAtRef implements Git.
type Git ¶
type Git interface {
// Worktree creates a linked worktree of cloneDir at worktreeDir, checked out on branch,
// creating branch from base if it does not already exist locally. It is idempotent: a
// worktreeDir already registered on branch is left exactly as it is (never reset, so a
// commit made by an earlier, killed run of the same promotion is never discarded); a
// worktreeDir that exists on disk but is not a registered worktree (a stale directory
// left by a run killed before `git worktree add` finished its own bookkeeping) is
// cleaned up and recreated, never silently rm -rf'd without that check.
Worktree(ctx context.Context, cloneDir, worktreeDir, branch, base string) error
// RemoveWorktree removes the linked worktree at worktreeDir. Callers must not call this
// while any process still has worktreeDir as its current directory (AGENTS.md known bug
// class: deleting a worktree out from under a spawned git process races and can fail in
// platform-specific ways) — remove only after every child using it has exited. Removing
// an already-absent worktree is not an error.
RemoveWorktree(ctx context.Context, cloneDir, worktreeDir string) error
// LsRemoteBranch reports origin's current tip of branch, ok=false when the ref does not
// exist there. This is the one source of truth Observe uses for "has this been pushed"
// — never a locally cached belief.
LsRemoteBranch(ctx context.Context, cloneDir, remote, branch string) (sha string, ok bool, err error)
// FetchBranch fetches remote's current tip of branch into dir's local object database. It
// never touches dir's own checked-out branch, working tree or index (never a checkout,
// never a fast-forward of a local branch) — that is the guarantee callers actually depend
// on. It does update dir's remote-tracking ref for branch (refs/remotes/<remote>/<branch>),
// exactly as a plain `git fetch <remote> <branch>` always does; that is ordinary fetch
// behavior, not something this method avoids, and is not itself a problem for anything in
// this repo, which never trusts a remote-tracking ref as evidence without re-fetching it
// first. Reports the sha it fetched; ok=false when the ref does not exist on remote. Added
// in M4 for MergedStep's Observe: LsRemoteBranch alone reports a live sha over the network,
// but a subsequent LsTreeBlob/IsAncestor needs the sha's commit and tree objects actually
// present locally, which only a fetch (not ls-remote) provides — this repo otherwise never
// fetches at all (the worktree is branched from the clone's own local ref, on the stated
// assumption the clone already matches Base for the files a promotion touches; see doc.go).
// Verifying the base branch's *current* state against a historical merge record is the one
// place that assumption isn't good enough, because the whole point is to catch the base
// having moved on origin without the clone's local ref ever being told.
FetchBranch(ctx context.Context, dir, remote, branch string) (sha string, ok bool, err error)
// LsTreeBlob reports the blob hash of path in rev's tree, ok=false when rev cannot be
// resolved (nothing committed yet) or path is not present in it.
LsTreeBlob(ctx context.Context, worktreeDir, rev, path string) (blob string, ok bool, err error)
// IsAncestor reports whether ancestor is an ancestor of (or identical to) descendant in
// dir's object graph — `git merge-base --is-ancestor`, which git itself defines as true for
// a commit and itself, not just for a strict ancestor. Both revs must already be resolvable
// locally (a caller verifying a remote branch's tip should FetchBranch it first, the same
// way LsTreeBlob callers do). Added in M4 for MergedStep's Observe (finding #2, round 3):
// whether a promotion's own merge commit is still reachable from the base's current tip is
// the correct test for "did something revert past this merge", not a content/blob
// comparison — a later, legitimate commit that changes the very same paths (an ordinary
// re-promotion to the same env) moves the tip forward without ever making the earlier merge
// unreachable, while a real revert (reset, force-push, or a base rebuilt from an earlier
// point) does. An error here means ancestor or descendant could not be resolved at all (e.g.
// a truly unknown sha), not "not an ancestor" — that is ok=false, err=nil.
IsAncestor(ctx context.Context, dir, ancestor, descendant string) (isAncestor bool, err error)
// ObjectExists reports whether sha names an object this clone's own object database
// actually has (`git cat-file -e`), never a syntactic check. RevParse below cannot answer
// this: `git rev-parse --verify --quiet` on a well-formed 40-hex-character string returns it
// unchanged and "verified" even when no such object exists — it only validates that the
// string parses as a revision, not that the object is present. A caller checking whether a
// specific commit sha is genuinely resolvable in this clone (M4 hardening, MergedStep's
// mergeWasReverted) needs this method, not RevParse.
ObjectExists(ctx context.Context, dir, sha string) (bool, error)
// IsShallow reports whether dir is a shallow clone (`git rev-parse --is-shallow-repository`).
// A shallow clone's object database is cut off at its depth, so ObjectExists and IsAncestor
// can answer "no" for a commit that is perfectly real on origin; a caller about to read
// either answer as a revert needs to know the clone cannot be trusted to hold history
// (MergedStep's mergeWasReverted, issue #46).
IsShallow(ctx context.Context, dir string) (bool, error)
// RevParse resolves rev to a commit sha inside worktreeDir, ok=false when rev cannot be
// resolved (e.g. HEAD before any commit exists). Added beyond the brief's listed shape:
// Observe needs to read the worktree's current HEAD without creating a commit to find
// out, and ls-tree alone reports a blob's content, never the commit id that holds it.
RevParse(ctx context.Context, worktreeDir, rev string) (sha string, ok bool, err error)
// Commit stages exactly paths (relative to worktreeDir) and commits message, running
// through the worktree's inherited signing configuration. It runs under timeout; if the
// commit has not returned after 5s, onWaiting is called exactly once (this is the
// interactive 1Password SSH-sign approval, not a hang the caller should give up on). A
// timeout is reported as an error satisfying errors.Is(err, ErrTimeout) — a retryable
// failure, never a crash — and never leaves a stale index.lock behind for the next
// attempt to trip over.
Commit(ctx context.Context, worktreeDir, message string, paths []string, timeout time.Duration, onWaiting func()) (sha string, err error)
// Push pushes worktreeDir's branch to remote/branch. A rejected non-fast-forward push
// (something else moved the branch) is reported as an error distinguishable by the
// caller re-querying LsRemoteBranch — Push itself never retries with --force.
Push(ctx context.Context, worktreeDir, remote, branch string) error
// DeleteRemoteBranch deletes branch on remote (M4: MergedStep's cleanup after a successful
// merge). Deleting a branch that is already gone is not an error — idempotent the same way
// RemoveWorktree is, so a resumed MergedStep that already deleted the branch on an earlier,
// killed run can call this again safely. Added beyond the brief's listed shape (§4.7: a
// small, justified addition) rather than a new pkg/forge method, since branch deletion is
// exactly the same remote-ref operation Push already performs through the user's own git
// remote and SSH config — routing it through pkg/forge instead would need a GitHub API
// scope this repo does not otherwise require merging to need.
DeleteRemoteBranch(ctx context.Context, cloneDir, remote, branch string) error
// PushHeadTo pushes worktreeDir's current HEAD directly onto remote's remoteBranch, even
// when remoteBranch is not the name of any branch checked out in worktreeDir. Direct mode
// (AGENTS.md M6) uses this to land a promotion's own commit straight on a target env's
// base branch (e.g. "main") without ever checking that name out in a second worktree:
// git refuses to check out a branch that is already checked out elsewhere, and the base
// branch is, by far the common case, exactly what the user's own clone already has
// checked out in its primary worktree. Pushing HEAD (a commit, not a branch ref) as the
// source side of the refspec sidesteps that entirely — the promotion's own worktree stays
// on its own distinct local branch (BranchedStep's normal hoist/<env>/<id> name), and only
// the remote ref named remoteBranch ever moves. Like Push, a rejected non-fast-forward
// push is reported as an error and never retried with --force.
PushHeadTo(ctx context.Context, worktreeDir, remote, remoteBranch string) error
// HashObject returns the git blob hash content would have, without requiring it to be
// committed or even written to the object database. Used to precompute ExpectedBlobs
// from a planned edit's "after" bytes before any commit exists.
HashObject(ctx context.Context, worktreeDir string, content []byte) (blob string, err error)
// DiffNameOnly lists the repo-relative paths that differ between fromRev and toRev,
// exactly as `git diff --name-only` reports them. Used to confirm a commit changed
// exactly the planned paths and nothing more (AGENTS.md §4.2: "a one-line-per-occurrence
// diff is the whole review surface for a production change") — a pre-commit hook or a
// pre-existing staged change riding along in the same commit must be caught here, not
// discovered only once the branch is pushed.
DiffNameOnly(ctx context.Context, worktreeDir, fromRev, toRev string) (paths []string, err error)
// WorktreeBranch reports whether worktreeDir is registered as a linked worktree of
// cloneDir, and if so, which branch it is checked out on. ok=false means it is not
// currently registered — absent, or a stale directory git's own registry does not know
// about — which callers must treat identically to "not set up yet". This is the only
// trustworthy way to answer "is this worktree actually mine, on the right branch": the
// mere presence of a ".git" file at worktreeDir proves nothing about which clone or
// branch it resolves to (a stale pointer from an unrelated prior state, or one that
// happens to resolve into cloneDir's own real git dir but on a different branch).
WorktreeBranch(ctx context.Context, cloneDir, worktreeDir string) (branch string, ok bool, err error)
// Log lists commit SHAs in revRange (e.g. "main..hoist/app-production/abc123"), in the
// order `git log --format=%H` reports them (most recent first). Not used by any
// production step — every step's own idempotency is proven structurally, by Observe
// re-deriving truth, never by counting — but exposed for tests that need to assert a
// promotion produced exactly one commit: a ref only ever has one tip, so LsRemoteBranch
// alone cannot distinguish one commit behind it from several.
Log(ctx context.Context, worktreeDir, revRange string) (shas []string, err error)
// CommitTime reports sha's real committer date, read from local git object data in dir
// (either cloneDir or a worktree sharing its object store — the object need only be
// reachable, not checked out). Added in M4 so ApprovedStep can anchor "was this comment
// posted after the code it approves" on the commit's own timestamp rather than the PR's
// CreatedAt: PR.CreatedAt is only a safe stand-in for that while other steps' checks
// happen to make an earlier PR on stale content unreachable (see steps_m4.go's doc
// comment); CommitTime lets Approved state that fact directly instead of leaning on it.
CommitTime(ctx context.Context, dir, sha string) (time.Time, error)
// WorktreeAtRef creates a throwaway, detached linked worktree of cloneDir at dir, checked
// out at ref exactly as it resolves right now — no branch is created, reused or advanced,
// unlike Worktree. The caller owns dir and must call RemoveWorktree once done reading from
// it; dir is meant to be read from and discarded within one invocation, never reused across
// a restart. Added for direct mode's own discovery/planning pass (cmd/hoist/promote.go): it
// reads a fresh, uncommitted snapshot of origin/<base>'s current tree — never cloneDir's own
// checked-out branch or working files (AGENTS.md §4.6) — so direct mode never has to trust
// cloneDir's local disk being current with origin/<base> the way discovery historically has.
WorktreeAtRef(ctx context.Context, cloneDir, dir, ref string) error
}
Git is the seam between internal/engine and the git binary. Every method is scoped to either cloneDir (the user's own clone — read and worktree-administration operations only, never a write to its checked-out branch or index) or worktreeDir (a linked worktree of cloneDir, where all file writes and commits happen).