From 1332f8b5234b593568cd87874442b4086a019ea1 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 4 Nov 2026 19:38:50 +0100 Subject: [PATCH 1/2] Ask git ls-tree for nested named paths in rounds: given go and go/seats/x together it lists go's children, so mesh-contracts' preview failed (issue 1966) Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig --- modules/mesh-build-go/buildstep/trees.go | 53 +++++++++++++++---- .../buildstep/trees_integration_test.go | 12 +++++ 2 files changed, 55 insertions(+), 10 deletions(-) diff --git a/modules/mesh-build-go/buildstep/trees.go b/modules/mesh-build-go/buildstep/trees.go index 0371f162..fb41c758 100644 --- a/modules/mesh-build-go/buildstep/trees.go +++ b/modules/mesh-build-go/buildstep/trees.go @@ -82,18 +82,20 @@ func narrow(ctx context.Context, run forgekey.Runner, env []string, dir, commit, for i, p := range paths { full[i] = path.Join(mdir, p) } - r := run(ctx, env, "git", append([]string{"-C", dir, "ls-tree", "-z", commit, "--"}, full...)...) - if r.Status != 0 || r.Err != "" { - return fmt.Errorf("git ls-tree %s of the module %s: %s", commit, mdir, forgekey.Said(r.Err+"\n"+r.Stderr)) - } objects := map[string]string{} - for _, l := range strings.Split(r.Stdout, "\x00") { - meta, name, ok := strings.Cut(l, "\t") - fields := strings.Fields(meta) - if !ok || len(fields) != 3 { - continue + for _, round := range unnested(full) { + r := run(ctx, env, "git", append([]string{"-C", dir, "ls-tree", "-z", commit, "--"}, round...)...) + if r.Status != 0 || r.Err != "" { + return fmt.Errorf("git ls-tree %s of the module %s: %s", commit, mdir, forgekey.Said(r.Err+"\n"+r.Stderr)) + } + for _, l := range strings.Split(r.Stdout, "\x00") { + meta, name, ok := strings.Cut(l, "\t") + fields := strings.Fields(meta) + if !ok || len(fields) != 3 { + continue + } + objects[name] = fields[2] } - objects[name] = fields[2] } mt.Paths = map[string]string{} for i, p := range paths { @@ -106,4 +108,35 @@ func narrow(ctx context.Context, run forgekey.Runner, env []string, dir, commit, return nil } +// unnested splits paths into rounds of one git ls-tree each, none of whose paths lies inside another of its round: given +// "go" and "go/seats/x" together, git ls-tree descends into go and lists its children instead of go itself, so the +// object of go is missing from its answer (issue 1966: mesh-contracts' manifest names both). Usually one round; the +// order of paths is kept within a round. +func unnested(paths []string) [][]string { + var rounds [][]string + for _, p := range paths { + placed := false + for i, round := range rounds { + if !nestsWithAny(p, round) { + rounds[i] = append(round, p) + placed = true + break + } + } + if !placed { + rounds = append(rounds, []string{p}) + } + } + return rounds +} + +func nestsWithAny(p string, round []string) bool { + for _, q := range round { + if p == q || strings.HasPrefix(p, q+"/") || strings.HasPrefix(q, p+"/") || p == "." || q == "." { + return true + } + } + return false +} + func sha256Hex(b []byte) string { h := sha256.Sum256(b); return hex.EncodeToString(h[:]) } diff --git a/modules/mesh-build-go/buildstep/trees_integration_test.go b/modules/mesh-build-go/buildstep/trees_integration_test.go index 65df0519..6bf462db 100644 --- a/modules/mesh-build-go/buildstep/trees_integration_test.go +++ b/modules/mesh-build-go/buildstep/trees_integration_test.go @@ -62,4 +62,16 @@ func TestAGoOnlyCommitLeavesTheNamedObjectsEqualOnRealGit(t *testing.T) { if src["."].Paths["src"] != realGit(t, repo, "rev-parse", "HEAD:src") { t.Fatalf("the object of src is %s, git says %s", src["."].Paths["src"], realGit(t, repo, "rev-parse", "HEAD:src")) } + + // A manifest naming a path and a path inside it (mesh-contracts' "go" and "go/seats/meshvault"; issue 1966): each + // gets its own object, not git ls-tree's listing of the outer one's children. + writeFile(t, repo, "nested/module.json", `{"module":"nested","version":"1","build":{"artifacts":[`+ + `{"name":"go","kind":"package","language":"go","paths":["go"]},`+ + `{"name":"go-seat","kind":"package","language":"go","paths":["go/seats/a"]}]}}`) + writeFile(t, repo, "nested/go/go.mod", "module x\n") + writeFile(t, repo, "nested/go/seats/a/a.go", "package a\n") + nested := trees(commit("nested"))["nested"] + if nested.Paths["go"] != realGit(t, repo, "rev-parse", "HEAD:nested/go") || nested.Paths["go/seats/a"] != realGit(t, repo, "rev-parse", "HEAD:nested/go/seats/a") { + t.Fatalf("a path and a path inside it: %v", nested.Paths) + } } -- 2.54.0 From da2aa918322145198eb6def10ae36133adae7f71 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 4 Nov 2026 19:45:55 +0100 Subject: [PATCH 2/2] Keep Go's indented continuation in a step's cause: the composer's reason ended at "requires" and hid the module that could not be found (issue 1966) Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig --- .../cmd/mesh-build-go/candidate.go | 5 +++-- .../mesh-build-go/cmd/mesh-build-go/fetch.go | 22 +++++++++++++++++-- .../cmd/mesh-build-go/fetch_test.go | 5 +++++ 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/modules/mesh-build-go/cmd/mesh-build-go/candidate.go b/modules/mesh-build-go/cmd/mesh-build-go/candidate.go index 4379d3ca..27f43d77 100644 --- a/modules/mesh-build-go/cmd/mesh-build-go/candidate.go +++ b/modules/mesh-build-go/cmd/mesh-build-go/candidate.go @@ -97,9 +97,10 @@ func (m *Module) candidate(ctx context.Context, workspace string) (string, error // compileError is the first line of the compiler's output that says an error: not the `# package` heading go build // prints above a package's errors, nor Go's progress. func compileError(out string) string { - for _, l := range strings.Split(out, "\n") { + lines := strings.Split(out, "\n") + for i, l := range lines { if l = strings.TrimSpace(l); l != "" && !strings.HasPrefix(l, "#") && !strings.HasPrefix(l, "go: downloading ") { - return l + return withContinuation(l, lines[i+1:]) } } return firstLine(out) diff --git a/modules/mesh-build-go/cmd/mesh-build-go/fetch.go b/modules/mesh-build-go/cmd/mesh-build-go/fetch.go index 8644edd9..8f66a1a9 100644 --- a/modules/mesh-build-go/cmd/mesh-build-go/fetch.go +++ b/modules/mesh-build-go/cmd/mesh-build-go/fetch.go @@ -351,15 +351,33 @@ func (m *Module) now() time.Time { return m.clock() } // causeLine is the first line of a go command's output that says something other than Go's own progress: `go: // downloading ...` comes first when Go fetches a module, and `# ` heads a compile error of go build or go vet // (issue 1703: the reason said only the package), and the cause of a failure on a later line. +// +// A cause Go writes over several lines keeps its indented continuation lines, joined by a space: a module graph error +// is "X requires" with the module that fails, and why, on the next line (issue 1966: the composer's reason ended at +// "requires" and hid "golang.org/x/tools@v0.42.0: module lookup disabled"). func causeLine(s string) string { - for _, l := range strings.Split(s, "\n") { + lines := strings.Split(s, "\n") + for i, l := range lines { if l = strings.TrimSpace(l); l != "" && !strings.HasPrefix(l, "go: downloading ") && !strings.HasPrefix(l, "# ") { - return l + return withContinuation(l, lines[i+1:]) } } return firstLine(s) } +// withContinuation is first with each following line Go indented under it, trimmed and joined by a space. +func withContinuation(first string, rest []string) string { + for _, l := range rest { + if !strings.HasPrefix(l, "\t") && !strings.HasPrefix(l, " ") { + break + } + if l = strings.TrimSpace(l); l != "" { + first += " " + l + } + } + return first +} + func firstLine(s string) string { for _, l := range strings.Split(s, "\n") { if l = strings.TrimSpace(l); l != "" { diff --git a/modules/mesh-build-go/cmd/mesh-build-go/fetch_test.go b/modules/mesh-build-go/cmd/mesh-build-go/fetch_test.go index e86e1559..81d7c5d5 100644 --- a/modules/mesh-build-go/cmd/mesh-build-go/fetch_test.go +++ b/modules/mesh-build-go/cmd/mesh-build-go/fetch_test.go @@ -309,9 +309,14 @@ func TestCauseLineSkipsProgressAndThePackageHeading(t *testing.T) { "go: downloading x v1\n# git.example/m/cmd\n./main.go:3:2: undefined: y\n": "./main.go:3:2: undefined: y", "\ngo: example.com/x@v1: reading: 404\n": "go: example.com/x@v1: reading: 404", "go: downloading x v1\n": "go: downloading x v1", + // issue 1966: a module graph error goes on, indented, on the next line; a later error is not taken in. + "a.go:3:8: x@v1 requires\n\ty@v2: module lookup disabled by GOPROXY=off\nb.go:1:1: other\n": "a.go:3:8: x@v1 requires y@v2: module lookup disabled by GOPROXY=off", } { if got := causeLine(in); got != want { t.Errorf("causeLine(%q) = %q, want %q", in, got, want) } + if got := compileError(in); got != want { + t.Errorf("compileError(%q) = %q, want %q", in, got, want) + } } } -- 2.54.0