Documentation
¶
Overview ¶
Package gitplan is StageFreight's planner for shared-history git operations.
It is the pure heart of "the plan IS the UX": given a fully-resolved Situation (repository State + Destination + Policy, folded into destination-relative facts), Resolve produces a Plan — an ordered graph of Operations (StageFreight verbs, not git verbs). Every consumer — render, explain, audit, simulate, execute — walks that one graph, so they can never diverge.
Resolve is PURE: no I/O, no clock, no network. That is what makes the hard decisions (which operations, which gates) exhaustively unit-testable with zero git. All I/O lives in the resolver that builds a Situation and in the executor that walks the Plan.
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
Types ¶
type Destination ¶
type Destination struct {
Remote string
Branch string
Protected bool // resolved from policy — the switch that "wakes" replay-onto-trunk
}
Destination is where commits are going, with protected-ness resolved from policy.
func (Destination) Ref ¶
func (d Destination) Ref() string
Ref renders the destination as "remote/branch".
type DivergeRule ¶
type DivergeRule string
DivergeRule is the policy-resolved handling of a diverged NON-protected destination. Empty means "no policy" → the planner must ask (Decide), never guess: a diverged feature branch is user-intent ambiguity (pull vs rebase vs force), not policy's call.
const ( DivergeAsk DivergeRule = "" // user-intent ambiguity — Decide DivergeRebase DivergeRule = "rebase" // repo policy says linearize — Confirm→Replay )
type InteractionLevel ¶
type InteractionLevel string
InteractionLevel is DERIVED from which interaction op the graph contains — it is execution policy (how autonomously the plan may run), not planner certainty.
const ( Automatic InteractionLevel = "automatic" // no interaction op Inform InteractionLevel = "inform" // Teach present Confirm InteractionLevel = "confirm" // Confirm present Decide InteractionLevel = "decide" // Decide present )
type OpKind ¶
type OpKind string
OpKind is a StageFreight operation primitive. Interaction (Teach/Confirm/Decide) is itself an operation in the sequenced graph, not an attribute — so Execute and every other consumer just walk the graph; none has to remember how Confirm gates Replay.
const ( OpNoop OpKind = "noop" OpUpload OpKind = "upload" // push commits to a fast-forwardable ref OpCreateTracking OpKind = "create-tracking" // first push: push + set upstream OpFastForward OpKind = "fast-forward" // advance local to a strictly-ahead remote OpReplay OpKind = "replay" // rebase local commits onto the destination (new SHAs) OpOfferMR OpKind = "offer-mr" // offer to open a merge request OpRefuse OpKind = "refuse" // stop: no safe/allowed action OpDirectPush OpKind = "direct-push" // push HEAD straight to an explicit refspec, no reconcile (CI detached-HEAD) // Interaction operations — sequenced in the graph, never attributes. OpTeach OpKind = "teach" // explain; execution continues OpConfirm OpKind = "confirm" // require a yes before the operations that follow OpDecide OpKind = "decide" // present choices; the operator picks )
type Operation ¶
type Operation struct {
Kind OpKind
Detail string // human description ("replay 3 commits onto origin/main")
Reason string // required for governed/escape-hatch ops; captured to the audit trail
Choices []string // for OpDecide
}
Operation is one node in the plan graph.
type Plan ¶
type Plan struct {
Operations []Operation
Dest Destination
Summary string
}
Plan is the durable IR: an ordered operation graph plus metadata. Consumers grow by reading metadata; do not thread bare []Operation through APIs.
func DirectPush ¶
DirectPush is the CI detached-HEAD / explicit-refspec case: push HEAD straight to the given refspec with no fetch, classify, or reconcile — a trivial single-op plan that does not fit the ahead/behind model. It replaces the pre-planner engine.Sync refspec fast path.
func Resolve ¶
Resolve is the pure planner: a Situation → a Plan. Deterministic — the same Situation always yields the same Plan (no clock, no randomness). This is the whole "intelligence": everything downstream is a consumer of the graph it returns.
func ResolveExplicitPush ¶
ResolveExplicitPush plans an EXPLICIT cross-target push — `stagefreight push <remote> <branch>`, e.g. landing a feature branch onto origin/main. Ahead/behind are measured against the destination ref (not the branch's own upstream). A new or fast-forwardable destination is a direct push of HEAD to the ref (gated by Confirm when the destination is protected trunk); a destination that already contains you refuses; a diverged destination is handed back to git to rebase onto first — cross-branch replay execution is a follow-on (the linear-replay engine is coupled to the branch's own upstream).
func ResolvePull ¶
ResolvePull is the pure planner for down-sync (`stagefreight pull`): bring the remote's commits into the local branch. It never pushes — same operation vocabulary, other direction. Behind → fast-forward; diverged → rebase local onto the remote (Replay), gated by Confirm because it rewrites local commits; ahead/synced → nothing to pull.
func (Plan) Interaction ¶
func (p Plan) Interaction() InteractionLevel
Interaction derives the interaction level from the graph (most-involved op present).
type Policy ¶
type Policy struct {
Protected []string // glob patterns for protected destinations, e.g. "main", "release/*"
}
Policy is the small, declarative "what is allowed" data — NOT a rules engine. The planner holds the intelligence; policy holds a protected-destination list (and, later, branch classes). Deliberately almost boring: no expression language, ever.
func DefaultPolicy ¶
func DefaultPolicy() Policy
DefaultPolicy is the fallback protected set when a repo declares none. Callers that have a config-provided list should prefer it; this keeps the sensible-default in one place instead of hardcoded at each call site.
func WithProtected ¶
WithProtected returns a Policy from a config-provided protected list, falling back to the default when the list is empty.
func (Policy) DivergeRule ¶
func (p Policy) DivergeRule(branch string) DivergeRule
DivergeRule returns the policy-resolved handling of a diverged NON-protected destination. Default: DivergeAsk — user-intent ambiguity (pull vs rebase vs force) is not policy's to resolve. Grows from real need, not speculation.
func (Policy) IsProtected ¶
IsProtected reports whether a branch name matches any protected pattern.
type Situation ¶
type Situation struct {
Dest Destination
HasUpstream bool // is a tracking ref configured for this push path?
Ahead int // local commits the destination lacks
Behind int // destination commits the local lacks
OnDiverge DivergeRule // policy for a diverged non-protected destination
// AutoConverge is the CI / `commit --push` context: an authorized auto-flow that
// converges rather than asking. It makes `behind` fast-forward (not Refuse+Teach) and
// any `diverged` replay (not Decide) — matching the pre-planner engine.Sync behavior.
// Interactive `sf push` leaves it false. The Confirm gate is still emitted for shared
// mutations; the auto-flow satisfies it via ExecuteOptions.Approved.
AutoConverge bool
// InProgressOp names a mid-flight git operation (merge/rebase/cherry-pick/revert). When
// non-empty the planner refuses with guidance — StageFreight is a first-class git citizen
// and never acts on a half-finished state (this is the "silent flatten" fix).
InProgressOp string
}
Situation is the fully-resolved, destination-relative input to the pure planner. A resolver (impure, reads git + policy) builds this; Resolve consumes it. Ahead/Behind are relative to the DESTINATION ref, so the same planner serves "push to my own upstream" and "land on a protected branch" without knowing which it is.
func SituationFromState ¶
SituationFromState builds a Situation for a push to the branch's OWN tracked upstream (the `sf push` default): Ahead/Behind come straight from RepoState — already relative to the upstream — and destination protected-ness comes from policy. Pure over RepoState (a plain struct), so it is fully unit-testable with no git.
Pushing to an explicitly-named different branch (feat → main) computes destination- relative counts separately (a later slice) and builds a Situation the same way, so the pure planner never learns which case it is.
func SituationFromStateConverge ¶
SituationFromStateConverge is SituationFromState for the CI / `commit --push` auto-flow: an authorized context that converges (fast-forward when behind, replay when diverged) rather than asking. It is the single seam that lets `commit --push` share the planner while preserving the pre-planner engine.Sync behavior.