gitplan

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: 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