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:
+138
-29
@@ -6,6 +6,7 @@ import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -616,14 +617,24 @@ func lastLookAt(entries []inventory.Entry, m link.SourceMoved) time.Time {
|
||||
}
|
||||
|
||||
// whatTheMergeTouched narrows the modules built from a repository to the ones the merge changed: **a
|
||||
// changed file touches exactly the modules whose build reads it** (novox/hq issue 280, ADR 0237 as amended).
|
||||
// changed file touches exactly the modules whose build reads it** (novox/hq issue 280, ADR 0238). It is
|
||||
// touchedBy's first answer; touchedBy is the one place the mesh maps a changed file onto its modules.
|
||||
func whatTheMergeTouched(candidates, known []inventory.Entry, m link.SourceMoved) []inventory.Entry {
|
||||
touched, _, _ := touchedBy(candidates, known, m)
|
||||
return touched
|
||||
}
|
||||
|
||||
// touchedBy maps a merge's changed files onto the mesh's modules — **the one place it is done**, for the
|
||||
// merge handler, the release planner's what-if, the merge gate and a pull request's check alike (novox/hq
|
||||
// ADR 0238), so planning and gating cannot disagree about what a change touches.
|
||||
//
|
||||
// What a build reads is the module's own directory — the builder clones the repository and builds within
|
||||
// that directory alone: the manifest, the recipes, the bundles' sources, the Docker context — or the whole
|
||||
// repository for a module built from its root. A second repository a recipe packages (an artifact's
|
||||
// `context`) is read too, and that is the build record's `read`, answered by readsFrom beside this. So a
|
||||
// changed file inside a module's directory is that module's; a file in no module's directory — a script
|
||||
// at the root, a README, CI configuration, a directory of notes — is read by no build and touches nothing.
|
||||
// **A changed file touches exactly the modules whose build reads it.** What a build reads is the module's
|
||||
// own directory — the builder clones the repository and builds within that directory alone: the manifest,
|
||||
// the recipes, the bundles' sources, the Docker context — or the whole repository for a module built from
|
||||
// its root. A second repository a recipe packages (an artifact's `context`) is read too; that is the build
|
||||
// record's `read`, answered by readsFrom in mergeCandidates. So a changed file inside a module's directory
|
||||
// is that module's; a file in no module's directory — a script at the root, a README, CI configuration —
|
||||
// is read by no build and touches nothing: it is answered as `unread`.
|
||||
//
|
||||
// **It used to be read as shared code**, rebuilding everything built from the repository, on the theory
|
||||
// that a root file might be a build input (04-ISSUES/131). No build reads one: the theory cost a rebuild
|
||||
@@ -631,23 +642,131 @@ func lastLookAt(entries []inventory.Entry, m link.SourceMoved) time.Time {
|
||||
// held by no machine (278) and a new module's directory (252), each patched as an exception to a rule that
|
||||
// was wrong. Rebuilding too much is not the safe direction when every rebuild is a rollout.
|
||||
//
|
||||
// Nothing said about the files, or not all of them said, is still everything: what is not known cannot be
|
||||
// narrowed. `known` is kept for the callers; which directories hold a module is no longer needed to read a
|
||||
// change, because a file is a module's only by being inside it.
|
||||
func whatTheMergeTouched(candidates, known []inventory.Entry, m link.SourceMoved) []inventory.Entry {
|
||||
_ = known
|
||||
if len(m.Paths) == 0 || m.PathsTruncated {
|
||||
return candidates
|
||||
}
|
||||
var out []inventory.Entry
|
||||
for _, e := range candidates {
|
||||
if strings.Trim(e.Source.Path, "/") == "" || anyInside(m.Paths, e.Source.Path) {
|
||||
out = append(out, e)
|
||||
// `added` is the directories the change holds a module in that no module of this repository is known
|
||||
// from — said by the announcer at the head (issue 278), or a module.json among the changed files — `.`
|
||||
// for the root: a new module, which a check judges before it merges and a merge does not build.
|
||||
//
|
||||
// Nothing said about the files, or not all of them said, is still everything: what is not known cannot
|
||||
// be narrowed.
|
||||
func touchedBy(candidates, known []inventory.Entry, m link.SourceMoved) (touched []inventory.Entry, added, unread []string) {
|
||||
knownDirs := map[string]bool{}
|
||||
for _, e := range known {
|
||||
if !e.Provided && sameRepository(e.Source.Repository, m) {
|
||||
knownDirs[strings.Trim(e.Source.Path, "/")] = true
|
||||
}
|
||||
}
|
||||
newDir := map[string]bool{}
|
||||
for _, d := range saidModuleDirs(m) {
|
||||
if !knownDirs[d] {
|
||||
newDir[d] = true
|
||||
}
|
||||
}
|
||||
for _, p := range m.Paths {
|
||||
p = strings.Trim(p, "/")
|
||||
if p != moduleManifestFile && !strings.HasSuffix(p, "/"+moduleManifestFile) {
|
||||
continue
|
||||
}
|
||||
d := strings.TrimSuffix(strings.TrimSuffix(p, moduleManifestFile), "/")
|
||||
if !knownDirs[d] {
|
||||
if d == "" {
|
||||
d = "."
|
||||
}
|
||||
newDir[d] = true
|
||||
}
|
||||
}
|
||||
for d := range newDir {
|
||||
added = append(added, d)
|
||||
}
|
||||
sort.Strings(added)
|
||||
|
||||
if len(m.Paths) == 0 || m.PathsTruncated {
|
||||
return candidates, added, nil
|
||||
}
|
||||
for _, e := range candidates {
|
||||
if strings.Trim(e.Source.Path, "/") == "" || anyInside(m.Paths, e.Source.Path) {
|
||||
touched = append(touched, e)
|
||||
}
|
||||
}
|
||||
for _, p := range m.Paths {
|
||||
read := newDir["."]
|
||||
for _, e := range candidates {
|
||||
read = read || strings.Trim(e.Source.Path, "/") == "" || inside(p, e.Source.Path)
|
||||
}
|
||||
for d := range newDir {
|
||||
read = read || inside(p, d)
|
||||
}
|
||||
if !read {
|
||||
unread = append(unread, p)
|
||||
}
|
||||
}
|
||||
return touched, added, unread
|
||||
}
|
||||
|
||||
// mergeReach is what a merge of a repository's branch reaches, as the planner reckons it: the planner's
|
||||
// one answer, given to the release planner's what-if, the merge gate and a pull request's check (novox/hq
|
||||
// ADR 0238). The merge handler acts on the same pieces — mergeCandidates, touchedBy, splitDeleted,
|
||||
// planOfMerge — as it goes.
|
||||
type mergeReach struct {
|
||||
// Touched are the modules built from the repository and branch whose build reads a changed file, and
|
||||
// Deleted those of them whose manifest the merge removes.
|
||||
Touched, Deleted []inventory.Entry
|
||||
// Packaging are the modules whose build packages source from the repository.
|
||||
Packaging []inventory.Entry
|
||||
// Already counts the modules built from the repository that are built from this very commit.
|
||||
Already int
|
||||
// Added are the directories the change holds a new module in; Unread the changed files no build reads.
|
||||
Added, Unread []string
|
||||
// Plan is what moves and everything that follows it along the catalogue's dependencies, tiered —
|
||||
// what a merge would build. Empty when nothing moves.
|
||||
Plan inventory.Plan
|
||||
}
|
||||
|
||||
// Moved are the modules the merge moves itself, by name: touched, deleted and packaging.
|
||||
func (r mergeReach) Moved() []string {
|
||||
var out []string
|
||||
for _, group := range [][]inventory.Entry{r.Touched, r.Deleted, r.Packaging} {
|
||||
for _, e := range group {
|
||||
out = append(out, e.Manifest.Module)
|
||||
}
|
||||
}
|
||||
sort.Strings(out)
|
||||
return slices.Compact(out)
|
||||
}
|
||||
|
||||
// Dependents are the modules the plan builds after the moved ones because they stand on them.
|
||||
func (r mergeReach) Dependents() []string {
|
||||
moved := map[string]bool{}
|
||||
for _, name := range r.Moved() {
|
||||
moved[name] = true
|
||||
}
|
||||
var out []string
|
||||
for name := range r.Plan.Modules {
|
||||
if !moved[name] {
|
||||
out = append(out, name)
|
||||
}
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// reachOfMerge is what a merge of these changed files into this branch would move and build, without
|
||||
// acting: the merge handler's own judgement and the release plan's dependency walk.
|
||||
func reachOfMerge(m link.SourceMoved, entries []inventory.Entry, read map[string][]inventory.ReadRepository,
|
||||
edges []inventory.Edge) mergeReach {
|
||||
from, packaging, already := mergeCandidates(m, entries, read)
|
||||
touched, added, unread := touchedBy(from, entries, m)
|
||||
kept, deleted := splitDeleted(touched, m)
|
||||
r := mergeReach{Touched: kept, Deleted: deleted, Packaging: packaging, Already: already, Added: added, Unread: unread}
|
||||
var building []string
|
||||
for _, e := range append(append([]inventory.Entry{}, kept...), packaging...) {
|
||||
building = append(building, e.Manifest.Module)
|
||||
}
|
||||
if len(building) > 0 {
|
||||
r.Plan = planOfMerge(m, building, edges)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// saidModuleDirs is the directories the announcer says hold a module at the merge commit (novox/hq
|
||||
// issue 278): a changed file in one of them is that module's business and nobody else's — the
|
||||
// catalogue's reference module, held by no machine, whose code a merge touched beside its manifest,
|
||||
@@ -674,16 +793,6 @@ func inside(path, dir string) bool {
|
||||
return path == dir || strings.HasPrefix(path, dir+"/")
|
||||
}
|
||||
|
||||
// insideAny is whether a changed file is in any of these directories.
|
||||
func insideAny(path string, dirs []string) bool {
|
||||
for _, dir := range dirs {
|
||||
if inside(path, dir) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// anyInside is whether any of these changed files is in a directory.
|
||||
func anyInside(paths []string, dir string) bool {
|
||||
for _, p := range paths {
|
||||
|
||||
Reference in New Issue
Block a user