gitstate

package
v0.7.0 Latest Latest
Warning

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

Go to latest
Published: Jul 11, 2026 License: AGPL-3.0, AGPL-3.0-only Imports: 17 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

View Source
var ErrDetachedHEAD = errors.New("HEAD is not on a branch — checkout a named branch first")

ErrDetachedHEAD is returned when HEAD is not on a named branch.

View Source
var ErrDirtyWorktree = errors.New("worktree has uncommitted changes — commit or stash first")

ErrDirtyWorktree is returned when uncommitted changes are present before replay.

View Source
var ErrNoUpstream = errors.New("branch has no upstream tracking ref — run: git branch --set-upstream-to=<remote>/<branch>")

ErrNoUpstream is returned when the current branch has no tracking upstream configured.

View Source
var ErrUpstreamMoved = errors.New("upstream ref changed between fetch and push — retry from fetch")

ErrUpstreamMoved is returned when the remote ref changes between fetch and push.

Functions

func AllChangedFiles

func AllChangedFiles(s git.Status) []string

AllChangedFiles returns paths with any modification (staged, unstaged, or untracked).

func AllDirtyPaths

func AllDirtyPaths(s git.Status) []string

AllDirtyPaths returns paths with staged or unstaged modifications, excluding untracked files. Use for bundle/artifact generation where both staged and unstaged changes must be captured. Output is sorted for deterministic ordering.

func CountCommitsBetween

func CountCommitsBetween(repo *git.Repository, fromRef, toRef string) (int, error)

CountCommitsBetween counts commits reachable from `to` but not from `from`. Equivalent to `git rev-list --count <from>..<to>`.

func DetectInProgressOp added in v0.7.0

func DetectInProgressOp(rootDir string) string

DetectInProgressOp reports an in-progress git operation (merge, rebase, cherry-pick, revert) by probing the git directory for the marker files git itself writes. This is how StageFreight stays a first-class git citizen: it recognizes a state git created (mid-merge) and can refuse with guidance instead of silently flattening it.

Best-effort: returns "" when the git directory can't be probed as a plain directory (e.g. a linked worktree whose .git is a file), which is no worse than the pre-detection behavior.

func DiffStats

func DiffStats(repo *git.Repository, fromRef, toRef string) (files, insertions, deletions int, err error)

DiffStats returns file/insertion/deletion counts between two refs.

func ExactTagAtHEAD

func ExactTagAtHEAD(repo *git.Repository) (string, error)

ExactTagAtHEAD returns the tag name if HEAD is exactly at a tagged commit, matching `git describe --tags --exact-match HEAD`. Returns "" if not at a tag.

func HasStagedChanges

func HasStagedChanges(s git.Status) bool

HasStagedChanges returns true when any file has a staged modification.

func HasUnstagedChanges

func HasUnstagedChanges(s git.Status) bool

HasUnstagedChanges returns true when any tracked file has unstaged modifications.

func IsAncestor

func IsAncestor(repo *git.Repository, ancestorRef, descendantRef string) (bool, error)

IsAncestor returns true if ancestorRef is a (possibly indirect) ancestor of descendantRef. Equivalent to `git merge-base --is-ancestor <ancestor> <descendant>`.

func IsClean

func IsClean(s git.Status) bool

IsClean returns true when the status has no modifications of any kind.

func IsCleanIgnoringUntracked added in v0.7.0

func IsCleanIgnoringUntracked(s git.Status) bool

IsCleanIgnoringUntracked reports whether the worktree has no STAGED or unstaged-TRACKED changes, ignoring untracked files. Untracked files are never part of a commit or a push and don't obstruct a fast-forward, so the sync/push state machine must not treat a stray untracked file (a scratch doc, an unrelated new file) as "dirty" and refuse to push a commit that's already made. A rebase that would collide with an untracked file still surfaces its own clear git error.

NOTE: go-git marks an untracked file as {Staging: Untracked, Worktree: Untracked} — the Staging code is Untracked, NOT Unmodified — so this must exclude Untracked in BOTH the staged and unstaged dimensions. (HasStagedChanges alone counts untracked as staged.)

func IsSSHURL

func IsSSHURL(url string) bool

IsSSHURL is the exported form of isSSHURL for use by other packages.

func ListTagsSorted

func ListTagsSorted(repo *git.Repository) ([]string, error)

ListTagsSorted returns all tags sorted by version descending, matching git's --sort=-version:refname behaviour. Semver tags are sorted by semver; non-semver tags are sorted lexicographically after all semver tags.

Parity with git: Masterminds/semver parses the same set of tags as git's version-aware sort for real-world repos (semver + v-prefix). Both classify tags as version-like or lexicographic using equivalent rules, and both produce version-like tags before lexicographic tags in descending order. Edge case: tags with identical version values but different prefixes (v1.0.0 and 1.0.0) are sorted stably by the sort.Slice guarantee — same as git.

func OpenRepo

func OpenRepo(rootDir string) (*git.Repository, error)

OpenRepo is the single entry point for all *git.Repository instances. No package outside src/gitstate/ or src/commit/ may call git.PlainOpen directly. DetectDotGit walks parent directories to find .git, matching git CLI behaviour. EnableDotGitCommonDir resolves a LINKED WORKTREE's `.git` file (gitdir: …/.git/worktrees/<name>) against the shared common dir, so objects and refs are read/written to the real store — not the worktree's private dir. Without it a commit made in a worktree is silently MISPLACED (a hash is returned but the object never enters the shared store and the branch is left dangling). Harmless for a normal repo, whose common dir IS its git dir.

func ParseCommitLog

func ParseCommitLog(repo *git.Repository, fromRef, toRef string) ([]*object.Commit, error)

ParseCommitLog returns commits in the range fromRef..toRef as (hash, subject, body, author) tuples.

func RemoteRefHash

func RemoteRefHash(repo *git.Repository, remoteName, refName string, auth transport.AuthMethod) (plumbing.Hash, error)

RemoteRefHash returns the hash of a specific ref on the remote. Equivalent to `git ls-remote origin refs/heads/<branch>`. Requires network access.

func RemoteURL

func RemoteURL(repo *git.Repository, remoteName string) (string, error)

RemoteURL returns the URL for the given remote (typically "origin").

func RepoRoot

func RepoRoot(repo *git.Repository) (string, error)

RepoRoot returns the absolute path of the repository root directory (the directory containing .git). Use this instead of wt.Filesystem.Root() directly — encapsulates the go-git worktree filesystem contract in one place.

func RequireAttached

func RequireAttached(state RepoState) error

RequireAttached returns ErrDetachedHEAD if the repo is in detached HEAD state.

func RequireClean

func RequireClean(state RepoState) error

RequireClean returns ErrDirtyWorktree if the worktree has uncommitted changes.

func RequireState

func RequireState(state RepoState, action string, allowed ...StateClass) error

RequireState verifies that the current state falls into one of the allowed StateClasses for the given action. Returns ErrInvalidTransition if not.

func RequireUpstream

func RequireUpstream(state RepoState) error

RequireUpstream returns ErrNoUpstream if no upstream tracking branch is configured.

func RequireValid

func RequireValid(state RepoState) error

RequireValid enforces that none of the X-class blocked states are active. This is the universal pre-mutation gate — call it before any operation that touches the repository.

func ResolveAuth

func ResolveAuth(remoteURL string) (transport.AuthMethod, error)

ResolveAuth resolves the go-git SSH transport auth method for a remote URL.

Resolution order (exclusive — first match wins):

  1. SSH_PRIVATE_KEY env var (in-memory, no filesystem dependency)
  2. SSH agent (SSH_AUTH_SOCK)
  3. Standard key files: id_ed25519, id_ecdsa, id_rsa

Host key verification is resolved via sfxssh.ResolveHostKeyCallback (same priority as raw SSH transport — SSH_KNOWN_HOSTS_CONTENT, SSH_KNOWN_HOSTS, ~/.ssh/known_hosts, SSH_INSECURE_SKIP_HOST_KEY_CHECK).

Returns an error when no auth is available — SSH auth failure is always fatal.

func ResolveHTTPAuth

func ResolveHTTPAuth(_ string) (*githttp.BasicAuth, error)

ResolveHTTPAuth returns HTTP basic auth for an HTTPS remote, resolving a credential from the environment so CI write-back (e.g. the deps auto-commit push) authenticates instead of failing with "HTTP Basic: Access denied".

Resolution order (first match wins):

  1. STAGEFREIGHT_GIT_USERNAME + STAGEFREIGHT_GIT_PASSWORD — explicit override.
  2. GITLAB_TOKEN — a Personal/Project Access Token (username "oauth2").
  3. GITHUB_TOKEN — username "x-access-token".
  4. CI_JOB_TOKEN — GitLab's per-job token (username "gitlab-ci-token"). LAST resort: it is read-only for repository writes by default, so a push needs a write-scoped token from (1)/(2); the job token only authenticates reads.

Returns (nil, nil) when nothing is set — preserving anonymous access to public HTTPS repos. A nil return is not an error: SSH remotes never reach here, and an unauthenticated push to a private remote fails loudly at push time.

func ResolveRef

func ResolveRef(repo *git.Repository, ref string) (string, error)

ResolveRef resolves any git ref (tag, branch, commit SHA, HEAD) to a commit SHA. Equivalent to `git rev-parse --verify <ref>^{commit}`.

func StagedFiles

func StagedFiles(s git.Status) []string

StagedFiles returns paths with staged changes (Staging != Unmodified).

func TagMessage

func TagMessage(repo *git.Repository, ref string) string

TagMessage returns the annotation message for an annotated tag. Returns "" for lightweight tags or on error (best-effort).

func TagPointsAtHEAD added in v0.7.0

func TagPointsAtHEAD(repo *git.Repository, tagName string) (bool, error)

TagPointsAtHEAD reports whether the named tag resolves to the same commit as HEAD. Unlike ExactTagAtHEAD (which returns whichever tag it happens to find first at HEAD), this checks a SPECIFIC tag — so a co-located channel ref (e.g. a minted dev-{sha8}) cannot mask a real release tag that is also at HEAD. A missing tag is not an error here; it simply does not point at HEAD.

func UnstagedDirtyPaths

func UnstagedDirtyPaths(s git.Status) []string

UnstagedDirtyPaths returns paths with unstaged modifications (not staged, not untracked).

func WorktreeStatus

func WorktreeStatus(wt *git.Worktree) (git.Status, error)

WorktreeStatus returns the current worktree status (staged + unstaged + untracked). Equivalent to `git status --porcelain`.

Types

type ErrHookRejected

type ErrHookRejected struct {
	Hook     string
	ExitCode int
}

ErrHookRejected is returned when a pre-commit or commit-msg hook exits non-zero.

func (*ErrHookRejected) Error

func (e *ErrHookRejected) Error() string

type ErrIndexDrift

type ErrIndexDrift struct {
	CommitHash plumbing.Hash
}

ErrIndexDrift is returned when the index diverges from the intended tree mid-replay. A hard reset to originalHEAD is performed before this error is returned.

func (*ErrIndexDrift) Error

func (e *ErrIndexDrift) Error() string

type ErrInvalidTransition

type ErrInvalidTransition struct {
	From    StateClass
	Action  string
	Allowed []StateClass
}

ErrInvalidTransition is returned when an operation is requested from a state that does not permit it. This is the definitive "impossible transition" error.

func (*ErrInvalidTransition) Error

func (e *ErrInvalidTransition) Error() string

type ErrPathTraversal

type ErrPathTraversal struct {
	Path string
}

ErrPathTraversal is returned when a diff path would escape the repository root.

func (*ErrPathTraversal) Error

func (e *ErrPathTraversal) Error() string

type ErrReplayCorrupted

type ErrReplayCorrupted struct {
	Original plumbing.Hash
	Replayed plumbing.Hash
}

ErrReplayCorrupted is returned when the post-replay tree does not match the original HEAD tree. Mutation occurred and diverged. A hard reset to originalHEAD is performed before returning.

func (*ErrReplayCorrupted) Error

func (e *ErrReplayCorrupted) Error() string

type ErrReplayUnsafe

type ErrReplayUnsafe struct {
	Reasons []string // descriptions of gate violations per commit
}

ErrReplayUnsafe is returned when the replay gate rejects commits before any mutation. This is distinct from ErrReplayCorrupted: no mutation has occurred.

func (*ErrReplayUnsafe) Error

func (e *ErrReplayUnsafe) Error() string

type ErrTransport

type ErrTransport struct {
	Err error
	Msg string
}

ErrTransport wraps SSH auth or push failures with an actionable message.

func (*ErrTransport) Error

func (e *ErrTransport) Error() string

func (*ErrTransport) Unwrap

func (e *ErrTransport) Unwrap() error

type RepoState

type RepoState struct {
	Branch              string // current branch name (empty if DetachedHEAD)
	UpstreamRef         string // e.g. "origin/main" — empty if not configured
	UpstreamConfigured  bool
	AheadCount          int // commits local has that remote does not
	BehindCount         int // commits remote has that local does not
	DetachedHEAD        bool
	WorktreeClean       bool          // true when no staged or unstaged-TRACKED changes exist (untracked files ignored) — gates push
	WorktreeCleanStrict bool          // true only when the tree is pristine INCLUDING no untracked files — gates worktree-MUTATING transitions (fast-forward, rebase)
	HeadHash            plumbing.Hash // current commit hash
	UpstreamHash        plumbing.Hash // remote tracking hash (zero if not configured)
	RemoteName          string        // e.g. "origin"
	InProgressOp        string        // "merge"/"rebase"/"cherry-pick"/"revert" if a git op is mid-flight, else ""
}

RepoState is the result of interrogating the current repository condition. ReadRepoState always returns a fully populated struct — callers must check DetachedHEAD and UpstreamConfigured before interpreting other fields.

All fields are resolved once at read time. No polling.

func ReadRepoState

func ReadRepoState(repo *git.Repository) (RepoState, error)

ReadRepoState reads the current repository state. This is the single read-only knowledge pool for all repo facts. No package should read HEAD, upstream, or branch state independently.

func (RepoState) Diverged

func (s RepoState) Diverged() bool

Diverged returns true when local and remote have independent commits.

type StateClass

type StateClass string

StateClass classifies a RepoState into a named state for the transition table.

S-class states are stable — transitions are permitted from them. X-class states are hard stops — no transitions are allowed until resolved.

Transition table:

S0 CLEAN_SYNCED  → no-op
S1 CLEAN_AHEAD   → PUSH       → S0
S2 CLEAN_BEHIND  → FAST_FORWARD → S0
S3 DIVERGED      → REPLAY     → S1 → PUSH → S0

X1 DETACHED_HEAD  → hard stop (checkout a branch)
X2 DIRTY_WORKTREE → hard stop (commit or stash)
X3 NO_UPSTREAM    → hard stop (set upstream tracking)
const (
	// Stable states — transitions permitted
	StateCleanSynced StateClass = "CLEAN_SYNCED" // S0: ahead=0, behind=0, clean
	StateCleanAhead  StateClass = "CLEAN_AHEAD"  // S1: ahead>0, behind=0
	StateCleanBehind StateClass = "CLEAN_BEHIND" // S2: ahead=0, behind>0
	StateDiverged    StateClass = "DIVERGED"     // S3: ahead>0, behind>0

	// Blocked states — hard stops
	StateDetachedHEAD  StateClass = "DETACHED_HEAD"  // X1
	StateDirtyWorktree StateClass = "DIRTY_WORKTREE" // X2
	StateNoUpstream    StateClass = "NO_UPSTREAM"    // X3
)

func Classify

func Classify(s RepoState) StateClass

Classify derives the StateClass from a RepoState snapshot. Classification order: blocked states are checked first (fail-fast), then stable states by ahead/behind counts.

This function is pure — same input always produces the same output.

func (StateClass) IsValid

func (c StateClass) IsValid() bool

IsValid returns true for S-class states where transitions are permitted.

func (StateClass) String

func (c StateClass) String() string

String implements Stringer.

type StatusFileChange

type StatusFileChange struct {
	Path    string
	Deleted bool
}

StatusFileChange represents a file with its change status for the forge backend.

func ChangedFiles

func ChangedFiles(s git.Status) []StatusFileChange

ChangedFiles returns all changed files (staged + unstaged + untracked) with delete status. Equivalent to `git status --porcelain=v1 -z -uall`.

func ChangedFilesInDir

func ChangedFilesInDir(s git.Status, dir string) []StatusFileChange

ChangedFilesInDir returns changed files within a specific directory prefix.

func StagedChanges

func StagedChanges(s git.Status) []StatusFileChange

StagedChanges returns staged files with delete status. Equivalent to `git diff --cached --name-status`.

type SyncSession

type SyncSession struct {
	// contains filtered or unexported fields
}

SyncSession is opened once per sync/push operation. State is resolved once at Open() and explicitly refreshed only after state-changing operations (Fetch, FastForward). Remote polling never happens opportunistically.

The network surface (Fetch, FastForward, Push, RemoteRefHash) is delegated to a Transport chosen once at Open(): the system git binary for repository-local workflows, or in-process go-git when StageFreight holds an explicit credential or no git is available. A single session therefore never mixes transports, and the credential never escapes the transport boundary.

func OpenSyncSession

func OpenSyncSession(rootDir string) (*SyncSession, error)

OpenSyncSession opens a SyncSession for the repository at rootDir. Reads repo state and resolves the transport authority once; both are reused throughout the session. The transport decision is centralized in ResolveTransport — and when it selects system git, no go-git credential is resolved (so none can fail with the wrong key), because Git owns authentication.

func (*SyncSession) FastForward

func (s *SyncSession) FastForward(remote string) error

FastForward fast-forwards the tracked upstream via the session transport. The commit engine only reaches it from CLEAN_BEHIND (a state guard), so a non-fast-forward case is unreachable here — the embedded and system transports are therefore semantically equivalent even though their error types differ (no caller matches the typed go-git ErrNonFastForwardUpdate).

func (*SyncSession) Fetch

func (s *SyncSession) Fetch(remote string) error

Fetch fetches branch heads from the remote via the session transport, then refreshes state.

func (*SyncSession) FetchedUpstreamHash

func (s *SyncSession) FetchedUpstreamHash() plumbing.Hash

FetchedUpstreamHash returns the upstream hash as observed after the last Fetch. Used by the replay race guard to detect concurrent pushes.

func (*SyncSession) Push

func (s *SyncSession) Push(remote, refspec string, setUpstream bool) error

Push pushes to remote via the session transport. When setUpstream is true it also configures branch tracking in .git/config — a local write, transport- agnostic, so it applies under both system git and embedded transport.

func (*SyncSession) Refresh

func (s *SyncSession) Refresh() error

Refresh re-reads repo state after a mutation (fetch, fast-forward, reset).

func (*SyncSession) RemoteRefHash added in v0.7.0

func (s *SyncSession) RemoteRefHash(remote, ref string) (plumbing.Hash, error)

RemoteRefHash resolves a branch head on the remote through the session transport (system git ls-remote, or embedded go-git). Keeping remote reads behind the boundary is why no credential accessor leaks out of the session.

func (*SyncSession) Repo

func (s *SyncSession) Repo() *git.Repository

Repo returns the underlying git.Repository for local (non-transport) reads.

func (*SyncSession) State

func (s *SyncSession) State() RepoState

State returns the current resolved repo state.

type TargetFacts added in v0.7.0

type TargetFacts struct {
	Exists bool // does refs/remotes/<remote>/<branch> exist?
	Ahead  int  // commits HEAD has that the destination lacks
	Behind int  // commits the destination has that HEAD lacks
}

TargetFacts describes where local HEAD sits relative to an EXPLICIT push destination (remote/branch) — e.g. pushing a feature branch to origin/main. Unlike RepoState's ahead/behind (always vs the branch's own upstream), these are computed against an arbitrary destination ref.

func ResolveTargetFacts added in v0.7.0

func ResolveTargetFacts(session *SyncSession, remote, branch string) (TargetFacts, error)

ResolveTargetFacts computes HEAD's position relative to an explicit destination. The caller must have fetched the remote first so the remote-tracking ref is current; this function performs no I/O beyond local ref/commit reads.

type TransitionEvent

type TransitionEvent struct {
	From   StateClass `json:"from"`
	Action string     `json:"action"`
	To     StateClass `json:"to,omitempty"`
	Note   string     `json:"note,omitempty"`
}

TransitionEvent is emitted by the Engine for every state transition. Consumers may log, forward to a UI, or record for audit.

JSON encoding is intentional — these are the structured facts that CI logs and diagnostic tooling consume.

type Transport added in v0.7.0

type Transport interface {
	Fetch(remote string) error
	FastForward(remote string) error
	Push(remote, refspec string) error
	RemoteRefHash(remote, ref string) (plumbing.Hash, error)
}

Transport is the network-facing Git surface for a session. Two implementations exist: systemTransport delegates to the system git binary (the authority for repository-local workflows — it honors ~/.ssh/config, credential helpers, agents, certs, ProxyJump, Include, enterprise auth), and embeddedTransport runs in-process via go-git with a StageFreight-supplied credential. The whole surface is selected once per session so a single operation never mixes the two.

Seam note: FastForward is, strictly, repository-sync policy rather than transport (it integrates fetched refs into the worktree). It lives here for now because both implementations must express it consistently; the honest future boundary is RemoteTransport{Fetch,Push,RemoteRefHash} + RepositorySync{FastForward}. RemoteRefHash, by contrast, is a genuine network read and belongs here.

type TransportDecision added in v0.7.0

type TransportDecision struct {
	Preference TransportPreference
	Auth       transport.AuthMethod
}

TransportDecision is the centralized transport-authority decision: which transport to use and, when embedded, the resolved credential to use with it.

func ResolveTransport added in v0.7.0

func ResolveTransport(remoteURL string) (TransportDecision, error)

ResolveTransport decides who owns the Git transport for remoteURL. The question is not "where are we running" but "was StageFreight explicitly entrusted with a credential to act independently of the user's Git environment?" For an SSH remote that credential is SSH_PRIVATE_KEY; for HTTPS it is one of the HTTP-token envs ResolveHTTPAuth reads. Absent an explicit credential, the repository's own Git is the transport authority (PreferSystemGit) — so credential helpers, config-mapped keys, agents, certs, and enterprise auth all work, because Git (not StageFreight) handles them.

Both conditions for system git live here so the decision is one model in one place: a git binary must be available to delegate to, AND no credential was injected. Selection never re-derives either half.

type TransportPreference added in v0.7.0

type TransportPreference int

TransportPreference expresses who owns the Git transport for a remote. It is the centralized output of credential resolution — transport SELECTION consumes it and never re-scans the environment in another package.

const (
	// PreferSystemGit delegates transport to the system git binary, the authority
	// for repository-local workflows: it already honors ~/.ssh/config, credential
	// helpers, agents, SSH certs, ProxyJump, Include, and enterprise auth that
	// StageFreight would otherwise have to reimplement — and be less capable than.
	PreferSystemGit TransportPreference = iota
	// RequireEmbeddedTransport uses in-process go-git with a StageFreight-supplied
	// credential. Chosen only when StageFreight was explicitly handed a credential
	// to act as, independent of the user's Git environment.
	RequireEmbeddedTransport
)

Jump to

Keyboard shortcuts

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