package web
import "html/template"
// A stack landed in one press of Land (RFC 0006 strata) leaves a run of
// merges on the mainline, one per rung, each merging a branch that was
// forked from the branch merged just below it. The timeline draws that
// run as ONE card beside its merge nodes, tinted in the stack's bottom
// branch's colour: the press is one decision, and a card per merge said
// it several times over. The graph is not changed: drawing the stack's
// branches as one lane meant dropping the older merges' lines, and the
// graph stopped saying what merged where (2026-10-29).
//
// It is read from the DAG and the landing decorations alone. The
// landing records cannot say it after the fact: a stack's upper rungs
// are retargeted onto the mainline as the rungs below them land.
// tlStack is a stack card's content, on the newest of its merge rows.
type tlStack struct {
Lines []tlStackLine // the landings, newest first
Approver string
CI template.HTML // the newest merge's verdict: the mainline after the press
}
// Shown is what the card lists at once; Rest opens in place.
func (s *tlStack) Shown() []tlStackLine { return s.Lines[:min(stackShown, len(s.Lines))] }
func (s *tlStack) Rest() []tlStackLine { return s.Lines[min(stackShown, len(s.Lines)):] }
// stackShown is how many landings a stack card lists before "N more":
// a stack of ten would otherwise push the rest of the day off a phone.
const stackShown = 3
type tlStackLine struct {
Landing string
Number int
Title string
Stat template.HTML
Note template.HTML // the merge's own note, such as which tip its checks tested
}
// stackLanes finds the landed stacks among entries (newest first): each
// one's merge hashes, newest first, and its bottom branch, whose colour
// tints the card.
func stackLanes(defaultBranch string, entries []timelineEntry) []landedStack {
branchOf := map[string]string{}
root := map[string]timelineEntry{} // a branch's oldest check-in on the page
for _, e := range entries {
if flatKind(e.Kind) {
continue
}
branchOf[e.Hash] = e.Branch
root[e.Branch] = e
}
// base is the branch a branch was forked from, when its fork is on
// the page.
base := func(b string) string {
r, ok := root[b]
if !ok || len(r.Parents) == 0 {
return ""
}
return branchOf[r.Parents[0]]
}
type landed struct {
at int
hash string
branch string
by string
}
var merges []landed
for i, e := range entries {
if flatKind(e.Kind) || e.Branch != defaultBranch || e.Landing == "" || len(e.Parents) < 2 {
continue
}
b := branchOf[e.Parents[1]]
if b == "" || b == defaultBranch {
continue
}
merges = append(merges, landed{at: i, hash: e.Hash, branch: b, by: e.LandingApprover})
}
var stacks []landedStack
for i := 0; i < len(merges); {
j := i
// One press: the next older row is the next merge, decided by
// the same person, of the branch this one was stacked on.
for j+1 < len(merges) && merges[j+1].at == merges[j].at+1 &&
merges[j+1].by == merges[j].by && base(merges[j].branch) == merges[j+1].branch {
j++
}
if j > i {
var hashes []string
bottom := merges[j].branch
for k := i; k <= j; k++ {
hashes = append(hashes, merges[k].hash)
}
stacks = append(stacks, landedStack{hashes: hashes, lane: bottom})
}
i = j + 1
}
return stacks
}
// landedStack is one press's merges, newest first, and its lane.
type landedStack struct {
hashes []string
lane string
}
// foldStacks turns each stack's merge rows into one card: the newest
// row carries it and spans the rest, whose graph cells stay. A stack
// split by a day boundary keeps a card per day.
func foldStacks(pal *palette, stacks []landedStack, days []tlDay, refs []rowRef) {
at := map[string]rowRef{}
for _, r := range refs {
if r.ci {
at[r.hash] = r
}
}
for _, stack := range stacks {
var run []rowRef
flush := func() {
if len(run) < 2 {
run = nil
return
}
head := &days[run[0].d].Rows[run[0].r]
st := &tlStack{Approver: head.LandingApprover, CI: head.CI}
for _, r := range run {
row := &days[r.d].Rows[r.r]
var note template.HTML
if len(row.Paras) > 0 {
note = row.Paras[0].Text
}
st.Lines = append(st.Lines, tlStackLine{Landing: row.Landing, Number: row.LandingNumber,
Title: row.LandingTitle, Stat: row.Stat, Note: note})
row.Skip = true
}
head.Skip, head.Span, head.Stack = false, len(run), st
head.StackTint = pal.tint(stack.lane)
run = nil
}
for _, h := range stack.hashes {
r, ok := at[h]
if !ok {
continue
}
if n := len(run); n > 0 && (run[n-1].d != r.d || run[n-1].r+1 != r.r) {
flush()
}
run = append(run, r)
}
flush()
}
}