A late merge is named however its modules read since the cut; a walk builds the commits it carries (hq ADR 0276 review)

This commit is contained in:
jochen
2026-10-10 13:39:21 +02:00
parent 96fc4209d3
commit e1499d4196
6 changed files with 229 additions and 60 deletions
+87 -51
View File
@@ -137,7 +137,23 @@ func (f following) hearMerge(ctx context.Context, m link.SourceMoved, now time.T
if err != nil {
return notNow(err)
}
if from, packaging, already := mergeCandidates(m, entries, read); len(from) == 0 && len(packaging) == 0 {
merged, err := time.Parse(time.RFC3339Nano, m.MergedAt)
if err != nil {
merged = now
}
// **What it touches, history or not** (review of this change): a merge heard after a later merge of its
// branch was cut reads as history for every module that cut marked seen, and was dropped from the record
// here, never named by the walk that carried it.
raw := m
raw.MergedAt = ""
touches := wouldMove(raw, entries, read)
var later []inventory.BatchedMerge
if len(touches) > 0 {
if later, err = inv.LaterMergesOf(ctx, repository, m.Base, merged.UTC()); err != nil {
return notNow(err)
}
}
if from, packaging, already := mergeCandidates(m, entries, read); len(from) == 0 && len(packaging) == 0 && len(later) == 0 {
// "Already built from it" and "nothing reads it" are different facts, and reading the first
// as the second sends somebody looking for a broken trigger when the mesh is up to date.
if already > 0 {
@@ -153,10 +169,9 @@ func (f following) hearMerge(ctx context.Context, m link.SourceMoved, now time.T
return notNow(err)
}
defer release()
merged, err := time.Parse(time.RFC3339Nano, m.MergedAt)
if err != nil {
merged = now
// Again under the hold: a controller handing over may have kept it since.
if _, known, err := inv.MergeOf(ctx, repository, m.Commit); err != nil || known {
return notNow(err)
}
event, err := json.Marshal(m)
if err != nil {
@@ -166,7 +181,7 @@ func (f following) hearMerge(ctx context.Context, m link.SourceMoved, now time.T
Heard: now, Event: event}
// **A merge heard after a later merge of its branch** is answered by the walk holding that one.
if p, ok, err := answeredByALaterMerge(ctx, inv, heard, m, entries, read); err != nil {
if p, ok, err := answeredByALaterMerge(ctx, inv, heard, touches); err != nil {
return notNow(err)
} else if ok {
heard.Plan = p.ID
@@ -190,7 +205,7 @@ func (f following) hearMerge(ctx context.Context, m link.SourceMoved, now time.T
return notNow(err)
}
heard.Plan = batch.ID
if _, err := inv.AddMerge(ctx, heard); err != nil {
if added, err := inv.AddMerge(ctx, heard); err != nil || !added {
return notNow(err)
}
if err := keepBatch(ctx, inv, &batch, now, ""); err != nil {
@@ -207,12 +222,11 @@ func (f following) hearMerge(ctx context.Context, m link.SourceMoved, now time.T
// later merge, which contains it. A failed or stopped walk answers nothing more, and a walk folded into
// another is followed to that one. False when none does: the merge joins the next batch.
func answeredByALaterMerge(ctx context.Context, inv *inventory.Inventory, heard inventory.BatchedMerge,
m link.SourceMoved, entries []inventory.Entry, read map[string][]inventory.ReadRepository) (inventory.Plan, bool, error) {
moves []inventory.Entry) (inventory.Plan, bool, error) {
later, err := inv.LaterMergesOf(ctx, heard.Repository, heard.Branch, heard.Merged)
if err != nil || len(later) == 0 {
return inventory.Plan{}, false, err
}
moves := wouldMove(m, entries, read)
for _, l := range later {
p, err := followTakeOver(ctx, inv, l.Plan)
if err != nil {
@@ -240,8 +254,12 @@ func followTakeOver(ctx context.Context, inv *inventory.Inventory, id string) (i
return p, err
}
// buildsEvery says a walk builds every one of the modules.
// buildsEvery says a walk builds every one of the modules, and that there is one: a merge that moves nothing
// is no walk's to name.
func buildsEvery(p inventory.Plan, modules []inventory.Entry) bool {
if len(modules) == 0 {
return false
}
for _, e := range modules {
if _, in := p.Modules[e.Manifest.Module]; !in {
return false
@@ -381,6 +399,15 @@ func carriedOf(merges []inventory.BatchedMerge) ([]inventory.PlanCommit, []inven
return commits, named
}
// spelledOf is a kept merge's repository as the forge spelled it; empty when its announcement does not say.
func spelledOf(m inventory.BatchedMerge) string {
var e link.SourceMoved
if json.Unmarshal(m.Event, &e) == nil && e.Owner != "" {
return e.Owner + "/" + e.Repo
}
return ""
}
// laterMerge says a was merged after b: by the forge's merge time, then by when each was heard.
func laterMerge(a, b inventory.BatchedMerge) bool {
if !a.Merged.Equal(b.Merged) {
@@ -406,15 +433,21 @@ func nameMerges(ctx context.Context, inv *inventory.Inventory, p *inventory.Plan
if err != nil {
return err
}
_, named := carriedOf(merges)
// The walk's own commits stand: a merge heard late is carried by the one of its repository.
for i := range named {
if c := p.CommitOf(named[i].Repository); c != "" && c != named[i].Commit {
named[i].Carried = c
} else if c == named[i].Commit {
named[i].Carried = ""
// The walk's own commits stand: a merge heard late is carried by the one of its repository's branch.
var named []inventory.PlanMerge
for _, m := range merges {
n := inventory.PlanMerge{Repository: m.Repository, Commit: m.Commit}
if e := spelledOf(m); e != "" {
n.Repository = e
}
if c := p.CommitOn(m.Repository, m.Branch); c != "" && c != m.Commit {
n.Carried = c
}
named = append(named, n)
}
sort.SliceStable(named, func(i, j int) bool {
return strings.ToLower(named[i].Repository) < strings.ToLower(named[j].Repository)
})
if p.Delivery == nil {
p.Delivery = &inventory.PlanDelivery{}
}
@@ -493,7 +526,11 @@ func cutBatchesHeld(ctx context.Context, open *stores, now time.Time) error {
}
if len(alone) > 0 {
if len(waiting) > 0 {
return nil // the waiting walk is the one open; the search walks after it
// The waiting walk is the one open; the search walks after it, and the batch waits behind both.
if batch != nil {
return keepBatch(ctx, inv, batch, now, waiting[0].ID)
}
return nil
}
return walkAlone(ctx, open, alone[0], now)
}
@@ -600,6 +637,10 @@ func planBatch(ctx context.Context, open *stores, batch *inventory.Plan, carry [
var moved []string
var members []batchMember
for _, m := range combinedMerges(merges) {
// **News, whatever was seen since** (review of this change): each merge was judged when it was heard, and
// a cut, a fold or a failed walk since marks its modules seen — so a merge walked alone after a failed
// walk, or folded, would read as history and move nothing. When it was made is not asked again here.
m.MergedAt = ""
names, err := movesOfMerge(ctx, inv, m, entries, read)
if err != nil {
return err
@@ -686,12 +727,17 @@ func planBatch(ctx context.Context, open *stores, batch *inventory.Plan, carry [
}
// combinedMerges is a batch's merges as one merge per repository's branch: the latest, with every file the
// merges of it changed, removed and said to be a module's — on a linear trunk the latest contains the others.
// merges of it changed and said to be a module's — on a linear trunk the latest contains the others. A file is
// removed when the last merge of the batch that changed it removed it. merges are in the order they were made.
func combinedMerges(merges []inventory.BatchedMerge) []link.SourceMoved {
type combined struct {
latest inventory.BatchedMerge
m link.SourceMoved
said bool
latest inventory.BatchedMerge
m link.SourceMoved
said bool
removed map[string]bool
paths []string
dirs []string
cut bool
}
by := map[string]*combined{}
var keys []string
@@ -703,49 +749,39 @@ func combinedMerges(merges []inventory.BatchedMerge) []link.SourceMoved {
k := strings.ToLower(b.Repository) + "\x00" + b.Branch
c, ok := by[k]
if !ok {
c = &combined{latest: b, m: e, said: e.ModuleDirsSaid}
c = &combined{latest: b, m: e, said: true, removed: map[string]bool{}}
by[k] = c
keys = append(keys, k)
} else {
paths, removed, dirs, truncated := c.m.Paths, c.m.Removed, c.m.ModuleDirs, c.m.PathsTruncated
said := c.said && e.ModuleDirsSaid
if laterMerge(b, c.latest) {
c.latest, c.m = b, e
}
c.m.Paths = union(paths, e.Paths)
c.m.Removed = union(removed, e.Removed)
c.m.ModuleDirs = union(dirs, e.ModuleDirs)
c.m.PathsTruncated = truncated || e.PathsTruncated
c.said = said
} else if laterMerge(b, c.latest) {
c.latest, c.m = b, e
}
c.said = c.said && e.ModuleDirsSaid
c.cut = c.cut || e.PathsTruncated
c.paths, c.dirs = union(c.paths, e.Paths), union(c.dirs, e.ModuleDirs)
for _, path := range e.Paths {
c.removed[path] = slices.Contains(e.Removed, path)
}
for _, path := range e.Removed {
c.removed[path] = true
}
}
sort.Strings(keys)
var out []link.SourceMoved
for _, k := range keys {
c := by[k]
c.m.ModuleDirsSaid = c.said
// A file a later merge of the batch brought back is no longer removed.
var removed []string
for _, r := range c.m.Removed {
if !restoredLater(r, c.latest, merges) {
removed = append(removed, r)
m := c.m
m.Paths, m.ModuleDirs, m.PathsTruncated, m.ModuleDirsSaid = c.paths, c.dirs, c.cut, c.said
m.Removed = nil
for _, path := range c.paths {
if c.removed[path] {
m.Removed = append(m.Removed, path)
}
}
c.m.Removed = removed
out = append(out, c.m)
out = append(out, m)
}
return out
}
// restoredLater says a file removed by one merge of a batch was changed, not removed, by the latest.
func restoredLater(path string, latest inventory.BatchedMerge, _ []inventory.BatchedMerge) bool {
var e link.SourceMoved
if json.Unmarshal(latest.Event, &e) != nil {
return false
}
return slices.Contains(e.Paths, path) && !slices.Contains(e.Removed, path)
}
// union is a and b without repeats, in order.
func union(a, b []string) []string {
out := append([]string{}, a...)