Documentation
¶
Index ¶
- Constants
- Variables
- func AllocateAdminDirName(repoPath, name string) (string, error)
- func BehindOriginMain(worktreePath, mainBranch string) (int, error)
- func CanonicalizeWorktreePath(path string) string
- func CheckoutBranch(repoPath, branchName string) error
- func CleanupWorktrees() error
- func DiffHashBetween(repoPath, baseSHA, headSHA string) (string, error)
- func EnsureBranchSyncedWithMain(worktreePath, branchName, mainBranch string, driftThreshold int) (ok bool, blockedSummary string)
- func FetchBranch(repoPath, branchName string) error
- func GetCurrentBranchName(path string) (string, error)
- func GetHeadCommitSHA(path string) (string, error)
- func GroupChangesByPath(baseToOurs, baseToTheirs object.Changes) (map[string]*PathChange, error)
- func InitializeProjectDirectory(path string) error
- func IsCommitOnMain(repoPath, mainBranch, sha string) (bool, error)
- func IsGitRepo(path string) bool
- func IsUnbornRepo(repoPath string) bool
- func MergeBaseResolver(ours, theirs *object.Commit) (*object.Commit, error)
- func NewConflictEntries(name string, baseHash, oursHash, theirsHash plumbing.Hash, ...) []*index.Entry
- func OpenRepo(path string) (*git.Repository, error)
- func PreviewWorktreePath(repoPath, sessionName string) (string, error)
- func RemoteURL(repoPath, remote string) (string, error)
- func ResolveDefaultBranchSHA(repoPath string) (branch, sha string, err error)
- func ResolveDefaultLocalBranchSHA(repoPath string) (branch, sha string, err error)
- func ResolveExplicitBranchSHA(repoPath, branchName string) (string, error)
- func ResolveLocalBranchSHA(repoPath, branchName string) (string, error)
- func ResolveOriginBranchSHA(repoPath, mainBranch string) (string, error)
- func ResolveWorktreeBaseCommit(repoPath string) (defaultBranch, baseSHA string, err error)
- func SanitizeBranchName(s string) string
- func TreeDiffPair(base, ours, theirs *object.Tree) (baseToOurs, baseToTheirs object.Changes, err error)
- func UntrackScaffolding(worktreePath string, patterns []string) ([]string, error)
- func ValidateBranchName(name string) error
- func WithRepoWorktreeLock(repoPath string, fn func() error) error
- type AdminFileWriter
- type AggregateDiffStat
- type BranchName
- type BranchStatus
- type ConflictMarkerStyle
- type DiffStats
- type FileConflictReason
- type FileMergeInput
- type FileMergeOutcome
- type FileStat
- type GitWorktree
- func NewGitWorktree(repoPath string, sessionName string, opts ...GitWorktreeOption) (tree *GitWorktree, branchname string, err error)
- func NewGitWorktreeFromCommitSHA(repoPath, sessionName, branchName, commitSHA string, opts ...GitWorktreeOption) (*GitWorktree, string, error)
- func NewGitWorktreeFromExisting(existingWorktreePath string, sessionName string, opts ...GitWorktreeOption) (*GitWorktree, error)
- func NewGitWorktreeFromExistingWithExecutor(existingWorktreePath string, sessionName string, opts ...GitWorktreeOption) (*GitWorktree, error)
- func NewGitWorktreeFromStorage(repoPath string, worktreePath string, sessionName string, branchName string, ...) *GitWorktree
- func NewGitWorktreeFromStorageWithExecutor(repoPath string, worktreePath string, sessionName string, branchName string, ...) *GitWorktree
- func NewGitWorktreeWithBranch(repoPath string, sessionName string, customBranch string, ...) (tree *GitWorktree, branchname string, err error)
- func NewGitWorktreeWithBranchAndExecutor(repoPath string, sessionName string, customBranch string, ...) (tree *GitWorktree, branchname string, err error)
- func (g *GitWorktree) Cleanup() error
- func (g *GitWorktree) ClosePR(prNumber int, comment string) error
- func (g *GitWorktree) CommitChanges(commitMessage string) error
- func (g *GitWorktree) CreatePR(opts PRCreateOptions) (prURL string, prNumber int, err error)
- func (g *GitWorktree) Diff() *DiffStats
- func (g *GitWorktree) EnablePRAutoMerge(prNumber int) error
- func (g *GitWorktree) GetBaseCommitSHA() string
- func (g *GitWorktree) GetBranchName() string
- func (g *GitWorktree) GetPRStatus(prNumber int) (*PRStatus, error)
- func (g *GitWorktree) GetRepoName() string
- func (g *GitWorktree) GetRepoPath() string
- func (g *GitWorktree) GetWorktreePath() string
- func (g *GitWorktree) HasCommitsAheadOfMain(mainBranch string) (bool, error)
- func (g *GitWorktree) HasStagedChanges() (bool, error)
- func (g *GitWorktree) InvalidateDirtyCache()
- func (g *GitWorktree) IsBranchCheckedOut() (bool, error)
- func (g *GitWorktree) IsDirty() (bool, error)
- func (g *GitWorktree) IsDirtyWithHint(claudeActive bool) (bool, error)
- func (g *GitWorktree) IsPRMerged(prNumber int) (bool, error)
- func (g *GitWorktree) OpenBranchURL() error
- func (g *GitWorktree) PrimeDirtyCacheAt(t time.Time)
- func (g *GitWorktree) Prune() error
- func (g *GitWorktree) PushBranch() error
- func (g *GitWorktree) PushChanges(commitMessage string, open bool) error
- func (g *GitWorktree) Remove() error
- func (g *GitWorktree) RenameBranch(newBranchName string) error
- func (g *GitWorktree) RequestCopilotReview(prNumber int) error
- func (g *GitWorktree) Setup() error
- func (g *GitWorktree) SetupLocked() error
- func (g *GitWorktree) StageAllExceptScaffolding() error
- type GitWorktreeOption
- type MergeHunk
- type MergeMainResult
- type MergeRegionKind
- type MergeResult
- type NativeWorktreeEntry
- type PRCreateOptions
- type PRStatus
- type PathChange
- type PreMergeIndexSnapshot
- type RemoteWorktree
- type RemoteWorktreeOps
- type ShippedCommit
- type ThreeWayFileMerger
- type WorktreePath
Constants ¶
const DefaultBranchDriftThreshold = 50
DefaultBranchDriftThreshold is how many commits a work session's branch can fall behind main before EnsureBranchSyncedWithMain treats it as drifted and attempts a proactive resync before review. See BUG-044: a branch left to drift unbounded across a multi-day item lifecycle eventually produces a review/PR diff dominated by unrelated upstream commits rather than the item's own work, which review then (correctly, given what it's shown) reports as unrelated — misdiagnosing branch staleness as bad work. 50 commits is comfortably past "a few days of normal repo activity" while still catching drift well before it reaches the hundreds-of-commits scale that made backlog item 693c2700's diff unreviewable (289 commits behind).
const IsDirtyCacheTTL = 30 * time.Second
IsDirtyCacheTTL is the duration for which a dirty (has changes) result is considered fresh. 30s keeps the review queue responsive when uncommitted changes are present. InvalidateDirtyCache() is called after commits/pushes so critical paths remain snappy.
const IsDirtyCleanCacheTTL = 5 * time.Minute
IsDirtyCleanCacheTTL is the TTL when the worktree is known to be clean. Clean worktrees won't change unless Claude commits or a user modifies files; InvalidateDirtyCache() is called on those code paths, so 5 min is safe and cuts subprocess calls by ~10x vs dirty-path TTL for quiescent sessions.
const IsDirtyErrorCacheTTL = 60 * time.Second
IsDirtyErrorCacheTTL is the TTL applied when `git status` itself fails (e.g. the worktree directory is missing — a stale path left behind by a rework/reopen cycle). Without a backoff, a broken worktree gets re-checked on every poller tick (every few seconds), burning a subprocess spawn per tick indefinitely; 60s keeps failure visible in logs at a sane rate while still recovering quickly once the worktree is fixed.
const SteeringBranchDriftThreshold = 20
SteeringBranchDriftThreshold is the lower drift threshold used by the PostToolUse steering hook (server/services/hook_receiver_drift.go) that fires after every git commit/push inside an autonomous backlog work session. Deliberately smaller than DefaultBranchDriftThreshold: that threshold governs when review itself blocks and is calibrated to "comfortably past normal multi-day activity"; this one governs when the *agent still doing the work* gets nudged, so it can self-correct well before drift ever reaches review-blocking scale. 20 is roughly a day or so of normal upstream activity — enough headroom that this doesn't fire on ordinary same-session commits, while leaving a wide margin before DefaultBranchDriftThreshold (50) so the agent has room to act on the nudge before review would block. See BUG-044.
Variables ¶
var CandidateDefaultBranches = []string{"main", "master", "develop", "trunk"} //nolint:gochecknoglobals
CandidateDefaultBranches are tried, in order, by ResolveDefaultBranchSHA when the caller doesn't know the repo's actual default branch name. Mirrors the candidate list session/unfinished's GoGitVCSReader.ResolveDefaultBranch already uses. "main" stays first so the common case costs exactly one fetch. Exported so other packages needing the same candidate list (session.RecoverBaseCommitSHA, GitWorktree.initBaseCommitSHA) share one definition instead of re-declaring the literal.
var ErrRemoteBasePathMissing = errors.New("remote worktree base path does not exist")
ErrRemoteBasePathMissing is returned by RemoteWorktreeOps.CreateWorktree when the remote directory a new worktree would be created under does not exist. Distinct from a connection/transport failure (an unreachable host, a dead SSH channel) so callers can tell "host is fine, path is wrong" apart from "host is unreachable" — see Task 2.2.1a and adversarial-review.md Blocker 3's TOCTOU note (this check is advisory only; the real error surfaces from `git worktree add` itself if the directory is removed/unmounted between this check and the add).
var ScaffoldingExcludePatterns = []string{
".backlog-context.md",
".claude/commands/backlog/",
"web-app/.next/",
}
ScaffoldingExcludePatterns are the git exclude patterns for files stapler-squad writes into worktrees (backlog automation scaffolding, build output) that must never be committed to the target repo, even if they were already tracked in that branch's history. This is the single source of truth: consumed by the worktree-write side (session.addWorktreeExcludes, session.selfHealWorktreeScaffolding), the commit/push staging guard (StageAllExceptScaffolding below), and QuickCommitPush (server/services/unfinished_work_service.go). Keep in sync with .gitignore — session/git/scaffolding_test.go asserts they match.
Functions ¶
func AllocateAdminDirName ¶ added in v1.53.0
AllocateAdminDirName picks and creates a `.git/worktrees/<name>/` directory (WorktreeAdminDir, plan.md's Domain Glossary) under repoPath, using the same allocation strategy real git's own add_worktree uses: a direct os.Mkdir attempt, and on EEXIST a numeric suffix appended directly onto name with no separator — "<name>1", "<name>2", ... incrementing on each further collision — until an attempt succeeds. Confirmed against builtin/worktree.c's add_worktree (`while (mkdir(...)) { counter++; strbuf_addf(&sb_repo, "%d", counter); }`) at git/git@9321f5936a11d43a666244b4a1737f231469ef7b: https://github.com/git/git/blob/9321f5936a11d43a666244b4a1737f231469ef7b/builtin/worktree.c#L507-L514
This is deliberately NOT the Lstat-then-MkdirAll approach go-git v6-alpha's own worktree Add uses: that is a confirmed TOCTOU race, since two concurrent callers can both pass the Lstat existence check before either calls Mkdir. A direct os.Mkdir is atomic at the filesystem level, so two callers racing the same name can never both succeed for the same path (plan.md's Pattern Decisions table).
func BehindOriginMain ¶ added in v1.41.0
BehindOriginMain fetches mainBranch from origin into worktreePath and reports how many commits origin/mainBranch has that worktreePath's currently checked-out HEAD does not — i.e. how far the checked-out branch has drifted behind main. Unlike BranchAheadBehind (which reads repoPath's LOCAL mainBranch ref and does no fetch of its own — fine for a UI badge computed against a repo some other process keeps fresh, but silently stale otherwise), this always fetches first, so the count can never be stale the way a check against an unfetched local branch ref would be. MergeMainIntoWorktree already fetches and merges against this exact origin/mainBranch ref, so this reuses the same reference point rather than introducing a second, possibly inconsistent notion of "how far behind" (BUG-044).
func CanonicalizeWorktreePath ¶ added in v1.45.1
CanonicalizeWorktreePath resolves path to its symlink-free (realpath'd) form, matching what `git worktree list --porcelain` reports and what getWorktreeDirectory already produces for freshly-created worktree parents. On macOS /var (and /tmp) is itself a symlink to /private/var, so two code paths that construct the "same" worktree path differently — one via filepath.Join on an unresolved parent, the other by reading git's already-resolved output — end up as different strings for the identical directory (see TestBacklogFullLifecycle_SDDTriageWorktreeIsReusedBySpawnedWorkSession). EvalSymlinks requires the path to exist, which doesn't hold for the pre-creation/rehydration cases this is also used in; falling back to filepath.Clean on ANY error (not just ENOENT) keeps this a pure, non-failing normalizer, matching the established pattern in session/history_detector.go, session/import_correlate.go, and session/unfinished/gogitstore/open.go.
func CheckoutBranch ¶ added in v1.37.0
CheckoutBranch checks out a branch in an existing repository. Uses go-git (no subprocess — the `prefer-go-git-over-subshells` skill); ValidateBranchName is still the defense against a flag-like branchName (unlike FetchBranch's "--" guard, go-git's Checkout takes a typed plumbing.ReferenceName, not a raw CLI argument, so there's no equivalent injection surface here to guard beyond that).
func CleanupWorktrees ¶
func CleanupWorktrees() error
CleanupWorktrees removes all worktree directories under the configured worktrees dir. It deliberately does NOT delete the associated branches: a branch can hold commits that exist nowhere else (never pushed, never merged), and this function has no way to know whether that's true for any given one. See GitWorktree.Cleanup's doc comment — same fix, same root cause (docs/tasks/backlog-feature-improvement.md).
Task 2.2.2c disposition: a real, non-test caller exists (main.go's `reset` command), contradicting architecture.md §1a's "none found" — recorded here per the Unresolved Questions entry. Its per-directory removal below is already a direct os.RemoveAll (no subprocess to seam); only its trailing `git worktree prune` step now dispatches natively (Epic 2.4, closing the gap this comment used to describe) via cleanupWorktreesPrune.
func DiffHashBetween ¶ added in v1.42.0
DiffHashBetween returns a stable content hash of the diff between baseSHA and headSHA in the repo at repoPath — the actual added/removed line content per file, not just per-file counts (a per-file path+status+addition-count+deletion-count tuple can collide across two genuinely different edits, e.g. two different single-line replacements on the same file both show as one addition and one deletion; hashing the actual line text avoids that false-collision shape). Feeds session.IsFlakyVerdictFlipFlop's DiffHash comparison: the same code reviewed twice must hash identically; any real change to the diff must not. Unlike a hash of the (possibly token-capped) prompt text sent to a reviewer, this is computed from the full, untruncated diff, so two different diffs that happen to share a truncated prefix can never collide here.
func EnsureBranchSyncedWithMain ¶ added in v1.41.0
func EnsureBranchSyncedWithMain(worktreePath, branchName, mainBranch string, driftThreshold int) (ok bool, blockedSummary string)
EnsureBranchSyncedWithMain checks how far worktreePath's checked-out branch has drifted behind origin/mainBranch (via BehindOriginMain, always freshly fetched) and, once past driftThreshold commits, proactively merges main in and pushes the result — the same sync backlog_service_triage.go's syncPRBranchWithMain performs reactively after a PR-fix cycle, offered here as a precondition any review caller can run before trusting a diff computed against this worktree (BUG-044 suggested fix direction #1: "make the main-sync a precondition of review, not a best-effort side effect of the fix-retry path").
ok=true means the branch is fine to review as-is — either it wasn't drifted past driftThreshold, or a sync just resolved it cleanly (merged and pushed, or found already up to date). ok=false means the caller must not proceed to review yet; blockedSummary explains why in language written for both a human operator and a follow-up fix session's prompt context, naming the exact conflicted files (or push failure) rather than leaving the cause to be inferred from a confusing diff later.
Fails OPEN on any error determining or acting on drift (bad repo, fetch failure, merge error unrelated to a real conflict): returns ok=true so a broken detector never itself blocks review — matches every other best-effort git check in this codebase (see syncPRBranchWithMain's "never blocks the spawn" doc comment for the identical rationale). branchName is used only to build the operator-facing/fix-session message; the merge and push themselves act on whatever is currently checked out at worktreePath.
func FetchBranch ¶ added in v1.37.0
FetchBranch fetches a specific branch from the origin remote.
func GetCurrentBranchName ¶ added in v1.35.0
GetCurrentBranchName returns the current branch name for a git repository or worktree. Returns an error if the repo is in detached HEAD state.
func GetHeadCommitSHA ¶ added in v1.37.0
GetHeadCommitSHA returns the SHA of the HEAD commit for a git repository or worktree.
func GroupChangesByPath ¶ added in v1.53.0
func GroupChangesByPath(baseToOurs, baseToTheirs object.Changes) (map[string]*PathChange, error)
GroupChangesByPath indexes TreeDiffPair's two change-sets by the path each Change touches, so per-path reconciliation (ReconcilePathChange) can see what each side independently did — deliberately NOT correlating a Delete on one side with an Insert elsewhere as a rename beyond whatever go-git's own default rename detection already folded into a single Modify (see the "Rename+modify merge collision" Pattern Decision).
func InitializeProjectDirectory ¶ added in v1.35.0
InitializeProjectDirectory creates a directory and initializes it as a git repository. Behavior by pre-existing state:
- Path does not exist: creates with os.MkdirAll(path, 0755), runs git init, commits.
- Path exists, no .git: runs git init in place, commits.
- Path exists, already a git repo: no-op, returns nil.
- Path exists but is a regular file: returns an error.
On partial failure (dir created, git init failed): attempts os.RemoveAll to roll back the newly created directory. Logs a warning if rollback also fails.
func IsCommitOnMain ¶ added in v1.39.0
IsCommitOnMain reports whether sha has actually landed on mainBranch — either the local branch (a commit merged directly to main without ever going through a PR) or origin's copy (a PR merged remotely on GitHub that hasn't been pulled locally yet). Approval (a passing review verdict) and shipping are different questions; this answers only the second one, and does so by checking ancestry rather than trusting any cached "PR merged" flag, since that flag can be stale, absent (no PR was ever opened), or simply wrong for a manually-merged branch.
Uses go-git rather than shelling out (repo convention — see the `prefer-go-git-over-subshells` skill). The origin fetch is best-effort: a failure (offline, no such remote, nothing new) does not fail the whole check, since the local-main check alone still answers the "merged directly to main locally" case.
func IsUnbornRepo ¶ added in v1.48.0
IsUnbornRepo reports whether repoPath is a git repository with zero commits (HEAD points at a branch ref that doesn't exist yet, e.g. right after `git init`). Distinct from "no candidate default branch found": that can also mean a repo with real commit history whose default branch just isn't named main/master/develop/trunk, which is not safe to fall back to ambient HEAD for (see CreateBacklogWorktree). An unborn repo has no commit history at all, so there is nothing to misattribute either way.
func MergeBaseResolver ¶ added in v1.53.0
MergeBaseResolver returns the merge-base commit for ours and theirs — the common ancestor the native three-way merge pipeline (Epic 3.1) diffs both sides against.
(*object.Commit).MergeBase can return more than one candidate for a criss-cross merge history (independent, mutually-unreachable merge bases). Per this project's explicit scope cut (octopus/criss-cross merge resolution is out of scope — see requirements.md), that ambiguity is not treated as an error: the first candidate is used deterministically, matching git's own tie-breaking behavior for the common (non-octopus) merge case, and every candidate is named in a WARN log line so the ambiguity is never silent.
func NewConflictEntries ¶ added in v1.53.0
func NewConflictEntries(name string, baseHash, oursHash, theirsHash plumbing.Hash, mode filemode.FileMode) []*index.Entry
NewConflictEntries constructs the stage-1/2/3 index.Entry values for one conflicted path (Story 3.3.2, ConflictEntry in plan.md's Domain Glossary) — the sole construction point for conflicted index entries, so the go-git/git stage semantics below are never duplicated at a call site.
A zero hash for baseHash/oursHash/theirsHash is skipped rather than turned into an entry: an add/add conflict has no ancestor (base), and a delete/modify conflict is missing whichever side deleted the path (stack.md §3's "a pure add/delete conflict may have only 2 of the 3 stages" finding).
Never construct a conflicted entry with index.Merged: go-git's own doc comment claims it means "the default stage, fully merged" (i.e. real git's stage 0), but its actual value is 1 — identical to AncestorMode (architecture-review.md's landmine #2). Unconflicted entries must keep their existing Stage(0) value untouched; only AncestorMode/OurMode/TheirMode name the three conflict stages.
This project uses []*index.Entry (matching index.Index.Entries' own field type, and what the encoder/sortConflictEntries actually operate on) rather than plan.md's prose-level "[]index.Entry" — a deliberate, no-op-for-correctness deviation that avoids a value/pointer conversion at every call site.
func OpenRepo ¶ added in v1.51.0
func OpenRepo(path string) (*git.Repository, error)
OpenRepo opens the git repository or worktree at path using defaultPlainOpenOptions. Every git repository open in this codebase must go through this function rather than a bare git.PlainOpen/PlainOpenWithOptions call — see tools/lint's norawgitopen analyzer, which enforces this.
func PreviewWorktreePath ¶ added in v1.41.0
PreviewWorktreePath returns the directory PREFIX a new worktree would be created under - the same filepath.Join(worktreeDir, sanitizeBranchName(sessionName)) that NewGitWorktreeWithBranchAndExecutor computes, WITHOUT the "_<random-suffix>" it appends at actual creation time (that suffix can't be predicted ahead of the call). Performs no git subprocess calls (deliberately skips the existing-worktree-for-branch lookup, which shells out to "git worktree list") and, unlike findGitRepoRoot, never mutates the filesystem: it only walks up from repoPath looking for an existing git repo. A preview is called on every Omnibar keystroke, so it must be a pure read - it must not create directories, run `git init`, or create commits the way findGitRepoRoot's create-if-missing fallback does for the real creation path.
func RemoteURL ¶ added in v1.37.0
RemoteURL returns the URL of the named remote (usually "origin") for a local repo, matching `git remote get-url <remote>`'s single-URL output — uses go-git (no subprocess — the `prefer-go-git-over-subshells` skill). Fetch always uses the first configured URL (see config.RemoteConfig.URLs' doc comment), so this returns urls[0] the same way the CLI does.
func ResolveDefaultBranchSHA ¶ added in v1.48.0
ResolveDefaultBranchSHA finds repoPath's real default branch and returns its freshly- fetched origin tip SHA, trying CandidateDefaultBranches in order via ResolveOriginBranchSHA until one exists on origin. Exists because a repo's default branch can be named anything (this repo's own sibling dotfiles project uses "master", not "main") — a caller that hardcodes "main" gets a fetch failure on every single call against such a repo, which used to silently fall through to branching from the caller's ambient HEAD instead. Querying each candidate by fetch (rather than inspecting locally-cached remote-tracking refs) works even when the repo has never been fetched before, since a nonexistent branch fails fast at the fetch step itself.
func ResolveDefaultLocalBranchSHA ¶ added in v1.48.0
ResolveDefaultLocalBranchSHA is ResolveDefaultBranchSHA's fully-offline counterpart: tries CandidateDefaultBranches against repoPath's own local refs (no fetch), for use when every origin candidate fetch has already failed.
func ResolveExplicitBranchSHA ¶ added in v1.52.0
ResolveExplicitBranchSHA resolves branchName's tip commit SHA for a caller that deliberately opted out of the default-branch behavior (BacklogItem. BaseBranch) — origin's copy first (fresh fetch), falling back to a local branch of the same name when the origin fetch fails (offline, no origin remote). Unlike ResolveWorktreeBaseCommit, there is no unborn-repo/ambient- HEAD fallback here: an explicitly requested branch that doesn't exist anywhere is always a hard error, never silently substituted.
func ResolveLocalBranchSHA ¶ added in v1.48.0
ResolveLocalBranchSHA returns the tip commit SHA of the local branch (refs/heads/<branchName>) in repoPath, without fetching. Used as a same-branch-only fallback when ResolveOriginBranchSHA's fetch fails (offline, no origin remote) — still guarantees the caller branches from branchName itself, just not guaranteed fresh, unlike falling back to repoPath's ambient HEAD which could be checked out to any branch a concurrent process last left it on.
func ResolveOriginBranchSHA ¶ added in v1.42.0
ResolveOriginBranchSHA fetches mainBranch from origin into repoPath and returns the resulting origin/mainBranch tip commit SHA. Unlike a bare `rev-parse HEAD` against repoPath's own checkout, this always fetches first, so the returned SHA reflects origin's true current tip rather than whatever repoPath happened to have checked out last — the gap that let a new backlog work session's worktree branch from a days-stale local checkout instead of the real main tip (see legacySetupNewWorktree's "branch from current HEAD" comment, and CreateBacklogWorktree's use of this func).
func ResolveWorktreeBaseCommit ¶ added in v1.52.0
ResolveWorktreeBaseCommit resolves the commit new backlog-item worktrees should fork from: origin's default branch tip, falling back to a local default branch when the origin fetch fails (offline, no origin remote). baseSHA == "" (with err == nil) means repoPath has no commits yet (IsUnbornRepo) — the only case it's safe for a caller to fall back to branching from ambient HEAD, since no other branch can exist to accidentally fork from instead. Any other resolution failure is returned as an error rather than silently falling back to ambient HEAD, which is what let new work fork from whatever branch repoPath's checkout happened to be sitting on (e.g. an agent's own in-progress feature branch) instead of main. Shared by session.CreateBacklogWorktree and TriggerTriage's isolated triage worktree (server/services/backlog_service_triage.go).
func SanitizeBranchName ¶ added in v1.47.0
SanitizeBranchName exports sanitizeBranchName for callers outside this package that need the same safe-subset transform for a remote worktree directory name (server/services/session_service.go's CreateSession mode-specific block, ssh-remote-workspaces Phase 4 Epic 4.2) as this package already applies to local branch/directory names -- including its path-traversal-segment stripping, which matters just as much for a remote `path.Join(base_path, name)` as it does for the local filepath.Join call sites in this file.
func TreeDiffPair ¶ added in v1.53.0
func TreeDiffPair(base, ours, theirs *object.Tree) (baseToOurs, baseToTheirs object.Changes, err error)
TreeDiffPair computes the two change-sets the diff3 reconciler (Epic 3.2) needs as its raw input: base→ours and base→theirs.
func UntrackScaffolding ¶ added in v1.41.0
UntrackScaffolding removes any git index entry matching patterns (git-rm-cached semantics: the working-tree file is left alone, only the index entry is dropped) and returns the list of paths it untracked. Uses go-git directly against the index rather than shelling out to `git rm --cached`, per the `prefer-go-git-over-subshells` skill.
Returns (nil, nil) — not an error — when worktreePath isn't a git repository at all (e.g. a directory-mode session with no git backing), matching the best-effort, non-fatal handling the rest of this package uses for that case.
func ValidateBranchName ¶ added in v1.52.0
ValidateBranchName rejects a branch name before it ever reaches a git subprocess call, closing a git argument-injection class (same family as CVE-2017-1000117): git's own argument parser treats any positional value starting with "-" as an option rather than a ref name, so an unvalidated caller-supplied branch name (BacklogItem.BaseBranch) reaching e.g. `git fetch origin <name>` could smuggle in a flag like "--upload-pack=<cmd>". FetchBranch also guards with "--" (safe for fetch's refspec argument); CheckoutBranch cannot use the same "--" guard (it puts checkout into path-restore mode instead, verified empirically), so this validation is its only defense. Deliberately conservative: rejects empty, anything starting with "-", and ".." (a valid git ref name is never required to use either).
func WithRepoWorktreeLock ¶ added in v1.43.0
WithRepoWorktreeLock serializes fn against every other goroutine and OS process (on this machine) operating on repoPath's git worktree metadata. See repoWorktreeLock's doc comment for why both mu and flock are required.
Exported so callers outside this package that need to run other repoPath-mutating work (e.g. session.RepairCorruptedGitRepo's destructive re-clone) in the same critical section as a GitWorktree.Setup()/Remove() can do so via GitWorktree.SetupLocked() — see that method's doc comment for the race this closes.
Types ¶
type AdminFileWriter ¶ added in v1.53.0
type AdminFileWriter struct {
// contains filtered or unexported fields
}
AdminFileWriter is the atomic write-temp-then-rename-then-fsync primitive (ADR-001, project_plans/go-git-worktree-and-merge/decisions/ADR-001-atomic-admin-file-write-protocol.md) used for every file written under a WorktreeAdminDir (.git/worktrees/<name>/) and for conflicted-index writes on the merge path. go-git's own writers (dotgit.IndexWriter, setRefRwfs) are neither atomic nor real-git-lock-compatible (ADR-001's Context), so this project owns the write protocol instead of reusing go-git's.
func NewAdminFileWriter ¶ added in v1.53.0
func NewAdminFileWriter(dir string) *AdminFileWriter
NewAdminFileWriter returns an AdminFileWriter rooted at dir. dir must already exist — WriteFile does not create it.
func (*AdminFileWriter) WriteFile ¶ added in v1.53.0
func (w *AdminFileWriter) WriteFile(name string, content []byte) error
WriteFile atomically and durably writes content to name within w.dir: either the target ends up containing exactly content, or a crash at any point leaves it completely untouched (old content or absence), never torn. The extra directory fsync after rename is required per ADR-001 step 4: renaming a file durably requires fsyncing its parent directory, not just the file.
type AggregateDiffStat ¶ added in v1.49.0
AggregateDiffStat is the files-changed/additions/deletions summary returned by DiffStatBetween — a DiffShortstat-equivalent for a branch's range vs. its base. Distinct from DiffStats (session/git/diff.go), which holds working-tree diff content, not a committed-range summary.
func DiffStatBetween ¶ added in v1.49.0
func DiffStatBetween(ctx context.Context, repoPath, baseSHA, headSHA string) (AggregateDiffStat, error)
DiffStatBetween returns the aggregate files-changed/additions/deletions between baseSHA and headSHA, summing FileStatsBetween's per-file counts. ctx only guards against starting once the caller's deadline has passed — FileStatsBetween's go-git patch computation takes no ctx, so this can't bound a hang mid-computation.
type BranchName ¶ added in v1.53.0
type BranchName string
BranchName is a git branch name (e.g. "main"), never a path or a commit SHA.
type BranchStatus ¶ added in v1.39.0
type BranchStatus struct {
// BranchExists is false once the branch has been deleted (e.g. after a
// "delete branch on merge" or manual cleanup). AheadOfMain/BehindMain are
// only meaningful when true.
BranchExists bool
AheadOfMain int
BehindMain int
}
BranchStatus describes branchName's position relative to mainBranch.
func BranchAheadBehind ¶ added in v1.39.0
func BranchAheadBehind(repoPath, branchName, mainBranch string) (BranchStatus, error)
BranchAheadBehind reports branchName's commit position relative to mainBranch: how many commits are on the branch but not on main (ahead), and vice versa (behind) — mirroring `git rev-list --left-right --count branch...main`. Checks the local branch ref only; a branch already deleted locally reports BranchExists=false rather than an error, since that's the expected state for a shipped, cleaned-up item, not a failure.
type ConflictMarkerStyle ¶ added in v1.53.0
type ConflictMarkerStyle int
ConflictMarkerStyle names which git.conflictStyle format renderConflictHunk emits.
Confirmed in this environment (2026-09-06, per plan.md's Unresolved Questions): `git config --global --get merge.conflictStyle` is unset (real git's own default, "merge" style — no `|||||||` base section); this repo's local .git/config happens to have `merge.conflictstyle = diff3` set, which this project deliberately does NOT follow — matching the *global* default (what a fresh clone/CI checkout produces) is the byte-compatibility target, not this one checkout's local override.
const ( // MergeStyleDefault renders 7-character <<<<<<</=======/>>>>>>> markers with no // ||||||| base section — git's default merge.conflictStyle ("merge"). MergeStyleDefault ConflictMarkerStyle = iota )
type DiffStats ¶
type DiffStats struct {
// Content is the full diff content
Content string
// Added is the number of added lines
Added int
// Removed is the number of removed lines
Removed int
// Error holds any error that occurred during diff computation
// This allows propagating setup errors (like missing base commit) without breaking the flow
Error error
}
DiffStats holds statistics about the changes in a diff
func DiffContentBetween ¶ added in v1.51.0
DiffContentBetween returns the unified-diff text and added/removed line counts between baseSHA and headSHA (both resolved as commits, not the working tree — equivalent to `git diff baseSHA..headSHA`), using go-git's typed diff API instead of a safeexec shell-out (the `prefer-go-git-over-subshells` skill). Distinct from GitWorktree.Diff (session/git/diff.go), which diffs the working tree (uncommitted changes and untracked files) against a single base commit — GitWorktree.Diff now has a go-git implementation too, built from lower-level tree/gitignore/line-diff primitives rather than a single library call; see its doc comment.
type FileConflictReason ¶ added in v1.53.0
type FileConflictReason string
FileConflictReason names why MergeFile short-circuited before attempting a content merge (Story 3.2.3) — a mode/binary/gitlink conflict is always reported this way, never silently mismerged or fed into content merging.
const ( // ReasonNone means no short-circuit fired; MergeFile ran a normal // content merge (see FileMergeOutcome.Result). ReasonNone FileConflictReason = "" // ReasonModeConflict: ours' and theirs' file modes differ. ReasonModeConflict FileConflictReason = "mode conflict" // ReasonBinaryConflict: a binary file changed differently on both sides. ReasonBinaryConflict FileConflictReason = "binary conflict" // ReasonGitlinkConflict: a gitlink (submodule) entry on either side. ReasonGitlinkConflict FileConflictReason = "gitlink conflict" )
type FileMergeInput ¶ added in v1.53.0
type FileMergeInput struct {
BaseMode, OursMode, TheirsMode filemode.FileMode
BaseContent, OursContent, TheirsContent []byte
}
FileMergeInput bundles one path's three-way inputs — content and file mode on each side — for MergeFile's mode/binary/gitlink short-circuits ahead of any content-level three-way merge.
type FileMergeOutcome ¶ added in v1.53.0
type FileMergeOutcome struct {
Reason FileConflictReason
Result *MergeResult
// ResolvedMode is the file mode the merged content should carry, meaningful only
// when Reason == ReasonNone: OursMode/TheirsMode when they agree, otherwise
// whichever one of them actually changed away from BaseMode (see resolveFileMode).
ResolvedMode filemode.FileMode
}
FileMergeOutcome is MergeFile's result: either a short-circuited conflict (Reason set, Result nil) or a fully classified content merge (Result set, Reason == ReasonNone, ResolvedMode set to whichever mode survives — see resolveFileMode).
type FileStat ¶ added in v1.39.0
FileStat describes one file's change between two commits, as returned by FileStatsBetween. Path is the file's path as of headSHA — for a rename this is the new path, not the old one. Status is one of "added", "deleted", "renamed", or "modified".
func FileStatsBetween ¶ added in v1.39.0
FileStatsBetween returns the per-file diff-stat summary (path, status, additions, deletions) for every file that changed between baseSHA and headSHA in the repo at repoPath, using go-git's typed diff API — no safeexec shell-out (the `prefer-go-git-over-subshells` skill).
Renames are reported as a single entry keyed by the file's new path, not a delete+add pair: go-git's FilePatch.Files() already exposes the from/to path pair needed to detect this directly, so unlike object.Patch.Stats() (whose FileStat.Name collapses a rename into a single "old => new" display string) this walks Patch.FilePatches() itself to keep the old and new paths distinct. Binary files are silently omitted — go-git produces zero diff chunks for them (the same signal it uses to skip submodule-ref-only changes), so there is no meaningful addition/deletion count to report; this mirrors go-git's own Stats() behavior rather than the "0/0 entry" shape one might expect, a discrepancy confirmed against go-git v5.14.0's source (getFileStatsFromFilePatches in plumbing/object/patch.go) and a throwaway spike before this function was written.
type GitWorktree ¶
type GitWorktree struct {
// contains filtered or unexported fields
}
GitWorktree manages git worktree operations for a session
func NewGitWorktree ¶
func NewGitWorktree(repoPath string, sessionName string, opts ...GitWorktreeOption) (tree *GitWorktree, branchname string, err error)
NewGitWorktree creates a new GitWorktree instance
func NewGitWorktreeFromCommitSHA ¶
func NewGitWorktreeFromCommitSHA(repoPath, sessionName, branchName, commitSHA string, opts ...GitWorktreeOption) (*GitWorktree, string, error)
NewGitWorktreeFromCommitSHA creates a new GitWorktree that will branch from the given commitSHA when Setup() is called, instead of branching from the current HEAD. This is used by ForkFromCheckpoint to recreate the exact git state at checkpoint time.
func NewGitWorktreeFromExisting ¶
func NewGitWorktreeFromExisting(existingWorktreePath string, sessionName string, opts ...GitWorktreeOption) (*GitWorktree, error)
NewGitWorktreeFromExisting creates a GitWorktree from an existing worktree path This is used when connecting to worktrees that were created manually or by deleted sessions
func NewGitWorktreeFromExistingWithExecutor ¶
func NewGitWorktreeFromExistingWithExecutor(existingWorktreePath string, sessionName string, opts ...GitWorktreeOption) (*GitWorktree, error)
NewGitWorktreeFromExistingWithExecutor creates a GitWorktree from an existing worktree path. The "WithExecutor" name predates CommandRunner (ADR-002) — see NewGitWorktreeFromStorageWithExecutor's doc comment; use WithCommandRunner to override how this worktree's subprocesses run.
func NewGitWorktreeFromStorage ¶
func NewGitWorktreeFromStorage(repoPath string, worktreePath string, sessionName string, branchName string, baseCommitSHA string, opts ...GitWorktreeOption) *GitWorktree
func NewGitWorktreeFromStorageWithExecutor ¶
func NewGitWorktreeFromStorageWithExecutor(repoPath string, worktreePath string, sessionName string, branchName string, baseCommitSHA string, opts ...GitWorktreeOption) *GitWorktree
NewGitWorktreeFromStorageWithExecutor creates a GitWorktree from stored data. The "WithExecutor" name predates CommandRunner (ADR-002): this used to also accept an optional executor.Executor parameter, removed once runGitCommand's last executor.Executor-gated branch was migrated onto CommandRunner and nothing else in the package read it (see ADR-002's addendum) — use WithCommandRunner instead to override how this worktree's subprocesses run.
func NewGitWorktreeWithBranch ¶
func NewGitWorktreeWithBranch(repoPath string, sessionName string, customBranch string, opts ...GitWorktreeOption) (tree *GitWorktree, branchname string, err error)
NewGitWorktreeWithBranch creates a new GitWorktree instance with an optional custom branch name
func NewGitWorktreeWithBranchAndExecutor ¶
func NewGitWorktreeWithBranchAndExecutor(repoPath string, sessionName string, customBranch string, opts ...GitWorktreeOption) (tree *GitWorktree, branchname string, err error)
NewGitWorktreeWithBranchAndExecutor creates a new GitWorktree with an optional branch name. The "WithExecutor" name predates CommandRunner (ADR-002) — see NewGitWorktreeFromStorageWithExecutor's doc comment; use WithCommandRunner to override how this worktree's subprocesses run.
func (*GitWorktree) Cleanup ¶
func (g *GitWorktree) Cleanup() error
Cleanup removes the worktree. It deliberately does NOT delete the branch: branch deletion via go-git's RemoveReference is not a "safe if merged" check, it is unconditional, and a branch can hold commits that exist nowhere else (never pushed, never merged). Silently destroying those on session teardown was a live bug — see docs/tasks/backlog-feature-improvement.md ("stop_session silently deletes the git branch"). A leftover local branch ref costs nothing; a lost commit is not recoverable through this code path. Equivalent to Remove() — kept as a separate method so callers don't need to know the two used to differ.
func (*GitWorktree) ClosePR ¶ added in v1.41.0
func (g *GitWorktree) ClosePR(prNumber int, comment string) error
ClosePR closes prNumber without merging, posting comment as an explanatory PR comment first. Used when a PR is discovered to be superseded (its branch's work already landed on main through a different path) rather than genuinely broken — see BUG-032.
func (*GitWorktree) CommitChanges ¶
func (g *GitWorktree) CommitChanges(commitMessage string) error
CommitChanges commits changes locally without pushing to remote
func (*GitWorktree) CreatePR ¶ added in v1.37.0
func (g *GitWorktree) CreatePR(opts PRCreateOptions) (prURL string, prNumber int, err error)
CreatePR creates a GitHub pull request for the current branch and returns the PR URL and number. If a PR already exists for the branch it is returned without creating a new one.
func (*GitWorktree) Diff ¶
func (g *GitWorktree) Diff() *DiffStats
Diff returns the git diff between the worktree and the base branch along with statistics. The diff content itself (this function and buildWorkingTreePatch) is computed via go-git by comparing the base commit's tree directly against the worktree's current on-disk state — tracked-file modifications, deletions, and untracked-but-not-gitignored new files alike — rather than shelling out to `git add -N .` + `git --no-pager diff <baseSHA>` as before. That -N staging trick only existed to coax the `git diff` CLI into including untracked files; this implementation includes them directly by construction, with no need to mutate the git index. ops.go's DiffContentBetween (commit-to-commit) claimed no go-git equivalent existed for this working-tree case; this is that equivalent, built from go-git's lower-level tree/gitignore/line-diff primitives rather than a single library call. See resolveBaseCommitSHA's doc comment for the one piece of this feature that deliberately stays on a subprocess (ref resolution, not diff content — a real go-git race documented elsewhere in this package).
func (*GitWorktree) EnablePRAutoMerge ¶ added in v1.37.0
func (g *GitWorktree) EnablePRAutoMerge(prNumber int) error
EnablePRAutoMerge enables GitHub auto-merge on the given PR so it merges automatically once required CI checks pass. Best-effort: fails silently when the repo does not have auto-merge enabled in its branch protection rules.
func (*GitWorktree) GetBaseCommitSHA ¶
func (g *GitWorktree) GetBaseCommitSHA() string
GetBaseCommitSHA returns the base commit SHA for the worktree
func (*GitWorktree) GetBranchName ¶
func (g *GitWorktree) GetBranchName() string
GetBranchName returns the name of the branch associated with this worktree
func (*GitWorktree) GetPRStatus ¶ added in v1.37.0
func (g *GitWorktree) GetPRStatus(prNumber int) (*PRStatus, error)
GetPRStatus fetches the combined CI check status, reviewer decisions, mergeability, and PR comments for the given pull request number.
func (*GitWorktree) GetRepoName ¶
func (g *GitWorktree) GetRepoName() string
GetRepoName returns the name of the repository (last part of the repoPath).
func (*GitWorktree) GetRepoPath ¶
func (g *GitWorktree) GetRepoPath() string
GetRepoPath returns the path to the repository
func (*GitWorktree) GetWorktreePath ¶
func (g *GitWorktree) GetWorktreePath() string
GetWorktreePath returns the path to the worktree
func (*GitWorktree) HasCommitsAheadOfMain ¶ added in v1.41.0
func (g *GitWorktree) HasCommitsAheadOfMain(mainBranch string) (bool, error)
HasCommitsAheadOfMain reports whether this worktree's branch has at least one commit not present on mainBranch — i.e. whether there is genuinely anything to ship. Used as a pre-flight check before attempting CreatePR: a branch with zero commits ahead of main makes `gh pr create` fail with "No commits between X and Y", which is not a retryable push/PR failure (see BUG-063) but a signal that the item was already fully addressed elsewhere. Returns true (the safe, existing default: attempt PR creation as before) if the check itself is inconclusive — an error opening the repo, or the branch not existing locally — so a check failure never causes a caller to skip PR creation for a branch that may well need it.
func (*GitWorktree) HasStagedChanges ¶ added in v1.41.0
func (g *GitWorktree) HasStagedChanges() (bool, error)
HasStagedChanges reports whether the git index differs from HEAD — i.e. whether a commit right now would actually record anything. Used after StageAllExceptScaffolding so a commit whose only staged change was a just-untracked scaffolding file is skipped gracefully instead of failing on "nothing to commit".
func (*GitWorktree) InvalidateDirtyCache ¶ added in v1.22.0
func (g *GitWorktree) InvalidateDirtyCache()
InvalidateDirtyCache clears the IsDirty cache so the next call re-runs git status. Call this whenever worktree state changes outside of Claude's control (e.g. after a manual commit, after running git operations, or in tests after writing files directly).
func (*GitWorktree) IsBranchCheckedOut ¶
func (g *GitWorktree) IsBranchCheckedOut() (bool, error)
IsBranchCheckedOut checks if the instance branch is currently checked out. Uses go-git to read HEAD directly (no subprocess).
func (*GitWorktree) IsDirty ¶
func (g *GitWorktree) IsDirty() (bool, error)
IsDirty checks if the worktree has uncommitted changes. Results are cached for IsDirtyCacheTTL (dirty) or IsDirtyCleanCacheTTL (clean).
func (*GitWorktree) IsDirtyWithHint ¶ added in v1.22.0
func (g *GitWorktree) IsDirtyWithHint(claudeActive bool) (bool, error)
IsDirtyWithHint checks if the worktree has uncommitted changes. When claudeActive is true the subprocess is skipped entirely and the cached value is returned (or false if no cached value is available yet), because Claude never modifies worktree state while it is actively generating output.
func (*GitWorktree) IsPRMerged ¶ added in v1.37.0
func (g *GitWorktree) IsPRMerged(prNumber int) (bool, error)
IsPRMerged reports whether the given PR number has been merged.
func (*GitWorktree) OpenBranchURL ¶
func (g *GitWorktree) OpenBranchURL() error
OpenBranchURL opens the branch URL in the default browser
func (*GitWorktree) PrimeDirtyCacheAt ¶ added in v1.35.0
func (g *GitWorktree) PrimeDirtyCacheAt(t time.Time)
PrimeDirtyCacheAt sets the dirty-cache timestamp to t without running git status. Use this to stagger per-session cache expiry so sessions added to the poller within a short window don't all expire simultaneously and burst-launch git subprocesses.
func (*GitWorktree) Prune ¶
func (g *GitWorktree) Prune() error
Prune removes all working tree administrative files and directories. Serialized per-repoPath like Setup/Remove — it rewrites the same shared .git/worktrees/ metadata.
func (*GitWorktree) PushBranch ¶ added in v1.37.0
func (g *GitWorktree) PushBranch() error
func (*GitWorktree) PushChanges ¶
func (g *GitWorktree) PushChanges(commitMessage string, open bool) error
PushChanges commits and pushes changes in the worktree to the remote branch
func (*GitWorktree) Remove ¶
func (g *GitWorktree) Remove() error
Remove removes the worktree but keeps the branch. Serialized per-repoPath like Setup — it prunes and removes shared .git/worktrees/ administrative metadata, the same resource Setup's branch-check + add dispatch touches.
func (*GitWorktree) RenameBranch ¶ added in v1.42.0
func (g *GitWorktree) RenameBranch(newBranchName string) error
RenameBranch renames the worktree's current branch in place (git branch -m) and updates g.branchName to match. Used to move a worktree created under a provisional name onto the final branch name once it's known, without losing the worktree's existing content or commits — e.g. TriggerTriage names its worktree before the LLM call reveals the item's slug, then renames it afterward to the same "backlog/<item>" branch a later SpawnSessionFromItem will look for.
func (*GitWorktree) RequestCopilotReview ¶ added in v1.41.0
func (g *GitWorktree) RequestCopilotReview(prNumber int) error
RequestCopilotReview requests a GitHub Copilot code review on prNumber. Best-effort: fails when Copilot code review isn't enabled for the org/repo, or on any other gh error — callers must not fail PR creation on this error. Uses the legacy bot-login form (copilot-pull-request-reviewer[bot]) via --add-reviewer rather than the newer @copilot alias, since the literal login is accepted by every gh version this repo targets while the alias is version-gated (see plan.md's Pattern Decisions table).
func (*GitWorktree) Setup ¶
func (g *GitWorktree) Setup() error
Setup creates a new worktree for the session. The entire branch-check + add/reuse dispatch is serialized per-repoPath (across goroutines and OS processes) because git worktree add mutates shared .git/worktrees/ administrative metadata that is not safe under concurrent access -- see WithRepoWorktreeLock.
func (*GitWorktree) SetupLocked ¶ added in v1.43.0
func (g *GitWorktree) SetupLocked() error
SetupLocked runs the same setup logic as Setup but assumes the caller already holds repoPath's worktree lock (via WithRepoWorktreeLock) -- e.g. because the caller needs to run other repoPath-mutating work (like a corrupted-repo repair/re-clone) in the very same critical section, immediately before the worktree add. Calling this without already holding the lock defeats the cross-process guarantee Setup() normally provides. See session.CreateBacklogWorktree for the motivating caller and the race this closes: without it, one process's unlocked repo repair (os.RemoveAll + re-clone) could delete/recreate repoPath's working tree while a concurrent process was mid-way through the locked `git worktree add` for the same repoPath, plausibly surfacing as git's generic "fatal: failed to resolve HEAD as a valid ref".
func (*GitWorktree) StageAllExceptScaffolding ¶ added in v1.41.0
func (g *GitWorktree) StageAllExceptScaffolding() error
StageAllExceptScaffolding stages all worktree changes (`git add .`) and then untracks any staged path matching ScaffoldingExcludePatterns, so backlog automation scaffolding files (.backlog-context.md, .claude/commands/backlog/*) don't get (re)committed even if they were already tracked in this branch's history — gitignore/info-exclude rules only stop a NEW path from being staged, not one that's already in the index (see UntrackScaffolding's doc comment). Deliberately fails open on the untrack step (logs and continues rather than blocking a real commit) — the CI backstop workflow (.github/workflows/backlog-scaffolding-guard.yml) is the second, independent layer for the rare case where the untrack step itself errors.
type GitWorktreeOption ¶ added in v1.47.0
type GitWorktreeOption func(*GitWorktree)
GitWorktreeOption is a functional option for GitWorktree construction, mirroring session/tmux's TmuxSessionOption. Trailing/variadic on every constructor that builds a *GitWorktree, so existing call sites are unaffected.
func WithCommandRunner ¶ added in v1.47.0
func WithCommandRunner(r tmux.CommandRunner) GitWorktreeOption
WithCommandRunner injects a CommandRunner, overriding the tmux.LocalRunner{} default every constructor otherwise applies. Used to swap in a remote-backed CommandRunner (Phase 2 of ssh-remote-workspaces) or a test spy that records/controls what the worktree's git/gh subprocess calls do.
type MergeHunk ¶ added in v1.53.0
type MergeHunk struct {
Kind MergeRegionKind
Base []string
Ours []string
Theirs []string
}
MergeHunk is one classified region of a file's reconciled content: a MergeRegionKind plus the base/ours/theirs line content it covers (lines have no trailing newline — content assembly re-joins them).
type MergeMainResult ¶ added in v1.39.0
type MergeMainResult struct {
// UpToDate is true when the worktree's branch already contained everything
// from mainBranch — nothing was merged in.
UpToDate bool
// Merged is true when the merge (including a fast-forward) brought in new
// commits from mainBranch.
Merged bool
// Conflicted is true when merging mainBranch produced conflicts. The merge is
// always aborted before returning, so the worktree is left clean either way —
// callers never have to clean up a half-merged tree.
Conflicted bool
// ConflictedFiles lists the paths that conflicted. Populated only when
// Conflicted is true.
ConflictedFiles []string
}
MergeMainResult describes the outcome of MergeMainIntoWorktree.
func MergeMainIntoWorktree ¶ added in v1.39.0
func MergeMainIntoWorktree(worktreePath, mainBranch string) (*MergeMainResult, error)
MergeMainIntoWorktree fetches mainBranch from origin and merges it into whatever branch is currently checked out in worktreePath. It never leaves the worktree in a conflicted state: on conflict it aborts the merge immediately and reports the conflicting paths, so the caller can hand that context to whoever resolves it rather than leaving a half-merged working tree behind for the next thing that touches it.
Dispatches to nativeMergeMainIntoWorktree (Epic 3.4) or legacyMergeMainIntoWorktree (the original subprocess-based implementation below) based on useNativeMerge, keyed by worktreePath per ADR-002 — every real call site (drift.go's EnsureBranchSyncedWithMain, backlog_service_triage.go's syncPRBranchWithMain, session/backlog_lifecycle.go's branchReconciler, which is assigned this exact function value) gets flag coverage with no changes of its own.
type MergeRegionKind ¶ added in v1.53.0
type MergeRegionKind int
MergeRegionKind classifies one reconciled region of a three-way file merge. It is a sum type, not boolean flags (isConflict/isOurs/isTheirs), so that illegal states (e.g. "ours and theirs simultaneously") cannot be represented — see the "Conflict classification" Pattern Decision.
const ( // RegionUnchanged covers base lines neither side touched. RegionUnchanged MergeRegionKind = iota // RegionOursOnly covers a region only ours changed (or both sides made // the identical change — see Story 3.2.2's "identical edits" case). RegionOursOnly // RegionTheirsOnly covers a region only theirs changed. RegionTheirsOnly // RegionConflict covers a region both sides changed differently. RegionConflict )
type MergeResult ¶ added in v1.53.0
MergeResult is the outcome of a ThreeWayFileMerger.Merge or MergeFile call: the classified hunks plus the assembled content for the auto-resolved case. When Conflicted is true, Content only reflects the non-conflicted hunks — rendering conflict markers into the working-tree file is Epic 3.3's renderConflictHunk, not this package's job (Task 3.2.2c).
func (*MergeResult) Conflicts ¶ added in v1.53.0
func (r *MergeResult) Conflicts() []MergeHunk
Conflicts returns the subset of Hunks classified as RegionConflict.
type NativeWorktreeEntry ¶ added in v1.53.0
type NativeWorktreeEntry struct {
// Name is the admin dir's own name under .git/worktrees/ (WorktreeAdminDir's leaf
// component), not necessarily the branch name — AllocateAdminDirName names it after
// the target directory's basename, and a collision can suffix it further.
Name string
// WorktreePath is the linked worktree's working-directory path, read from the admin
// dir's GitdirFile ("<WorktreePath>/.git").
WorktreePath string
// BranchRef is the full ref name (e.g. "refs/heads/feature-x") read from the admin
// dir's HEAD file, or the raw content of HEAD when it isn't a symbolic ref (detached
// HEAD) — empty if HEAD is missing or unreadable.
BranchRef string
// Locked reports whether the admin dir's LockedMarker ("locked") is present.
Locked bool
// Prunable is this project's deliberately scoped-down classification (stack.md
// §2.2): true when WorktreePath no longer exists on disk and the entry isn't
// Locked. No mtime grace period, unlike real git's own should_prune_worktree.
Prunable bool
}
NativeWorktreeEntry describes one worktree found under a repo's `.git/worktrees/` admin directory (plan.md's Domain Glossary), as produced by nativeListWorktrees — the pure-Go replacement for parsing `git worktree list --porcelain` output.
type PRCreateOptions ¶ added in v1.47.0
type PRCreateOptions struct {
// Title defaults to the branch name (with hyphens replaced by spaces) if empty.
Title string
Body string
// BaseBranch, if non-empty, is passed to `gh pr create --base`; if empty,
// gh's own default-branch resolution is used (preserves pre-existing
// behavior for every caller that doesn't care which branch it targets).
BaseBranch string
}
PRCreateOptions bundles CreatePR's arguments. Title, Body, and BaseBranch are all plain strings with no compiler-enforced distinction between them, so a transposed call (e.g. body and baseBranch swapped) would previously compile silently wrong — the exact smell the `primitive-obsession-checklist` skill exists to catch. A named field wins that transposition back at every call site.
type PRStatus ¶ added in v1.37.0
type PRStatus struct {
// CIFailing is true when at least one CI check has a terminal failure.
CIFailing bool
// HasBlockingReviews is true when a reviewer has requested changes.
HasBlockingReviews bool
// HasConflicts is true when GitHub reports mergeStateStatus == "DIRTY" or
// mergeable == "CONFLICTING" — its branch cannot be merged as-is and needs
// a rebase. Both fields are checked (see Task 1.1.1d) because gh's
// mergeable field has been observed returning stale data (cli/cli#9583).
HasConflicts bool
// IsClosed is true when the PR's state is CLOSED (rejected by a human without
// merging) rather than OPEN or MERGED. Callers must check this before treating
// "not merged" as "still open and healthy" — a closed PR will never merge on
// its own no matter how long ReconcilePRPending keeps polling it.
IsClosed bool
// IsDraft is true when the PR is still marked draft on GitHub. Captured from
// the same gh pr view call as everything else on this struct (no second API
// call) so callers such as the backlog stuck-item detector (prReadyToMergeSolo)
// can gate on it without an extra fetch.
IsDraft bool
// Mergeable is the raw upper-cased GitHub `mergeable` field ("MERGEABLE",
// "CONFLICTING", or "UNKNOWN"). HasConflicts is the belt-and-suspenders
// bool derived from this plus mergeStateStatus (see above); Mergeable is
// exposed separately for callers (prReadyToMergeSolo) that want the literal
// "MERGEABLE" check called out in ADR-001 rather than the inverse-of-conflict
// approximation.
Mergeable string
// ApprovedCount is the number of current non-dismissed APPROVED reviews.
ApprovedCount int
// ChangesRequestedCount is the number of current non-dismissed
// CHANGES_REQUESTED reviews (equivalently, len of the reviews backing
// HasBlockingReviews — exposed as a count so callers building a
// github.PRInfo-shaped value don't need to re-derive it from the bool).
ChangesRequestedCount int
// HasReviewFeedback is true when at least one substantive COMMENTED-state
// review or substantive plain PR comment exists (Copilot's typical review
// posture is COMMENTED, not CHANGES_REQUESTED, so this is a distinct signal
// from HasBlockingReviews). Non-substantive feedback (bare "lgtm", empty,
// or whitespace-only bodies) never sets this.
HasReviewFeedback bool
// LatestFeedbackAt is the newest GitHub-assigned submittedAt/createdAt
// timestamp among all substantive feedback captured this call; the zero
// value when HasReviewFeedback is false. Callers use this as the dedup
// watermark comparison point (see ReconcilePRPending's hasNewFeedback).
LatestFeedbackAt time.Time
// FeedbackText is a combined human-readable summary for the fix agent.
FeedbackText string
// contains filtered or unexported fields
}
PRStatus holds the CI, review, and conflict state for a pull request.
func ParsePRStatusPayload ¶ added in v1.41.0
ParsePRStatusPayload parses gh pr view's combined JSON output into a PRStatus. Exported so callers outside this package (e.g. session/backlog_lifecycle_test.go's ReconcilePRPending fixtures) can build a *PRStatus with commentReviews/generalComments genuinely populated — FeedbackAuthors() depends on those unexported fields, which a struct literal from another package cannot set directly.
func (*PRStatus) FeedbackAuthors ¶ added in v1.41.0
FeedbackAuthors returns one author login per countable feedback item (COMMENTED reviews plus countableGeneralComments) captured this call — NOT deduplicated, so the same login appears once per item they authored. Callers use len() of this slice as an item count and the logins as an author list for a single hasNewFeedback-triggered dispatch, since a partially-addressed multi-item batch is otherwise silently unresolved forever once the dedup watermark advances past the whole batch.
type PathChange ¶ added in v1.53.0
PathChange bundles what each side of TreeDiffPair's two object.Changes sets independently says happened to one path. Either side may be nil, meaning that side made no change to this path relative to base.
type PreMergeIndexSnapshot ¶ added in v1.53.0
type PreMergeIndexSnapshot struct {
// Entries are the original stage-0 index entries for every path that became
// conflicted, captured before the merge attempt touched them.
Entries []*index.Entry
// Content is each conflicted path's original working-tree file content, keyed by
// path relative to the worktree root.
Content map[string][]byte
}
PreMergeIndexSnapshot captures a conflicted path's state immediately before a merge attempt touched it, so abortNativeMerge can restore both the index and the working-tree file content after materializeConflictOnAbort's transient conflict markers/index entries are no longer wanted (Tech Debt Disposition: "always materialize, then abort" — this is the "then abort" half).
type RemoteWorktree ¶ added in v1.47.0
type RemoteWorktree struct {
// RepoPath is the existing repository on the remote host that `git worktree
// add`/`remove` runs against (the CommandRunner.Run "dir" argument).
RepoPath string
// WorktreePath is the new worktree's own directory. Its parent directory is
// the "base_path" CreateWorktree existence-checks before creating anything.
WorktreePath string
// Branch is the (already-existing) branch CreateWorktree checks out into the
// new worktree. CreateWorktree does not create the branch itself — mirroring
// GitWorktree's setupFromExistingBranch `git worktree add <path> <branch>`
// shape (worktree_ops.go), not the `-b <branch>` new-branch shape.
Branch string
}
RemoteWorktree identifies one git worktree on a remote host: the existing repository it is attached to (RepoPath — what `git worktree add`/`remove` runs against, mirroring GitWorktree.repoPath) and the worktree's own directory (WorktreePath — mirroring GitWorktree.worktreePath). Branch is the branch CreateWorktree checks out into the new worktree; it is ignored by RemoveWorktree. RepoPath and WorktreePath are bundled into this single value object rather than passed as separate positional string parameters so a caller can't silently transpose them at the call site (the `primitive-obsession-checklist` skill).
type RemoteWorktreeOps ¶ added in v1.47.0
type RemoteWorktreeOps struct {
// contains filtered or unexported fields
}
RemoteWorktreeOps performs git worktree creation, removal, and new-project initialization on a remote host, executed entirely through an injected tmux.CommandRunner (session/tmux, reused here per ADR-002 — see session/git/worktree.go's runner field doc comment for the same pattern applied to GitWorktree). It mirrors ops.go's free-function shape (explicit path/branch parameters, no persisted per-worktree state) rather than GitWorktree's stateful builder shape, since — unlike GitWorktree, which owns exactly one worktree's lifecycle — a single RemoteWorktreeOps is meant to be reused across many worktrees on the same remote CommandRunner.
func NewRemoteWorktreeOps ¶ added in v1.47.0
func NewRemoteWorktreeOps(runner tmux.CommandRunner) *RemoteWorktreeOps
NewRemoteWorktreeOps constructs a RemoteWorktreeOps that runs every command through runner (a *tmux.SSHRunner in production, tmux.LocalRunner{} or a test spy in tests).
func (*RemoteWorktreeOps) CreateWorktree ¶ added in v1.47.0
func (r *RemoteWorktreeOps) CreateWorktree(ctx context.Context, w RemoteWorktree) error
CreateWorktree runs `git worktree add <WorktreePath> <Branch>` on the remote host via the injected CommandRunner, mirroring GitWorktree.setupFromExistingBranch's argument construction (worktree_ops.go's `g.runGitCommand(g.repoPath, "worktree", "add", g.worktreePath, g.branchName)`) and returning the same result shape (an error only — the worktree's path is already known to the caller via w.WorktreePath, exactly as GitWorktree.Setup() returns only an error since the path is already stored on the receiver).
Before running `git worktree add`, this checks that w.WorktreePath's parent directory ("base_path") exists via `test -d <base_path>`, returning ErrRemoteBasePathMissing (not a generic git error) if it does not. This check is advisory only, not atomic with the add that follows (see ErrRemoteBasePathMissing's doc comment) — it exists to give a fast, distinguishable error for the common case (base_path never existed / was never mounted), not as a guarantee against a race with something removing it in between.
func (*RemoteWorktreeOps) InitializeProjectDirectory ¶ added in v1.47.0
func (r *RemoteWorktreeOps) InitializeProjectDirectory(ctx context.Context, projectPath string) error
InitializeProjectDirectory mirrors session.SessionTypeNewProject's local init flow (InitializeProjectDirectory in util.go, driven by session/instance_worktree.go's setupFirstTimeWorktree) on a remote host: a no-op if projectPath is already a git repo, otherwise create the directory, `git init`, and make an initial commit (git worktrees need at least one commit to exist before a worktree can be added against them).
The whole flow is shelled as a single `sh -c <script>` CommandRunner.Run call rather than several separate Run calls, for two reasons: (1) writing .gitignore's content requires shell output redirection ('> .gitignore'), which CommandRunner.Run cannot express directly — Run has no stdin parameter to pipe content through (see command_runner.go's doc comment: only Start is piped, and only for the one long-lived control-mode case), and buildRemoteCommand shell-quotes each argument individually, so passing '>' as a bare Run argument would reach the remote as a literal, inert string rather than a shell operator; (2) it keeps this to one round trip against what may be a slow remote link, rather than five.
The already-a-repo check tests for a ".git" entry directly under projectPath (`[ -e .git ]` after cd, not `-d`: a git worktree's ".git" is a *file* containing a `gitdir:` pointer, not a directory) rather than `git rev-parse --is-inside-work-tree`, which walks UP the directory tree looking for the nearest .git — that would wrongly report "already a repo" for a projectPath nested inside some unrelated ancestor repository. This matches go-git's PlainOpen(path) semantics (no DetectDotGit, so no upward search; and PlainOpen itself accepts either a .git directory or a .git file), which is what the local InitializeProjectDirectory uses for its own no-op check.
Deliberately simpler than the local flow's rollback-on-failure behavior (which os.RemoveAll's the directory it created if `git init` or the initial commit fails): there is no local os.RemoveAll equivalent here cheap enough to add safely — embedding an `rm -rf` in a remote script is exactly the kind of command this package should be conservative about — and there is no production caller of this method yet (Epic 2.2 wires no caller; Phase 4 is where remote SessionTypeNewProject support would consume it). A failed remote init can leave a partially-initialized directory behind for a caller/operator to inspect or retry against.
func (*RemoteWorktreeOps) RemoveWorktree ¶ added in v1.47.0
func (r *RemoteWorktreeOps) RemoveWorktree(ctx context.Context, w RemoteWorktree) error
RemoveWorktree runs `git worktree remove <WorktreePath> --force` on the remote host via the injected CommandRunner, mirroring CreateWorktree's argument construction. Callers use this as best-effort compensating cleanup (Task 4.2.1e's partial-failure handling, adversarial-review.md Blocker 3: if remote tmux setup fails after CreateWorktree already succeeded, or the SSH connection drops between the two steps, the orchestrating caller attempts this before surfacing the *original* error) — "best-effort" describes how a caller should treat this method's return value (log and continue, don't let a cleanup failure mask the real error), not behavior internal to this method itself, which always reports its own failures rather than swallowing them.
type ShippedCommit ¶ added in v1.39.0
type ShippedCommit struct {
SHA string
Summary string // first line of the commit message
AuthorAt time.Time
AuthorName string
}
ShippedCommit describes one commit in the range shipped by a work session.
func CommitInfo ¶ added in v1.41.0
func CommitInfo(repoPath, sha string) (ShippedCommit, error)
CommitInfo returns the summary line, author and author timestamp for a single resolved commit hash in the repo at repoPath, read via go-git — no subshell (the `prefer-go-git-over-subshells` skill).
func ListShippedCommits ¶ added in v1.39.0
func ListShippedCommits(ctx context.Context, repoPath, baseSHA, headSHA string) (commits []ShippedCommit, truncated bool, err error)
ListShippedCommits returns the commits reachable from headSHA but not from baseSHA — i.e. what a work session's commit range actually shipped — newest first, like a PR's "Commits" tab. Both SHAs must already be resolved commit hashes (not branch names): the caller typically has these directly from GitWorktreeData.BaseCommitSHA and the work session's LastCommitSha, which remain valid even after the branch itself has been deleted post-merge. truncated reports whether the walk hit listShippedCommitsCap before exhausting the range — the caller may want to surface that a longer list exists than what's returned.
type ThreeWayFileMerger ¶ added in v1.53.0
type ThreeWayFileMerger struct{}
ThreeWayFileMerger runs the diff3 hunk-reconciliation algorithm over one path's base/ours/theirs content, producing merged content or a conflict classification. It holds no state — a zero value is ready to use.
func (ThreeWayFileMerger) Merge ¶ added in v1.53.0
func (ThreeWayFileMerger) Merge(base, ours, theirs string) (*MergeResult, error)
Merge runs the diff3 hunk-reconciliation algorithm over one file's base/ours/theirs content (Story 3.2.2). It returns an error rather than panicking or silently truncating when content isn't valid UTF-8 and wasn't already filtered out by the binary heuristic (MergeFile) — splitting invalid UTF-8 into "lines" can otherwise corrupt line boundaries silently.
func (ThreeWayFileMerger) MergeFile ¶ added in v1.53.0
func (m ThreeWayFileMerger) MergeFile(in FileMergeInput) (*FileMergeOutcome, error)
MergeFile classifies one path's three-way merge, short-circuiting to a conflict reason before attempting any content merge for gitlink (Task 3.2.3c), mode-only (Task 3.2.3a), and binary (Task 3.2.3b) cases — in that order, since a gitlink entry's value is a commit SHA, not diffable text, and must never reach lineDiff/mode-diff/binary-diff at all.
func (ThreeWayFileMerger) ReconcilePathChange ¶ added in v1.53.0
func (m ThreeWayFileMerger) ReconcilePathChange(pc *PathChange) (*MergeResult, error)
ReconcilePathChange resolves one path's PathChange (grouped from TreeDiffPair's raw, rename-uncorrelated object.Changes) to a MergeResult.
A Delete on either side leaves nothing on that side to diff against base for this path; per the "Rename+modify merge collision" Pattern Decision this project does not chase where that content went (a rename elsewhere) — it simply keeps whichever side still has content, without flagging a conflict. This is a defensive fallback: go-git's own default rename detection (RenameScore 60, already active in TreeDiffPair's Tree.Diff calls) folds a near-identical rename into a single Modify keyed by the new path before this function ever sees it, so this branch only fires for a dissimilar rename go-git didn't correlate.
type WorktreePath ¶ added in v1.53.0
type WorktreePath string
WorktreePath is an absolute filesystem path to a worktree's working directory.
Source Files
¶
- diff.go
- drift.go
- native_admin_writer.go
- native_merge.go
- native_merge_base.go
- native_merge_conflict.go
- native_merge_diff3.go
- native_merge_index.go
- native_merge_threeway.go
- native_rollout.go
- native_worktree_add.go
- native_worktree_common.go
- native_worktree_list.go
- native_worktree_prune.go
- native_worktree_remove.go
- ops.go
- remote_worktree.go
- scaffolding.go
- util.go
- worktree.go
- worktree_branch.go
- worktree_git.go
- worktree_lock.go
- worktree_ops.go