git

package
v4.8.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 6, 2026 License: MIT Imports: 7 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

View Source
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

func EnableRerere(repoDir string) error

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

func MirrorSubmodules(sourcePath, destPath string) error

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

func ValidateBranchName(name string) error

ValidateBranchName checks if a name is valid for a git branch.

Types

type Commit

type Commit struct {
	Hash    string
	Subject string
	Author  string
}

Commit represents a git commit

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 New

func New(repoDir string) *Git

New creates a new Git wrapper for the given repo directory

func (*Git) AddRemote

func (g *Git) AddRemote(name, url string) error

AddRemote adds a new git remote

func (*Git) BranchExists

func (g *Git) BranchExists(branch string) bool

BranchExists checks if a local branch exists

func (*Git) BranchFromRebaseState added in v4.8.0

func (g *Git) BranchFromRebaseState() string

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

func (g *Git) CheckoutBranch(branchName string) error

CheckoutBranch switches to an existing branch

func (*Git) CreateBranchOnly

func (g *Git) CreateBranchOnly(branchName, baseBranch string) error

CreateBranchOnly creates a new branch without a worktree

func (*Git) CreateWorktree

func (g *Git) CreateWorktree(branchName, worktreePath, baseBranch string) error

CreateWorktree creates a new worktree

func (*Git) CreateWorktreeFromRemoteBranch

func (g *Git) CreateWorktreeFromRemoteBranch(localBranch, worktreePath string) error

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

func (g *Git) CurrentBranch() (string, error)

CurrentBranch returns the current branch name

func (*Git) DeleteBranch

func (g *Git) DeleteBranch(branchName string, force bool) error

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) Fetch

func (g *Git) Fetch() error

Fetch fetches from origin remote

func (*Git) FetchRemote

func (g *Git) FetchRemote(remote string) error

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

func (g *Git) FindAnyEzstackStash() (int, bool)

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

func (g *Git) FindEzstackStash(branchName string) (int, bool)

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

func (g *Git) FindRemoteByOwner(owner string) (string, string, error)

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

func (g *Git) GetAheadBehind(local, remote string) (ahead, behind int, err error)

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

func (g *Git) GetBranchCommit(branch string) (string, error)

GetBranchCommit gets the commit hash of a branch

func (*Git) GetCommitCount

func (g *Git) GetCommitCount(base, head string) (int, error)

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

func (g *Git) GetCommitsAhead(branch, target string) (int, error)

GetCommitsAhead returns the number of commits branch is ahead of target

func (*Git) GetCommitsBehind

func (g *Git) GetCommitsBehind(branch, target string) (int, error)

GetCommitsBehind returns the number of commits branch is behind target

func (*Git) GetCommitsBetween

func (g *Git) GetCommitsBetween(base, head string) ([]Commit, error)

GetCommitsBetween returns commits between base and head (exclusive of base)

func (*Git) GetDiffStat

func (g *Git) GetDiffStat(base, head string) (added int, removed int, err error)

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

func (g *Git) GetLastCommitMessage() (string, error)

GetLastCommitMessage returns the message of the last commit on the current branch

func (*Git) GetLastCommitMessageOf added in v4.8.0

func (g *Git) GetLastCommitMessageOf(branch string) (string, error)

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

func (g *Git) GetMainWorktree() (string, error)

GetMainWorktree returns the path to the main worktree

func (*Git) GetMergeBase

func (g *Git) GetMergeBase(branch1, branch2 string) (string, error)

GetMergeBase finds the common ancestor between two branches

func (*Git) GetPRTemplate

func (g *Git) GetPRTemplate() string

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) GetRemote

func (g *Git) GetRemote(name string) (string, error)

GetRemote gets the remote URL

func (*Git) GetRepoRoot

func (g *Git) GetRepoRoot() (string, error)

GetRepoRoot returns the root directory of the git repository

func (*Git) HasChanges

func (g *Git) HasChanges() (bool, error)

HasChanges returns true if the working directory has uncommitted changes

func (*Git) HasDivergedFromRemote added in v4.8.0

func (g *Git) HasDivergedFromRemote(branch, remote string) (bool, int, int, error)

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

func (g *Git) HasUnresolvedConflicts() (bool, error)

HasUnresolvedConflicts checks if there are unresolved merge conflicts

func (*Git) InitSubmodules added in v4.6.0

func (g *Git) InitSubmodules(paths []string) error

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

func (g *Git) IsAncestor(ancestor, descendant string) (bool, error)

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

func (g *Git) IsBranchMerged(branch, target string) (bool, error)

IsBranchMerged checks if a branch has been merged into target

func (*Git) IsLocalAheadOfRemote

func (g *Git) IsLocalAheadOfRemote(branch string, remote string) (bool, error)

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

func (g *Git) IsMergeInProgress() (bool, error)

IsMergeInProgress checks if a merge is in progress

func (*Git) IsRebaseInProgress

func (g *Git) IsRebaseInProgress() (bool, error)

IsRebaseInProgress checks if a rebase is in progress

func (*Git) ListInitializedSubmodules added in v4.6.0

func (g *Git) ListInitializedSubmodules() ([]string, error)

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

func (g *Git) ListLocalBranches() ([]string, error)

ListLocalBranches returns all local branch names

func (*Git) ListWorktrees

func (g *Git) ListWorktrees() ([]Worktree, error)

ListWorktrees lists all worktrees

func (*Git) Merge

func (g *Git) Merge(target string) error

Merge merges target into the current branch interactively

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

func (g *Git) PruneWorktrees() error

PruneWorktrees prunes stale worktree metadata from git

func (*Git) Push deprecated

func (g *Git) Push(force bool, remote ...string) error

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

func (g *Git) PushBranch(branch string, force bool, remote ...string) error

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

func (g *Git) PushBranchSetUpstream(branch string, remote ...string) error

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

func (g *Git) PushForce(remote ...string) error

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

func (g *Git) PushForceBranch(branch string, remote ...string) error

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) Rebase

func (g *Git) Rebase(target string) error

Rebase rebases current branch onto target

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

func (g *Git) RebaseOnto(newBase, oldBase string) error

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

func (g *Git) RefExists(ref string) bool

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

func (g *Git) RemoteBranchExists(branch string) bool

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

func (g *Git) RemoteHasBranch(remote, branch string) bool

RemoteHasBranch checks if `branch` exists on the given remote. Defaults to `origin` if remote is empty.

func (*Git) RemoveWorktree

func (g *Git) RemoveWorktree(worktreePath string, deleteBranch bool, branchName string) error

RemoveWorktree removes a worktree and optionally deletes the branch

func (*Git) ResetHard

func (g *Git) ResetHard(ref string) error

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

func (g *Git) RunCapture(args ...string) (string, error)

run executes a git command and returns the output RunCapture executes a git command and returns stdout.

func (*Git) RunInteractive

func (g *Git) RunInteractive(args ...string) error

RunInteractive runs a git command interactively (for rebase with conflicts)

func (*Git) StashPop

func (g *Git) StashPop() error

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

func (g *Git) StashPopIndex(index int) error

StashPopIndex pops a specific stash entry by index.

func (*Git) StashPush

func (g *Git) StashPush() error

StashPush stashes all changes including untracked files

type RebaseResult

type RebaseResult struct {
	Success     bool
	HasConflict bool
	Error       error
}

RebaseResult contains the result of a rebase operation

type Worktree

type Worktree struct {
	Path   string
	Branch string
}

Worktree represents a git worktree

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL