Documentation
¶
Index ¶
- Variables
- func AllChangedFiles(s git.Status) []string
- func AllDirtyPaths(s git.Status) []string
- func CountCommitsBetween(repo *git.Repository, fromRef, toRef string) (int, error)
- func DetectInProgressOp(rootDir string) string
- func DiffStats(repo *git.Repository, fromRef, toRef string) (files, insertions, deletions int, err error)
- func ExactTagAtHEAD(repo *git.Repository) (string, error)
- func HasStagedChanges(s git.Status) bool
- func HasUnstagedChanges(s git.Status) bool
- func IsAncestor(repo *git.Repository, ancestorRef, descendantRef string) (bool, error)
- func IsClean(s git.Status) bool
- func IsCleanIgnoringUntracked(s git.Status) bool
- func IsSSHURL(url string) bool
- func ListTagsSorted(repo *git.Repository) ([]string, error)
- func OpenRepo(rootDir string) (*git.Repository, error)
- func ParseCommitLog(repo *git.Repository, fromRef, toRef string) ([]*object.Commit, error)
- func RemoteRefHash(repo *git.Repository, remoteName, refName string, auth transport.AuthMethod) (plumbing.Hash, error)
- func RemoteURL(repo *git.Repository, remoteName string) (string, error)
- func RepoRoot(repo *git.Repository) (string, error)
- func RequireAttached(state RepoState) error
- func RequireClean(state RepoState) error
- func RequireState(state RepoState, action string, allowed ...StateClass) error
- func RequireUpstream(state RepoState) error
- func RequireValid(state RepoState) error
- func ResolveAuth(remoteURL string) (transport.AuthMethod, error)
- func ResolveHTTPAuth(_ string) (*githttp.BasicAuth, error)
- func ResolveRef(repo *git.Repository, ref string) (string, error)
- func StagedFiles(s git.Status) []string
- func TagMessage(repo *git.Repository, ref string) string
- func TagPointsAtHEAD(repo *git.Repository, tagName string) (bool, error)
- func UnstagedDirtyPaths(s git.Status) []string
- func WorktreeStatus(wt *git.Worktree) (git.Status, error)
- type ErrHookRejected
- type ErrIndexDrift
- type ErrInvalidTransition
- type ErrPathTraversal
- type ErrReplayCorrupted
- type ErrReplayUnsafe
- type ErrTransport
- type RepoState
- type StateClass
- type StatusFileChange
- type SyncSession
- func (s *SyncSession) FastForward(remote string) error
- func (s *SyncSession) Fetch(remote string) error
- func (s *SyncSession) FetchedUpstreamHash() plumbing.Hash
- func (s *SyncSession) Push(remote, refspec string, setUpstream bool) error
- func (s *SyncSession) Refresh() error
- func (s *SyncSession) RemoteRefHash(remote, ref string) (plumbing.Hash, error)
- func (s *SyncSession) Repo() *git.Repository
- func (s *SyncSession) State() RepoState
- type TargetFacts
- type TransitionEvent
- type Transport
- type TransportDecision
- type TransportPreference
Constants ¶
This section is empty.
Variables ¶
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.
var ErrDirtyWorktree = errors.New("worktree has uncommitted changes — commit or stash first")
ErrDirtyWorktree is returned when uncommitted changes are present before replay.
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.
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 ¶
AllChangedFiles returns paths with any modification (staged, unstaged, or untracked).
func AllDirtyPaths ¶
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
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 ¶
HasStagedChanges returns true when any file has a staged modification.
func HasUnstagedChanges ¶
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 IsCleanIgnoringUntracked ¶ added in v0.7.0
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 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 ¶
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 ¶
RequireAttached returns ErrDetachedHEAD if the repo is in detached HEAD state.
func RequireClean ¶
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 ¶
RequireUpstream returns ErrNoUpstream if no upstream tracking branch is configured.
func RequireValid ¶
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):
- SSH_PRIVATE_KEY env var (in-memory, no filesystem dependency)
- SSH agent (SSH_AUTH_SOCK)
- 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 ¶
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):
- STAGEFREIGHT_GIT_USERNAME + STAGEFREIGHT_GIT_PASSWORD — explicit override.
- GITLAB_TOKEN — a Personal/Project Access Token (username "oauth2").
- GITHUB_TOKEN — username "x-access-token".
- 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 ¶
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 ¶
UnstagedDirtyPaths returns paths with unstaged modifications (not staged, not untracked).
Types ¶
type ErrHookRejected ¶
ErrHookRejected is returned when a pre-commit or commit-msg hook exits non-zero.
func (*ErrHookRejected) Error ¶
func (e *ErrHookRejected) Error() string
type ErrIndexDrift ¶
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 ¶
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 ¶
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.
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.
type StatusFileChange ¶
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 )