diff --git a/modules/mesh-build-go/buildstep/trees.go b/modules/mesh-build-go/buildstep/trees.go index 0371f1621..fb41c758b 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 65df0519f..6bf462db6 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) + } }