Gate: seed source.main with the trunk head of a repository it names none of (issue 1422)

source.main moved only at a merge, so a repository never merged through the gate had no trunk head and the checker could
never judge its pull requests. The gate already hears main's head on each gitea.pull.updated (base_sha); it now adds it for
a repository source.main lacks, from the newest open pull request into main, in one compare-and-set write that never
replaces a head. A pull request into another base seeds nothing (ADR 0484 §11).
This commit is contained in:
2026-10-28 11:03:40 +01:00
parent 9fd95693db
commit 0c6b7b5d64
4 changed files with 95 additions and 0 deletions
@@ -57,6 +57,8 @@ type Holder struct {
// catchUp asks the next look to weigh again the newest merge of each repository against source.main (SourceCatchUp).
catchUp bool
// seeded are the repositories source.main was found to name, or was seeded with, since this owner started (seedTrunks).
seeded map[string]bool
mu sync.Mutex
deliveries map[string]*Delivery
@@ -1,6 +1,7 @@
package main
import (
"strings"
"testing"
"time"
@@ -48,6 +49,36 @@ func TestStartingWritesTheNewestMergeOfEachRepositoryNotYetInSourceMain(t *testi
t.Fatalf("a second catch-up moved source.main again: %d moves, then %d", n, len(w.source.moves))
}
// Issue 1422: a repository never merged through the gate gets main's head as the forge announced its open pull request
// with, once, beside the others; a pull request into another base, or one announced with no base head (the forge could
// not read it), seeds nothing; a head source.main holds is never replaced.
const never, trunkN, trunkM = "novox/never-merged", "eeee000000000000000000000000000000000001", "eeee000000000000000000000000000000000002"
s := newWorld(t)
s.source.main = &sdk.Source{V: sdk.Version, Group: "before", Heads: map[string]string{i1726Repo: i1726Merge1}}
other := pr("novox/other-base", 6, "ffff000000000000000000000000000000000001", "fix/c", "a.go")
other.Base, other.BaseSHA = "nox-mesh", trunkM
s.h.PullUpdated(other)
unread := pr("novox/unread", 2, "ffff000000000000000000000000000000000002", "fix/d", "a.go")
s.h.PullUpdated(unread)
seed := pr(never, 4, "ffff000000000000000000000000000000000003", "fix/e", "a.go")
seed.BaseSHA = trunkN
s.h.PullUpdated(seed)
s.settleAll()
if h := s.source.main.Heads; h[never] != trunkN || h[i1726Repo] != i1726Merge1 || h["novox/other-base"] != "" || h["novox/unread"] != "" || len(s.source.moves) != 1 {
t.Fatalf("source.main is %v after %d moves; wanted %s seeded at %s, the catalogue kept and nothing else, in one move", h, len(s.source.moves), never, trunkN)
}
if !strings.Contains(s.logs(), "seeded with "+trunkN[:12]) {
t.Errorf("the seed was not said: %s", s.logs())
}
again := pr(never, 5, "ffff000000000000000000000000000000000004", "fix/f", "a.go")
again.BaseSHA = trunkM
s.h.PullUpdated(again)
s.h.SourceCatchUp()
s.settleAll()
if got := s.source.main.Heads[never]; got != trunkN || len(s.source.moves) != 1 {
t.Fatalf("a seeded head was replaced by %s (%d moves)", got, len(s.source.moves))
}
o := newWorld(t)
o.source.main = &sdk.Source{V: sdk.Version, Group: "newer", Heads: map[string]string{i1726Repo: i1726Merge2}}
o.h.PullUpdated(pr(i1726Repo, 471, head2, "fix/b", "modules/app/b.go"))
@@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"reflect"
"regexp"
"sort"
"strings"
"time"
@@ -524,6 +525,66 @@ func (h *Holder) settleForgeMerges() []func() {
return out
}
// trunkHead is a full commit id as the forge names a branch's head.
var trunkHead = regexp.MustCompile(`^[0-9a-f]{40}$`)
// seedTrunks gives source.main the trunk head of a repository it names none of (issue 1422): one never merged through the
// gate, whose open pull requests the checker could otherwise never judge. The head is the base_sha the forge announced the
// newest open pull request into main with (gitea.pull.updated; ADR 0323), the trunk's head when it was announced. It is
// added in one compare-and-set write that never replaces a head source.main holds, so a merge always wins; the first merge
// through the gate moves it as any other. source.main is per repository and names main's head only (ADR 0484 §11, MergeBranch):
// a pull request into another base seeds nothing. A repository is weighed once per start after it was found or seeded.
// Under the lock; the writing happens outside it.
func (h *Holder) seedTrunks() []func() {
newest := map[string]*Delivery{}
for _, d := range h.deliveries {
if h.seeded[d.Repository] || d.Base != MergeBranch || !trunkHead.MatchString(d.BaseSHA) || d.State.Final() || d.MergedAs != "" {
continue
}
if o := newest[d.Repository]; o == nil || d.Created.After(o.Created) {
newest[d.Repository] = d
}
}
var out []func()
for repository, d := range newest {
id, head, group := d.ID, d.BaseSHA, firstOf(d.Branch, d.Repository)
out = append(out, func() {
seeded := false
err := h.Source.Move(func(prev *sdk.Source) (sdk.Source, bool) {
seeded = false
next := sdk.Source{Group: group, Heads: map[string]string{}}
if prev != nil {
if prev.Heads[repository] != "" {
return *prev, false // named already: never replaced
}
next.Group = prev.Group
for r, held := range prev.Heads {
next.Heads[r] = held
}
}
next.Heads[repository] = head
seeded = true
return next, true
})
h.mu.Lock()
defer h.mu.Unlock()
if err != nil {
h.Logf("[mesh-delivery] %s: source.main names no trunk head of %s, and it cannot be seeded yet: %v; asked again in %s", id, repository, err, askAgainAfter)
h.again("source.main, seeded with the trunk head of a repository it names none of")
return
}
if h.seeded == nil {
h.seeded = map[string]bool{}
}
h.seeded[repository] = true
if seeded {
h.Logf("[mesh-delivery] source.main named no trunk head of %s: seeded with %s, main's head as the forge announced %s with, so its pull requests are judged (issue 1422)", repository, shortOf(head, 12), id)
}
})
}
return out
}
// replayLate is how long after its merge a merge heard only from the forge's word is a replay: the forge announces a merge
// within seconds, so one heard this late was replayed from the bus's history, and the source already holds what came after.
const replayLate = 15 * time.Minute
@@ -294,6 +294,7 @@ func (h *Holder) readyAsks(now time.Time) []func() {
asks = append(asks, h.turnWrites()...) // the merge turns, after the holds of this look (ADR 0548 §3)
asks = append(asks, h.settleMerges()...)
asks = append(asks, h.settleForgeMerges()...)
asks = append(asks, h.seedTrunks()...)
asks = append(asks, h.withdrawnKeys()...)
asks = append(h.cancels, asks...)
h.cancels = nil