package main import ( "sort" "strings" "github.com/novox/mesh-controller/internal/inventory" ) // What relates two changes (novox/hq ADR 0304 decision 1, as ADR 0306 decision 3 reads it): two changes are // related when they share a repository, a module, a delivery group's head, or an edge of the module graph // between their modules. A module is every one the walk builds or sends: the modules its merges moved and the // modules that depend on them, not only the ones the merges touched. Unrelated changes may walk beside each // other (ADR 0304 decision 2, ADR 0282 decision 3); nothing walks so yet, and nothing but tests reads this. // footprint is what one change reaches: its repositories (lower case), its modules, and its delivery group // heads (the head branch names of its merges). unknown says a merge of it was kept without the modules it // moves, so what it reaches is not known: it is related to every change, never wrongly to none. type footprint struct { repos, modules, heads map[string]bool unknown string } func newFootprint() footprint { return footprint{repos: map[string]bool{}, modules: map[string]bool{}, heads: map[string]bool{}} } // footprintOfWalk is a walk's footprint: the repositories it carries and the modules it plans. A walk keeps no // head of its merges; kept are the merges the record holds for it, whose announcements say their heads, and // may be nil. func footprintOfWalk(p inventory.Plan, kept []inventory.BatchedMerge) footprint { f := newFootprint() for _, c := range p.Carried() { f.repos[strings.ToLower(c.Repository)] = true } for name := range p.Modules { f.modules[name] = true } for _, m := range kept { f.repos[strings.ToLower(m.Repository)] = true if h := announcedOf(m).Head; h != "" { f.heads[h] = true } } return f } // footprintOfBatch is an assembling batch's footprint: each merge's repository and head, and the modules its // merges moved when heard, widened to their dependents (planOfMoves). A merge kept with no moves (one owed a // later look, or kept for its packaging) is judged again at the cut and may move anything then: its batch is // unknown, related to everything. func footprintOfBatch(merges []inventory.BatchedMerge, edges []inventory.Edge) footprint { f := newFootprint() var moved []string for _, m := range merges { k := announcedOf(m) f.repos[strings.ToLower(m.Repository)] = true if k.Head != "" { f.heads[k.Head] = true } if len(k.Moves) == 0 && f.unknown == "" { f.unknown = "the modules a merge of " + m.Repository + " moves are not known until its batch is cut" } moved = append(moved, k.Moves...) } for _, name := range reachableFrom(moved, edges) { f.modules[name] = true } return f } // related says whether two changes are related, and the first reason in plain words; false and empty when // they are not. func related(a, b footprint, edges []inventory.Edge) (bool, string) { if a.unknown != "" { return true, a.unknown } if b.unknown != "" { return true, b.unknown } if r := firstShared(a.repos, b.repos); r != "" { return true, "both change " + r } if m := firstShared(a.modules, b.modules); m != "" { return true, "both move " + m } if h := firstShared(a.heads, b.heads); h != "" { return true, "both are members of group " + h } for _, e := range edges { if a.modules[e.From] && b.modules[e.To] || b.modules[e.From] && a.modules[e.To] { return true, e.From + " depends on " + e.To + " (" + e.Kind + ")" } } return false, "" } // firstShared is the first key, in order, both sets hold; empty when none. func firstShared(a, b map[string]bool) string { var both []string for k := range a { if b[k] { both = append(both, k) } } if len(both) == 0 { return "" } sort.Strings(both) return both[0] }