Documentation
¶
Index ¶
- Variables
- func EnableRerere(repoDir string) error
- func MirrorSubmodules(sourcePath, destPath string) error
- func ValidateBranchName(name string) error
- type Commit
- type ContinueResult
- type Git
- func (g *Git) AddRemote(name, url string) error
- func (g *Git) BranchExists(branch string) bool
- func (g *Git) BranchFromRebaseState() string
- func (g *Git) CheckoutBranch(branchName string) error
- func (g *Git) CreateBranchOnly(branchName, baseBranch string) error
- func (g *Git) CreateWorktree(branchName, worktreePath, baseBranch string) error
- func (g *Git) CreateWorktreeFromRemoteBranch(localBranch, worktreePath string) error
- func (g *Git) CurrentBranch() (string, error)
- func (g *Git) DeleteBranch(branchName string, force bool) error
- func (g *Git) FastForwardMerge(target string) RebaseResult
- func (g *Git) Fetch() error
- func (g *Git) FetchRemote(remote string) error
- func (g *Git) FindAnyEzstackStash() (int, bool)
- func (g *Git) FindEzstackStash(branchName string) (int, bool)
- func (g *Git) FindRemoteByOwner(owner string) (string, string, error)
- func (g *Git) GetAheadBehind(local, remote string) (ahead, behind int, err error)
- func (g *Git) GetBranchCommit(branch string) (string, error)
- func (g *Git) GetCommitCount(base, head string) (int, error)
- func (g *Git) GetCommitsAhead(branch, target string) (int, error)
- func (g *Git) GetCommitsBehind(branch, target string) (int, error)
- func (g *Git) GetCommitsBetween(base, head string) ([]Commit, error)
- func (g *Git) GetDiffStat(base, head string) (added int, removed int, err error)
- func (g *Git) GetLastCommitMessage() (string, error)
- func (g *Git) GetLastCommitMessageOf(branch string) (string, error)
- func (g *Git) GetMainWorktree() (string, error)
- func (g *Git) GetMergeBase(branch1, branch2 string) (string, error)
- func (g *Git) GetPRTemplate() string
- func (g *Git) GetRemote(name string) (string, error)
- func (g *Git) GetRepoRoot() (string, error)
- func (g *Git) HasChanges() (bool, error)
- func (g *Git) HasDivergedFromRemote(branch, remote string) (bool, int, int, error)
- func (g *Git) HasUnresolvedConflicts() (bool, error)
- func (g *Git) InitSubmodules(paths []string) error
- func (g *Git) IsAncestor(ancestor, descendant string) (bool, error)
- func (g *Git) IsBranchMerged(branch, target string) (bool, error)
- func (g *Git) IsLocalAheadOfRemote(branch string, remote string) (bool, error)
- func (g *Git) IsMergeInProgress() (bool, error)
- func (g *Git) IsRebaseInProgress() (bool, error)
- func (g *Git) ListInitializedSubmodules() ([]string, error)
- func (g *Git) ListLocalBranches() ([]string, error)
- func (g *Git) ListWorktrees() ([]Worktree, error)
- func (g *Git) Merge(target string) error
- func (g *Git) MergeContinue() ContinueResult
- func (g *Git) MergeNonInteractive(target string) RebaseResult
- func (g *Git) PruneWorktrees() error
- func (g *Git) Push(force bool, remote ...string) errordeprecated
- func (g *Git) PushBranch(branch string, force bool, remote ...string) error
- func (g *Git) PushBranchSetUpstream(branch string, remote ...string) error
- func (g *Git) PushForce(remote ...string) errordeprecated
- func (g *Git) PushForceBranch(branch string, remote ...string) error
- func (g *Git) Rebase(target string) error
- func (g *Git) RebaseContinue() ContinueResult
- func (g *Git) RebaseNonInteractive(target string) RebaseResult
- func (g *Git) RebaseOnto(newBase, oldBase string) error
- func (g *Git) RebaseOntoNonInteractive(newBase, oldBase string) RebaseResult
- func (g *Git) RefExists(ref string) bool
- func (g *Git) RemoteBranchExists(branch string) bool
- func (g *Git) RemoteHasBranch(remote, branch string) bool
- func (g *Git) RemoveWorktree(worktreePath string, deleteBranch bool, branchName string) error
- func (g *Git) ResetHard(ref string) error
- func (g *Git) RunCapture(args ...string) (string, error)
- func (g *Git) RunInteractive(args ...string) error
- func (g *Git) StashPop() error
- func (g *Git) StashPopIndex(index int) error
- func (g *Git) StashPush() error
- type RebaseResult
- type Worktree
Constants ¶
This section is empty.
Variables ¶
var ProgressStart func(message string) (stop func())
ProgressStart, when non-nil, is invoked by long-running git operations (rebase, merge, fetch, worktree add) to report a delayed progress indicator. It returns a stop function the operation calls when work completes (success or failure). When nil — the default — every call site is a no-op, which is what tests, the MCP binary, and any other non-interactive consumer want.
Why a hook here rather than importing internal/ui directly: internal/git is a leaf in the package layering — internal/config, internal/stack, and the entire CLI depend on it. Importing ui would force ui's surface (term, spinner, ANSI codes) onto every transitive dependent, including non-interactive callers that have no use for spinners. The cmd/ezs binary wires this hook in main; everything else stays silent without ceremony.
Functions ¶
func EnableRerere ¶ added in v4.8.0
EnableRerere ensures `rerere.enabled` and `rerere.autoupdate` are set on the repository. Idempotent: it only writes when a setting is unset at the repo-local scope, so a user who has explicitly set `rerere.enabled = false` in this repo's config keeps that value untouched. A user who has set it to `false` in their *global* config (~/.gitconfig) WILL be overridden here — the repo-local true wins by git config precedence — which matches the intent of opt-in scoped behaviour for ezstack-managed repos.
rerere ("reuse recorded resolution") records the conflict-and-resolution hunks the first time a conflict is resolved, then auto-applies that resolution on subsequent rebases that re-encounter the same textual conflict. In a stacked workflow this catches several edge cases the `--onto oldBase` fix doesn't:
- The user runs `git rebase --abort` and retries; rerere replays their prior resolution.
- Two siblings touch the same hunk, so each independently re-conflicts against an updated parent — rerere replays after the first manual fix.
- The cross-stack-parent fallback path in syncStackInternal where no PreSyncCommit is available.
Written at the repo level (not global) so it stays scoped to the project.
func MirrorSubmodules ¶ added in v4.6.0
MirrorSubmodules initializes in destPath the same submodules that are currently initialized in sourcePath. No-op when sourcePath has no initialized submodules. destPath must be a checked-out worktree of the same repository (submodules are per-worktree in Git).
func ValidateBranchName ¶
ValidateBranchName checks if a name is valid for a git branch.
Types ¶
type ContinueResult ¶ added in v4.8.0
type ContinueResult struct {
// Done is true when no rebase or merge is in progress after the continue.
Done bool
// StillInConflict is true when the operation paused on another conflict
// (a multi-step rebase whose next commit also has merge conflicts).
StillInConflict bool
// Err is set when the underlying git command failed for a reason that
// isn't a paused-on-conflict — bad state, invalid args, IO error, etc.
Err error
// Stderr captures git's diagnostic output for callers that want to surface
// it on Err or StillInConflict.
Stderr string
}
ContinueResult classifies the outcome of `git rebase --continue` / `git commit --no-edit` invoked to resume an in-progress rebase or merge.
A multi-step rebase can pause again on the next commit's conflict, so a successful exit code from `git rebase --continue` doesn't necessarily mean the rebase is complete. Callers must distinguish "fully done" from "paused again" so they don't push an unfinished branch or re-enter children whose parent isn't actually rebased yet.
type Git ¶
type Git struct {
RepoDir string
}
Git wraps git operations
func (*Git) BranchExists ¶
BranchExists checks if a local branch exists
func (*Git) BranchFromRebaseState ¶ added in v4.8.0
BranchFromRebaseState returns the branch name recorded in this repo's rebase-state files (.git/rebase-merge/head-name or .git/rebase-apply/head-name). During a rebase HEAD is detached, so `git rev-parse --abbrev-ref HEAD` returns "HEAD" — but ezstack still needs to know which branch the worktree is "really" on so commands like `ezs sync -s --continue` can resolve the current stack. Returns "" when no rebase is in progress or the file can't be read.
func (*Git) CheckoutBranch ¶
CheckoutBranch switches to an existing branch
func (*Git) CreateBranchOnly ¶
CreateBranchOnly creates a new branch without a worktree
func (*Git) CreateWorktree ¶
CreateWorktree creates a new worktree
func (*Git) CreateWorktreeFromRemoteBranch ¶
CreateWorktreeFromRemoteBranch creates a worktree for a remote branch with tracking. If the local branch already exists (and has no worktree), it is force-updated to match the remote. The resulting local branch tracks origin/<localBranch>.
func (*Git) CurrentBranch ¶
CurrentBranch returns the current branch name
func (*Git) DeleteBranch ¶
DeleteBranch deletes a local git branch
func (*Git) FastForwardMerge ¶ added in v4.8.0
func (g *Git) FastForwardMerge(target string) RebaseResult
FastForwardMerge runs `git merge --ff-only target` in the current repo. Used by sync's remote-integration step to advance a branch to its remote's new tip when the local has no diverging commits. Returns RebaseResult so callers can use the same conflict-handling shape as RebaseNonInteractive.
The --ff-only flag rejects any merge that would require a real merge commit, so this is safe to call even when the caller hasn't pre-checked divergence — git will refuse rather than create unexpected history.
func (*Git) FetchRemote ¶
FetchRemote fetches from a specific named remote (e.g. a fork). Used by integrate flows for non-origin remotes; the default Fetch() only handles origin to avoid hanging on unreachable remotes by default. Adds --prune so deleted remote branches stop showing as tracking refs.
func (*Git) FindAnyEzstackStash ¶
FindAnyEzstackStash returns the most recent stash entry tagged "ezstack-autostash" regardless of branch. Used only in the detached-HEAD fallback path where the current branch is unknown.
func (*Git) FindEzstackStash ¶
FindEzstackStash finds the stash index of an ezstack autostash entry for a specific branch. Git stash entries look like: "stash@{N}: On <branch>: ezstack-autostash" Returns (index, true) if found, (-1, false) if not found. Matches both branch name and message to avoid touching stashes from other worktrees.
func (*Git) FindRemoteByOwner ¶
FindRemoteByOwner finds a git remote that points to a repo owned by the given GitHub owner. Returns the remote name and URL, or empty strings if not found.
func (*Git) GetAheadBehind ¶ added in v4.8.0
GetAheadBehind returns the divergence between two refs in a single git invocation: ahead = commits in `local` not reachable from `remote`, behind = commits in `remote` not reachable from `local`.
Use this instead of two GetCommitsAhead/GetCommitsBehind calls when you need both numbers — git computes them simultaneously here and the result is consistent against a single snapshot of the object database.
func (*Git) GetBranchCommit ¶
GetBranchCommit gets the commit hash of a branch
func (*Git) GetCommitCount ¶
GetCommitCount returns the number of commits between base and head This is useful to check if a branch has any commits of its own
func (*Git) GetCommitsAhead ¶
GetCommitsAhead returns the number of commits branch is ahead of target
func (*Git) GetCommitsBehind ¶
GetCommitsBehind returns the number of commits branch is behind target
func (*Git) GetCommitsBetween ¶
GetCommitsBetween returns commits between base and head (exclusive of base)
func (*Git) GetDiffStat ¶
GetDiffStat returns the total lines added and removed between base and head. Uses three-dot diff (merge-base) to show only changes introduced by head, not changes on base that head doesn't have.
func (*Git) GetLastCommitMessage ¶
GetLastCommitMessage returns the message of the last commit on the current branch
func (*Git) GetLastCommitMessageOf ¶ added in v4.8.0
GetLastCommitMessageOf returns the message of the tip commit on a specific branch ref. Companion to GetLastCommitMessage for callers like `pr create --branch <other>` where HEAD is not the branch the PR is for.
func (*Git) GetMainWorktree ¶
GetMainWorktree returns the path to the main worktree
func (*Git) GetMergeBase ¶
GetMergeBase finds the common ancestor between two branches
func (*Git) GetPRTemplate ¶
GetPRTemplate finds and reads the GitHub PR template from common locations. Returns the template content or empty string if no template is found. GitHub looks for templates in these locations (in order of priority): - .github/pull_request_template.md - .github/PULL_REQUEST_TEMPLATE.md - docs/pull_request_template.md - pull_request_template.md - PULL_REQUEST_TEMPLATE.md
func (*Git) GetRepoRoot ¶
GetRepoRoot returns the root directory of the git repository
func (*Git) HasChanges ¶
HasChanges returns true if the working directory has uncommitted changes
func (*Git) HasDivergedFromRemote ¶ added in v4.8.0
HasDivergedFromRemote checks if local and remote branches have diverged. Returns (hasDiverged, localAhead, remoteBehind, error). hasDiverged is true only when local has commits not in remote AND remote has commits not in local. If the remote ref doesn't exist yet, returns (false, 0, 0, nil) — the branch just hasn't been pushed. Other rev-parse failures are surfaced as errors so callers can distinguish "no remote yet" from real repo problems. If remote is empty, defaults to "origin".
Uses a single `git rev-list --left-right --count` invocation so the ahead/behind counts are computed against one consistent snapshot of the object database. The previous two-call form (GetCommitsAhead + GetCommitsBehind) could see a stale upper bound if a concurrent fetch / prune landed between calls.
func (*Git) HasUnresolvedConflicts ¶
HasUnresolvedConflicts checks if there are unresolved merge conflicts
func (*Git) InitSubmodules ¶ added in v4.6.0
InitSubmodules runs `git submodule update --init -- <paths>...` in g.RepoDir. Passing an empty paths slice is a no-op.
func (*Git) IsAncestor ¶ added in v4.8.0
IsAncestor checks whether `ancestor` is reachable from `descendant`. Wraps `git merge-base --is-ancestor`. Returns (false, nil) when the answer is no (exit code 1) and (false, err) for any other failure (bad refs, etc.).
Used by lookupPreSyncSHA to detect snapshots that have been invalidated by out-of-band history rewrites (e.g. a manual `git rebase` between ezstack runs): if the recorded SHA is no longer reachable from the branch tip, git's `--onto staleSHA` would compute a bogus commit range, so the caller must discard the snapshot and fall back to plain rebase.
func (*Git) IsBranchMerged ¶
IsBranchMerged checks if a branch has been merged into target
func (*Git) IsLocalAheadOfRemote ¶
IsLocalAheadOfRemote checks if the local branch has commits not in the remote. If remote is empty, defaults to "origin". Returns true if local is ahead (needs push), false if in sync or behind.
Distinguishes "remote ref doesn't exist" (treat as ahead — first push) from transient or unrelated rev-parse failures (lock contention, repo corruption, bad ref name) which are surfaced as errors. Returning (true, nil) on any error would let downstream code push to a remote ref that may not be the one we expected.
func (*Git) IsMergeInProgress ¶
IsMergeInProgress checks if a merge is in progress
func (*Git) IsRebaseInProgress ¶
IsRebaseInProgress checks if a rebase is in progress
func (*Git) ListInitializedSubmodules ¶ added in v4.6.0
ListInitializedSubmodules returns the paths of submodules that are currently initialized in g.RepoDir. An empty slice is returned if the repo has no submodules or no submodules are initialized.
`git submodule status` output format:
<sha> <path> [(<desc>)] -> initialized and checked out -<sha> <path> -> not initialized +<sha> <path> [(<desc>)] -> initialized, SHA differs from recorded commit U<sha> <path> -> merge conflicts (treat as initialized)
We treat everything except '-' as initialized.
func (*Git) ListLocalBranches ¶
ListLocalBranches returns all local branch names
func (*Git) ListWorktrees ¶
ListWorktrees lists all worktrees
func (*Git) MergeContinue ¶
func (g *Git) MergeContinue() ContinueResult
MergeContinue commits the resolved merge in progress. See ContinueResult. Multi-conflict merges aren't possible (a merge is a single commit), but the shared shape lets the caller treat both flows uniformly.
func (*Git) MergeNonInteractive ¶
func (g *Git) MergeNonInteractive(target string) RebaseResult
MergeNonInteractive merges target into the current branch without interactive mode Returns structured result for conflict handling, matching RebaseResult for compatibility
func (*Git) PruneWorktrees ¶
PruneWorktrees prunes stale worktree metadata from git
func (*Git) Push
deprecated
Push pushes the current branch to the specified remote. If remote is empty, defaults to "origin".
Deprecated: prefer PushBranch(branchName, force, remote). Push consults CurrentBranch() at call time, which means after a transient checkout (e.g. the syncViaCheckout codepath that restores HEAD to main) it will push the wrong branch. New callers should pass an explicit branch name.
func (*Git) PushBranch ¶
PushBranch pushes a specific branch to a remote (not necessarily the current branch). If remote is empty, defaults to "origin".
func (*Git) PushBranchSetUpstream ¶ added in v4.8.0
PushBranchSetUpstream pushes a specific branch with -u (set upstream). Variant of PushBranch for the first-push case where the remote ref doesn't exist yet. Necessary because callers like `pr create --branch <other>` need to push by explicit branch name (not whatever HEAD happens to be). If remote is empty, defaults to "origin".
func (*Git) PushForce
deprecated
PushForce force pushes the current branch with lease (safer than --force). If remote is empty, defaults to "origin".
Deprecated: prefer PushForceBranch(branch, remote). PushForce consults CurrentBranch() at call time, which is wrong when the active checkout has been transiently swapped (e.g. syncViaCheckout restored HEAD to main) or when the caller acts on a branch other than HEAD.
func (*Git) PushForceBranch ¶ added in v4.8.0
PushForceBranch force-pushes a specific branch with lease (safer than --force). If remote is empty, defaults to "origin". Prefer this over PushForce — it never reads CurrentBranch().
func (*Git) RebaseContinue ¶
func (g *Git) RebaseContinue() ContinueResult
RebaseContinue resumes an in-progress rebase. See ContinueResult for the classification semantics.
func (*Git) RebaseNonInteractive ¶
func (g *Git) RebaseNonInteractive(target string) RebaseResult
RebaseNonInteractive rebases current branch onto target without interactive mode Returns structured result instead of just error for better conflict handling
func (*Git) RebaseOnto ¶ added in v4.8.0
RebaseOnto interactively rebases commits in `oldBase..HEAD` onto newBase. Used by stack-aware callers that know the OLD parent SHA the current branch was sitting on top of, so they can avoid replaying the parent's own commits when the parent has been rewritten. When oldBase equals newBase this is equivalent to plain `git rebase newBase`.
func (*Git) RebaseOntoNonInteractive ¶
func (g *Git) RebaseOntoNonInteractive(newBase, oldBase string) RebaseResult
RebaseOntoNonInteractive rebases commits from oldBase to current onto newBase Returns structured result for better conflict handling
func (*Git) RefExists ¶ added in v4.8.0
RefExists checks whether an arbitrary git ref resolves to a real object in the repository. Useful for symbolic refs (branches, tags). Combines `rev-parse --verify` (parses the ref to a SHA) with `cat-file -e` (proves the SHA names an actual object) — `rev-parse --verify` alone accepts any 40-char hex string as a "valid" SHA even when the object doesn't exist, which is too lenient for snapshot validity checks.
func (*Git) RemoteBranchExists ¶
RemoteBranchExists checks if a branch exists on `origin`. Use RemoteHasBranch when the remote may not be `origin` (e.g. fork remotes).
func (*Git) RemoteHasBranch ¶ added in v4.8.0
RemoteHasBranch checks if `branch` exists on the given remote. Defaults to `origin` if remote is empty.
func (*Git) RemoveWorktree ¶
RemoveWorktree removes a worktree and optionally deletes the branch
func (*Git) ResetHard ¶
ResetHard performs a hard reset to the given ref This is used to fast-forward a branch that has no commits of its own
func (*Git) RunCapture ¶
run executes a git command and returns the output RunCapture executes a git command and returns stdout.
func (*Git) RunInteractive ¶
RunInteractive runs a git command interactively (for rebase with conflicts)
func (*Git) StashPop ¶
StashPop pops the ezstack autostash entry for the current branch. Uses targeted lookup to avoid popping user stashes or stashes from other branches. Returns nil if no ezstack stash is found (nothing to pop).
func (*Git) StashPopIndex ¶
StashPopIndex pops a specific stash entry by index.
type RebaseResult ¶
RebaseResult contains the result of a rebase operation