package index
// strata.go — RFC 0006: a stack of dependent landings, derived and never
// stored. Follow target → branch across landings: a landing whose target
// is another landing's branch is a segment stacked on it; the one whose
// target is a plain branch is the base. The chain is read fresh from the
// summaries every time, so a segment retargeted by zit#144 after its base
// lands simply stops being one.
// MaxDrawnRungs bounds what the pages DRAW, not what a strata may be.
// GitLab stops drawing its stack control past twenty; past that a
// folded list and a map stop helping and start scrolling. The chain is
// still read and the walk still lands it; the page says how tall it is
// and points at the base.
const MaxDrawnRungs = 20
// StackSegment is one rung of a strata as the record page draws it.
type StackSegment struct {
ID string `json:"id"`
Title string `json:"title"`
Branch string `json:"branch"`
Target string `json:"target"`
State string `json:"state"`
Position int `json:"position"` // 1 is the base
// Landable marks the one open segment whose target is not itself an
// open landing's branch — the only rung accept can act on.
Landable bool `json:"landable"`
// CI is this rung's own verdict, filled by the reader from the tag.
CI string `json:"ci,omitempty"`
// Preflight is this rung's answer when the whole stack is probed
// onto the bottom rung's target with the rungs below it merged
// first (zit#211): clean, up-to-date, conflict, unrelated, or
// not-reached above the rung that stops. Empty when not probed.
Preflight string `json:"preflight,omitempty"`
PreflightConflicts []string `json:"preflightConflicts,omitempty"`
}
// Strata is a chain, bottom-up.
type Strata struct {
Base string `json:"base"`
// Top is the highest rung: landing "the strata" means landing up
// to it.
Top string `json:"top"`
// CI is the strata's verdict: the top rung's. Its tip contains every
// rung below, so a run there validates the stack as a unit — the
// reading Graphite and Aviator settled on. A lower rung's own
// verdict still says what it says; this is the one to land on.
CI string `json:"ci,omitempty"`
Segments []StackSegment `json:"segments"`
// Broken names why the chain cannot be walked: a cycle, or a base
// that was rejected so everything above it is stacked on work
// nobody will land (RFC 0006 §An abandoned base is flagged).
Broken string `json:"broken,omitempty"`
// Preflight says what walking the strata would do, known before
// anything is accepted (1f8c4148): "lands" when every rung merges
// onto the target with the ones below it, "stops" when one does not.
// Empty when the stack was not probed. StopsAt is the position of
// the rung that would stop the walk; LandTo and LandCount name how
// far a walk would get — the rung to land up to, and how many.
Preflight string `json:"preflight,omitempty"`
StopsAt int `json:"stopsAt,omitempty"`
LandTo string `json:"landTo,omitempty"`
LandCount int `json:"landCount,omitempty"`
}
// AttachStrata computes, for every OPEN landing in ls, which stack it
// belongs to and where: Stack (the base's id), Position and Depth. The
// input is every landing the caller knows about, whatever its state,
// because a segment's target may be the branch of a landing that was
// rejected — and that is a fact the chain must report, not lose.
//
// Superseded landings take no part: the current landing on a branch is
// the one the chain runs through.
func AttachStrata(ls []LandingSummary) {
byBranch := map[string]*LandingSummary{}
for i := range ls {
l := &ls[i]
if l.SupersededBy != "" {
continue
}
if cur, ok := byBranch[l.Branch]; !ok || l.State == "open" && cur.State != "open" {
byBranch[l.Branch] = l
}
}
for i := range ls {
l := &ls[i]
l.Stack, l.Position, l.Depth, l.Broken = "", 0, 0, ""
if l.State != "open" || l.SupersededBy != "" {
continue
}
// Walk down to the base, counting.
seen := map[string]bool{l.ID: true}
base, pos := l, 1
for {
below, ok := byBranch[base.Target]
if !ok {
break // a plain branch: this is the base
}
if seen[below.ID] {
l.Broken = "cycle"
break
}
if below.State != "open" {
if below.State == "rejected" {
l.Broken = "base rejected"
}
// Accepted and not yet retargeted: the rung below is
// done and this one is next. It counts as the base of
// what remains.
break
}
seen[below.ID] = true
base, pos = below, pos+1
}
// A landing whose base was REJECTED is not standalone, and this
// is where it used to become one.
//
// The walk breaks at a rejected rung with pos still 1, and
// nothing is stacked above a top segment — so this `continue`
// fired, l.Stack stayed empty, and StrataOf then returned
// Strata{} for it. The record page lost the whole chain:
// {base:"", top:"", segments:null}, Mergeable flipped from
// "stacked" to clean, and a live Accept appeared on work whose
// base is gone (ticket b3aad27c). RFC 0006 says the opposite —
// "an abandoned base is flagged: the record page says the base
// was rejected and names it".
//
// Broken is already set by then. Keeping the membership is what
// lets anything downstream read it: the segment becomes its own
// base, carrying the reason it is alone.
if pos == 1 && l.Broken == "" && !isStackedOn(byBranch, l) {
continue // standalone: no stack at all
}
l.Stack, l.Position = base.ID, pos
}
// Depth is the tallest position sharing a base.
depth := map[string]int{}
for i := range ls {
if ls[i].Stack != "" && ls[i].Position > depth[ls[i].Stack] {
depth[ls[i].Stack] = ls[i].Position
}
}
for i := range ls {
if ls[i].Stack != "" {
ls[i].Depth = depth[ls[i].Stack]
}
}
}
// isStackedOn reports whether some open landing targets l's branch.
func isStackedOn(byBranch map[string]*LandingSummary, l *LandingSummary) bool {
for _, other := range byBranch {
if other.State == "open" && other.Target == l.Branch && other.ID != l.ID {
return true
}
}
return false
}
// StrataOf assembles the chain a landing belongs to, bottom-up, for the
// record page's map. Empty for a standalone landing. ls must already
// carry AttachStrata's reading.
func StrataOf(ls []LandingSummary, id string) Strata {
var me *LandingSummary
for i := range ls {
if ls[i].ID == id {
me = &ls[i]
}
}
if me == nil || me.Stack == "" {
return Strata{}
}
out := Strata{Base: me.Stack, Broken: me.Broken}
for _, l := range ls {
if l.Stack != me.Stack {
continue
}
out.Segments = append(out.Segments, StackSegment{
ID: l.ID, Title: l.Title, Branch: l.Branch, Target: l.Target,
State: l.State, Position: l.Position, Landable: l.Position == 1 && l.Broken == "",
})
}
// Bottom-up, by position.
for i := 1; i < len(out.Segments); i++ {
for j := i; j > 0 && out.Segments[j].Position < out.Segments[j-1].Position; j-- {
out.Segments[j], out.Segments[j-1] = out.Segments[j-1], out.Segments[j]
}
}
if n := len(out.Segments); n > 0 {
out.Top = out.Segments[n-1].ID
}
// A chain whose base was rejected is kept together under its lowest
// OPEN rung (that is what keeps it unlandable), so me.Stack names a
// landing of the chain itself. Reported as-is, Base then named the
// segment as its own base while the refusal beside it named the
// rejected one — two fields answering one question, disagreeing.
// RFC 0006: "the record page says the base was rejected and names
// it". So Base names the rejected landing: the one the bottom rung
// targets.
if out.Broken == "base rejected" && len(out.Segments) > 0 {
bottom := out.Segments[0]
for _, l := range ls {
if l.Branch == bottom.Target && l.State == "rejected" {
out.Base = l.ID
}
}
}
return out
}
// Decisions counts what awaits a decision: every open landing that is
// not stacked on another open one. A stack of five is one decision —
// the base's — because nothing above it can be acted on until it is.
func Decisions(ls []LandingSummary) int {
n := 0
for _, l := range ls {
if l.State == "open" && l.SupersededBy == "" && (l.Stack == "" || l.Position == 1) {
n++
}
}
return n
}