The fetch walks the siblings for Go modules only when one is a Go module at its root; the beside workspace's go directive is the highest among its modules (issue 1921)
mesh/check checking 36c19694c9df on 0f5201387891
mesh/delivery merged: source.main moved to novox/mesh-catalog@0f5201387891

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
This commit is contained in:
2026-11-07 11:49:42 +01:00
parent 77b30bd296
commit 36c19694c9
2 changed files with 62 additions and 13 deletions
@@ -1,6 +1,7 @@
package main package main
import ( import (
"context"
"encoding/json" "encoding/json"
"os" "os"
"path/filepath" "path/filepath"
@@ -84,7 +85,7 @@ func TestASiblingsCommitChangesTheDigestOfAPackageWhoseTestsReachAReaderOfTheSib
// The fetch writes the siblings it placed, which the digest reads. // The fetch writes the siblings it placed, which the digest reads.
func TestTheFetchRecordsTheSiblingsItPlaced(t *testing.T) { func TestTheFetchRecordsTheSiblingsItPlaced(t *testing.T) {
// Every checkout writes this go.mod: the placed sibling is a Go module at its root, so the fetch downloads its graph. // Every checkout writes this go.mod: the placed sibling is a Go module at its root, so the fetch downloads its graph.
f := &fakeStep{gomods: "go.mod\n", files: map[string]string{"go.mod": "module git.novox.be/novox/mesh-sdk/go\n\ngo 1.26.0\n"}} f := &fakeStep{gomods: "go.mod\n", files: map[string]string{"go.mod": "module git.novox.be/novox/mesh-sdk/go\n\ngo 1.27.1\n"}}
m, states := fetchModule(t, f) m, states := fetchModule(t, f)
args := map[string]any{"repository": "novox/mesh-controller", "commit": commitSHA, "beside": []any{ args := map[string]any{"repository": "novox/mesh-controller", "commit": commitSHA, "beside": []any{
map[string]any{"repository": "novox/mesh-sdk", "commit": commitSHA}}} map[string]any{"repository": "novox/mesh-sdk", "commit": commitSHA}}}
@@ -113,6 +114,23 @@ func TestTheFetchRecordsTheSiblingsItPlaced(t *testing.T) {
} }
} }
} }
// The go.work's go directive is the highest of the laid modules.
if work, _ := os.ReadFile(filepath.Join(m.Work, res.Workspace, "beside-0.work")); !strings.HasPrefix(string(work), "go 1.27.1\n") {
t.Errorf("the workspace file begins %q, want go 1.27.1", work)
}
// No sibling a Go module at its root: nothing is walked, so a directory the walk cannot read does not fail the fetch.
plain := t.TempDir()
locked := filepath.Join(plain, "beside", "docs", "private")
if err := os.MkdirAll(locked, 0o700); err != nil {
t.Fatal(err)
}
if err := os.Chmod(locked, 0); err != nil {
t.Fatal(err)
}
t.Cleanup(func() { os.Chmod(locked, 0o700) })
if err := m.downloadBeside(context.Background(), plain, filepath.Join(plain, "tree"), nil); err != nil {
t.Errorf("a fetch with no Go-module sibling walked the siblings: %v", err)
}
if n != 1 { if n != 1 {
t.Errorf("%q ran %d times, want once:\n%s", want, n, strings.Join(f.lines(), "\n")) t.Errorf("%q ran %d times, want once:\n%s", want, n, strings.Join(f.lines(), "\n"))
} }
@@ -7,6 +7,7 @@ import (
"io/fs" "io/fs"
"os" "os"
"path/filepath" "path/filepath"
"strconv"
"strings" "strings"
) )
@@ -25,7 +26,7 @@ const zeroVersion = "v0.0.0-00010101000000-000000000000"
func workspaceOf(first string, candidates []string) (string, error) { func workspaceOf(first string, candidates []string) (string, error) {
paths := map[string]string{} paths := map[string]string{}
for _, d := range candidates { for _, d := range candidates {
path, _, err := goModOf(d) path, _, _, err := goModOf(d)
if err != nil { if err != nil {
return "", err return "", err
} }
@@ -35,11 +36,12 @@ func workspaceOf(first string, candidates []string) (string, error) {
} }
var use, replace strings.Builder var use, replace strings.Builder
laid := map[string]bool{} laid := map[string]bool{}
highest := "1.26.0" // the go directive is the highest among the laid modules, never lower than this
queue := []string{first} queue := []string{first}
for len(queue) > 0 { for len(queue) > 0 {
d := queue[0] d := queue[0]
queue = queue[1:] queue = queue[1:]
path, needs, err := goModOf(d) path, needs, goV, err := goModOf(d)
if err != nil { if err != nil {
return "", err return "", err
} }
@@ -47,6 +49,9 @@ func workspaceOf(first string, candidates []string) (string, error) {
continue continue
} }
laid[path] = true laid[path] = true
if goNewer(goV, highest) {
highest = goV
}
fmt.Fprintf(&use, "\t%s\n", d) fmt.Fprintf(&use, "\t%s\n", d)
fmt.Fprintf(&replace, "replace %s %s => %s\n", path, zeroVersion, d) fmt.Fprintf(&replace, "replace %s %s => %s\n", path, zeroVersion, d)
for _, need := range needs { for _, need := range needs {
@@ -60,28 +65,48 @@ func workspaceOf(first string, candidates []string) (string, error) {
queue = append(queue, dir) queue = append(queue, dir)
} }
} }
return "go 1.26.0\n\nuse (\n" + use.String() + ")\n\n" + replace.String(), nil return "go " + highest + "\n\nuse (\n" + use.String() + ")\n\n" + replace.String(), nil
} }
// goModOf is the module path of the go.mod in d and the modules it requires at the zero version. // goNewer reports whether the go directive a is higher than b (dotted numbers; a missing part is 0).
func goModOf(d string) (path string, needs []string, err error) { func goNewer(a, b string) bool {
x, y := strings.Split(a, "."), strings.Split(b, ".")
for i := 0; i < len(x) || i < len(y); i++ {
var p, q int
if i < len(x) {
p, _ = strconv.Atoi(x[i])
}
if i < len(y) {
q, _ = strconv.Atoi(y[i])
}
if p != q {
return p > q
}
}
return false
}
// goModOf is the module path of the go.mod in d, the modules it requires at the zero version and its go directive.
func goModOf(d string) (path string, needs []string, goV string, err error) {
raw, err := os.ReadFile(filepath.Join(d, "go.mod")) raw, err := os.ReadFile(filepath.Join(d, "go.mod"))
if err != nil { if err != nil {
return "", nil, err return "", nil, "", err
} }
for _, line := range strings.Split(string(raw), "\n") { for _, line := range strings.Split(string(raw), "\n") {
f := strings.Fields(strings.TrimPrefix(strings.TrimSpace(line), "require ")) f := strings.Fields(strings.TrimPrefix(strings.TrimSpace(line), "require "))
switch { switch {
case len(f) == 2 && f[0] == "module" && path == "": case len(f) == 2 && f[0] == "module" && path == "":
path = strings.Trim(f[1], `"`) path = strings.Trim(f[1], `"`)
case len(f) == 2 && f[0] == "go" && goV == "":
goV = f[1]
case len(f) >= 2 && f[1] == zeroVersion: case len(f) >= 2 && f[1] == zeroVersion:
needs = append(needs, strings.Trim(f[0], `"`)) needs = append(needs, strings.Trim(f[0], `"`))
} }
} }
if path == "" { if path == "" {
return "", nil, fmt.Errorf("%s/go.mod names no module", d) return "", nil, "", fmt.Errorf("%s/go.mod names no module", d)
} }
return path, needs, nil return path, needs, goV, nil
} }
// downloadBeside downloads, where the network is, the Go modules of the graph that the tests of a workspace's siblings // downloadBeside downloads, where the network is, the Go modules of the graph that the tests of a workspace's siblings
@@ -99,15 +124,21 @@ func (m *Module) downloadBeside(ctx context.Context, root, tree string, dirs []s
if err != nil { if err != nil {
return err return err
} }
var candidates, roots []string var candidates, roots, all []string
for _, d := range dirs {
candidates = append(candidates, filepath.Join(tree, filepath.FromSlash(d)))
}
for _, e := range entries { for _, e := range entries {
dir := filepath.Join(beside, e.Name()) dir := filepath.Join(beside, e.Name())
all = append(all, dir)
if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil { if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil {
roots = append(roots, dir) roots = append(roots, dir)
} }
}
if len(roots) == 0 {
return nil // no sibling is a Go module at its root: nothing is walked, nothing downloaded
}
for _, d := range dirs {
candidates = append(candidates, filepath.Join(tree, filepath.FromSlash(d)))
}
for _, dir := range all {
err := filepath.WalkDir(dir, func(p string, d fs.DirEntry, err error) error { err := filepath.WalkDir(dir, func(p string, d fs.DirEntry, err error) error {
if err != nil { if err != nil {
return err return err