Help us shape the future of Go! Take the 2026 Go Developer Survey

gitplan

package
v0.13.0 Latest Latest
Warning

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

Go to latest
Published: Oct 2, 2026 License: AGPL-3.0, AGPL-3.0-only Imports: 4 Imported by: 0

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

func Render

func Render(p Plan) string

Render turns a Plan into the block shown to the operator before anything runs — the "plan IS the UX". Deterministic (golden-testable). Render is one consumer of the graph; Execute walks the SAME graph, so the two can never describe different things.

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

func DirectPush(remote, refspec string) Plan

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

func Resolve(s Situation) Plan

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

func ResolveExplicitPush(s Situation) Plan

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

func ResolvePull(s Situation) Plan

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

func WithProtected(protected []string) Policy

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

func (p Policy) IsProtected(branch string) bool

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

func SituationFromState(state gitstate.RepoState, policy Policy) Situation

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

func SituationFromStateConverge(state gitstate.RepoState, policy Policy) Situation

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.

Jump to

Keyboard shortcuts

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