Ask the planner what a pull request reaches; map a changed file onto modules in one place (hq ADR 0238)
touchedBy is now the only mapping of changed files onto modules — touched, added, and read by no build — and reachOfMerge the planner's whole answer with the dependency walk. The merge handler, the plan what-if, the merge gate's width and composition, and a pull request's check all ask it, so planning and gating cannot disagree. The gate composes the definitions of the modules a merge would rebuild or add, not every one in the tree, and the check says the dependents a merge would build after them.
This commit is contained in:
@@ -206,9 +206,25 @@ func judgeChange(ctx context.Context, in mergeCheckInput) (mergeVerdict, error)
|
||||
}
|
||||
change := base
|
||||
sources := sourcesOfFacts(in.facts)
|
||||
// **What the change moves is the planner's answer** (reachOfMerge, novox/hq ADR 0238), asked of the
|
||||
// snapshot as if the change were merged: the definitions composed with the change are those of the
|
||||
// modules a merge of it would rebuild, and of the modules it adds — not every definition in the tree,
|
||||
// which may differ from what the mesh holds for reasons that are not this change's (a module pinned
|
||||
// at an older commit, a main the mesh has not built yet).
|
||||
var reach *mergeReach
|
||||
if in.repository != "" && len(in.changed) > 0 {
|
||||
r, err := reachOfChange(in.facts, in.repository, in.changed, in.tree)
|
||||
if err != nil {
|
||||
v.Notes = append(v.Notes, "what a merge of the change would rebuild could not be worked out, so every "+
|
||||
"definition in its tree is judged: "+err.Error())
|
||||
} else {
|
||||
reach = &r
|
||||
}
|
||||
}
|
||||
if in.tree != "" {
|
||||
var failures []string
|
||||
change, failures, err = shelfWithChange(base, sources, in.facts, in.repository, in.tree, v.Modules)
|
||||
change, failures, err = shelfWithChange(base, sources, in.facts, in.repository, in.tree, v.Modules,
|
||||
scopeOfReach(reach, in.repository))
|
||||
if err != nil {
|
||||
return v, err
|
||||
}
|
||||
@@ -306,20 +322,16 @@ func judgeChange(ctx context.Context, in mergeCheckInput) (mergeVerdict, error)
|
||||
}
|
||||
}
|
||||
|
||||
if in.repository != "" && len(in.changed) > 0 {
|
||||
w, err := rebuildWidth(in.facts, in.repository, in.changed, in.tree)
|
||||
if err != nil {
|
||||
v.Notes = append(v.Notes, "the width of the rebuild could not be worked out: "+err.Error())
|
||||
} else {
|
||||
v.Width = &w
|
||||
if len(w.Unread) > 0 {
|
||||
v.Notes = append(v.Notes, fmt.Sprintf("%s read by no module's build, and rebuild nothing (issue 280)",
|
||||
readableList(w.Unread)))
|
||||
}
|
||||
if len(w.Modules) > wideRebuild {
|
||||
v.Warnings = append(v.Warnings, fmt.Sprintf("a merge rebuilds %d module(s) in %d tier(s)",
|
||||
len(w.Modules), w.Tiers))
|
||||
}
|
||||
if reach != nil {
|
||||
w := widthOf(*reach)
|
||||
v.Width = &w
|
||||
if len(w.Unread) > 0 {
|
||||
v.Notes = append(v.Notes, fmt.Sprintf("%s read by no module's build, and rebuild nothing (issue 280)",
|
||||
readableList(w.Unread)))
|
||||
}
|
||||
if len(w.Modules) > wideRebuild {
|
||||
v.Warnings = append(v.Warnings, fmt.Sprintf("a merge rebuilds %d module(s) in %d tier(s)",
|
||||
len(w.Modules), w.Tiers))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -443,8 +455,12 @@ var ignoredInTree = map[string]bool{".git": true, "vendor": true, "node_modules"
|
||||
// read by this controller's strict parser — the one the mesh runs, for a change to a catalogue — resolved
|
||||
// with stand-in builds, and read again as registration reads a built one. A module the repository held
|
||||
// and the tree no longer has is removed. Answers the shelf and what the tree itself fails.
|
||||
//
|
||||
// `scope` is the directories of the modules a merge of the change would rebuild or add, as the planner says
|
||||
// (scopeOfReach); a definition elsewhere in the tree is left as the mesh holds it. Nil judges every one.
|
||||
func shelfWithChange(base map[string]catalogue.Manifest, sources map[string]inventory.Source, f snapshot.Facts,
|
||||
repository, tree string, moved map[string]string) (map[string]catalogue.Manifest, []string, error) {
|
||||
repository, tree string, moved map[string]string, scope map[string]bool) (map[string]catalogue.Manifest, []string, error) {
|
||||
inScope := func(dir string) bool { return scope == nil || scope[dir] }
|
||||
out := make(map[string]catalogue.Manifest, len(base))
|
||||
for k, m := range base {
|
||||
out[k] = m
|
||||
@@ -477,16 +493,23 @@ func shelfWithChange(base map[string]catalogue.Manifest, sources map[string]inve
|
||||
}
|
||||
m, err := catalogue.ParseManifest(raw)
|
||||
if err != nil {
|
||||
failures = append(failures, fmt.Sprintf("%s: the controller judging this (%s) refuses it — %s",
|
||||
orRoot(dir), version, oneLine(err.Error())))
|
||||
if inScope(dir) {
|
||||
failures = append(failures, fmt.Sprintf("%s: the controller judging this (%s) refuses it — %s",
|
||||
orRoot(dir), version, oneLine(err.Error())))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if was, twice := found[m.Module]; twice {
|
||||
failures = append(failures, fmt.Sprintf("%s and %s both define %s: two definitions of one module",
|
||||
orRoot(was), orRoot(dir), m.Module))
|
||||
if inScope(dir) || inScope(was) {
|
||||
failures = append(failures, fmt.Sprintf("%s and %s both define %s: two definitions of one module",
|
||||
orRoot(was), orRoot(dir), m.Module))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
found[m.Module] = dir
|
||||
if !inScope(dir) {
|
||||
return nil
|
||||
}
|
||||
if s, held := sources[m.Module]; held && s.Repository != "" && !sameRepository(s.Repository,
|
||||
link.SourceMoved{Owner: ownerOf(repository), Repo: repoOf(repository)}) {
|
||||
failures = append(failures, fmt.Sprintf("%s defines %s, which the mesh builds from %s: two definitions "+
|
||||
@@ -538,6 +561,9 @@ func shelfWithChange(base map[string]catalogue.Manifest, sources map[string]inve
|
||||
if _, still := found[name]; still {
|
||||
continue
|
||||
}
|
||||
if !inScope(strings.Trim(s.Path, "/")) {
|
||||
continue
|
||||
}
|
||||
delete(out, name)
|
||||
moved[name] = "removed"
|
||||
if on := running[name]; len(on) > 0 {
|
||||
@@ -961,25 +987,32 @@ func standInKey() (string, error) {
|
||||
return base64.StdEncoding.EncodeToString(k.PublicKey().Bytes()), nil
|
||||
}
|
||||
|
||||
// mergeWidth is what a merge of the change would rebuild, computed the way the merge handler computes
|
||||
// it, from the modules, sources and edges the snapshot carries.
|
||||
func rebuildWidth(f snapshot.Facts, repository string, paths []string, tree string) (mergeWidthOf, error) {
|
||||
// reachOfChange is what a merge of the change would move and build — **the planner's own answer**
|
||||
// (reachOfMerge), asked of the modules, sources, reads and edges the snapshot carries, never a mapping of
|
||||
// the gate's own: what a merge touches and what follows it are decided in one place, so the gate and the
|
||||
// plan a merge makes cannot disagree (novox/hq ADR 0238).
|
||||
func reachOfChange(f snapshot.Facts, repository string, paths []string, tree string) (mergeReach, error) {
|
||||
owner, repo, found := strings.Cut(repository, "/")
|
||||
if !found {
|
||||
return mergeWidthOf{}, fmt.Errorf("%q is not owner/repository", repository)
|
||||
return mergeReach{}, fmt.Errorf("%q is not owner/repository", repository)
|
||||
}
|
||||
m := link.SourceMoved{Owner: owner, Repo: repo, Base: "main", Commit: "gate", Paths: paths}
|
||||
// Which changed directories hold a module, read from the change's tree as the forge's announcer reads
|
||||
// them at the merge commit (issue 278): a file inside one is that module's business, held or not.
|
||||
// them at the head (issue 278): what the planner is told of a module the mesh does not hold yet.
|
||||
if tree != "" {
|
||||
m.ModuleDirs, m.ModuleDirsSaid = moduleDirsIn(tree, paths), true
|
||||
for _, p := range paths {
|
||||
if _, err := os.Stat(filepath.Join(tree, p)); os.IsNotExist(err) {
|
||||
m.Removed = append(m.Removed, p)
|
||||
}
|
||||
}
|
||||
}
|
||||
var entries []inventory.Entry
|
||||
read := map[string][]inventory.ReadRepository{}
|
||||
for _, mod := range f.Modules {
|
||||
var manifest catalogue.Manifest
|
||||
if err := json.Unmarshal(mod.Manifest, &manifest); err != nil {
|
||||
return mergeWidthOf{}, err
|
||||
return mergeReach{}, err
|
||||
}
|
||||
entries = append(entries, inventory.Entry{Manifest: manifest, Provided: mod.Provided,
|
||||
Source: inventory.Source{Repository: mod.Repository, Path: mod.Path, BuiltFrom: mod.Commit}})
|
||||
@@ -991,52 +1024,39 @@ func rebuildWidth(f snapshot.Facts, repository string, paths []string, tree stri
|
||||
for _, e := range f.Edges {
|
||||
edges = append(edges, inventory.Edge{From: e.From, To: e.To, Kind: e.Kind})
|
||||
}
|
||||
var from []inventory.Entry
|
||||
for _, e := range entries {
|
||||
if !e.Provided && sourceIs(e.Source, m) {
|
||||
from = append(from, e)
|
||||
return reachOfMerge(m, entries, read, edges), nil
|
||||
}
|
||||
|
||||
// widthOf is how wide the rebuild of a reach is.
|
||||
func widthOf(r mergeReach) mergeWidthOf {
|
||||
w := mergeWidthOf{Tiers: len(r.Plan.Tiers), Unread: r.Unread}
|
||||
for name := range r.Plan.Modules {
|
||||
w.Modules = append(w.Modules, name)
|
||||
}
|
||||
sort.Strings(w.Modules)
|
||||
return w
|
||||
}
|
||||
|
||||
// scopeOfReach is the directories, in the change's repository, of the modules a reach rebuilds or adds;
|
||||
// nil — every definition judged — when there is no reach.
|
||||
func scopeOfReach(r *mergeReach, repository string) map[string]bool {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
scope := map[string]bool{}
|
||||
same := link.SourceMoved{Owner: ownerOf(repository), Repo: repoOf(repository)}
|
||||
for _, e := range append(append([]inventory.Entry{}, r.Touched...), r.Deleted...) {
|
||||
if sameRepository(e.Source.Repository, same) {
|
||||
scope[strings.Trim(e.Source.Path, "/")] = true
|
||||
}
|
||||
}
|
||||
touched := whatTheMergeTouched(from, entries, m)
|
||||
var names []string
|
||||
for _, e := range touched {
|
||||
names = append(names, e.Manifest.Module)
|
||||
}
|
||||
for _, e := range entries {
|
||||
if readsFrom(read[e.Manifest.Module], m) && !slices.Contains(names, e.Manifest.Module) {
|
||||
names = append(names, e.Manifest.Module)
|
||||
for _, d := range r.Added {
|
||||
if d == "." {
|
||||
d = ""
|
||||
}
|
||||
scope[d] = true
|
||||
}
|
||||
w := mergeWidthOf{}
|
||||
if len(names) > 0 {
|
||||
p := planOfMerge(m, names, edges)
|
||||
w.Tiers = len(p.Tiers)
|
||||
for name := range p.Modules {
|
||||
w.Modules = append(w.Modules, name)
|
||||
}
|
||||
sort.Strings(w.Modules)
|
||||
}
|
||||
// The paths no build reads: in no directory of a module the mesh holds from this repository, or one
|
||||
// the tree holds.
|
||||
var dirs []string
|
||||
for _, e := range from {
|
||||
if d := strings.Trim(e.Source.Path, "/"); d != "" {
|
||||
dirs = append(dirs, d+"/")
|
||||
}
|
||||
}
|
||||
for _, d := range m.ModuleDirs {
|
||||
dirs = append(dirs, strings.Trim(d, "/")+"/")
|
||||
}
|
||||
for _, p := range paths {
|
||||
inModule := false
|
||||
for _, d := range dirs {
|
||||
inModule = inModule || strings.HasPrefix(p, d)
|
||||
}
|
||||
if !inModule && len(dirs) > 0 {
|
||||
w.Unread = append(w.Unread, p)
|
||||
}
|
||||
}
|
||||
return w, nil
|
||||
return scope
|
||||
}
|
||||
|
||||
// moduleDirsIn are the directories above the changed paths, never the root, that hold a module.json in
|
||||
|
||||
Reference in New Issue
Block a user