diff --git a/modules/mesh-build-go/cmd/mesh-build-go/pins.go b/modules/mesh-build-go/cmd/mesh-build-go/pins.go index 80a3dfb8..4247d447 100644 --- a/modules/mesh-build-go/cmd/mesh-build-go/pins.go +++ b/modules/mesh-build-go/cmd/mesh-build-go/pins.go @@ -107,7 +107,7 @@ func (m *Module) resolvePins(ctx context.Context, root, dir, failure string) (bo if !strings.Contains(failure, p.short) || strings.Contains(done, "replace "+p.module+" =>") { continue } - where, err := m.placePin(ctx, root, p) + where, err := m.placePin(ctx, root, p, failure) if err != nil { return false, err } @@ -121,7 +121,7 @@ func (m *Module) resolvePins(ctx context.Context, root, dir, failure string) (bo // placePin finds the commit of a pin among the pull request heads of the forge, places the repository at it beside // the workspace (the way a fetch places a sibling) and answers the directory of the module in it. -func (m *Module) placePin(ctx context.Context, root string, p pin) (string, error) { +func (m *Module) placePin(ctx context.Context, root string, p pin, failure string) (string, error) { repository := "novox/" + p.repo r := m.Run(ctx, []string{"GIT_TERMINAL_PROMPT=0"}, "git", "-c", "credential.helper=", "ls-remote", m.remote(repository), "refs/pull/*/head") if r.Status != 0 || r.Err != "" { @@ -135,12 +135,18 @@ func (m *Module) placePin(ctx context.Context, root string, p pin) (string, erro } } if commit == "" { - return "", fmt.Errorf("the pin of %s at commit %s is on no branch, tag or pull request head of %s on the forge", p.module, p.short, repository) + return "", fmt.Errorf("the pin of %s at commit %s is on no pull request head of %s on the forge; go said: %s", p.module, p.short, repository, causeLine(failure)) } - if _, err := m.placeSiblings(ctx, filepath.Join(root, "pinned"), []siblingArg{{repository: repository, commit: commit, ref: ref}}); err != nil { + // One placement per repository and commit, shared by every go.mod directory of the tree that pins it. + at := filepath.Join(root, "pinned", p.repo+"@"+commit) + where := filepath.Join(at, "beside", p.repo, filepath.FromSlash(p.sub)) + if _, err := os.Stat(filepath.Join(at, "beside", p.repo)); err == nil { + return where, nil + } + if _, err := m.placeSiblings(ctx, at, []siblingArg{{repository: repository, commit: commit, ref: ref}}); err != nil { return "", fmt.Errorf("the pin of %s at commit %s: %w", p.module, p.short, err) } - return filepath.Join(root, "pinned", "beside", p.repo, filepath.FromSlash(p.sub)), nil + return where, nil } // replaceInModfile adds `replace module => where` to the temporary modfile of dir, made from the directory's go.mod and diff --git a/modules/mesh-build-go/cmd/mesh-build-go/pins_integration_test.go b/modules/mesh-build-go/cmd/mesh-build-go/pins_integration_test.go index 96941d3d..f61519e5 100644 --- a/modules/mesh-build-go/cmd/mesh-build-go/pins_integration_test.go +++ b/modules/mesh-build-go/cmd/mesh-build-go/pins_integration_test.go @@ -9,6 +9,7 @@ import ( "context" "fmt" "os" + "os/exec" "path/filepath" "testing" @@ -19,7 +20,12 @@ import ( // commit) and a Go member pinning that head by a pseudo-version, when build_go_fetch then build_go_build run on the // member, then both pass, the build with GOPROXY=off. func TestRealFetchAndBuildOfAMemberPinningAPullHead(t *testing.T) { - needTools(t, "the pin", "go", "git") + // Not skipped when a tool is missing: a pass of the check must mean this test ran (the check prints no test log). + for _, tool := range []string{"go", "git"} { + if _, err := exec.LookPath(tool); err != nil { + t.Fatalf("no %s where this check runs: the pin on a pull request's head cannot be proven", tool) + } + } f := newForge(t) work, fork := f.repo("novox/dep", map[string]string{"README": "dep\n"}) head := f.commit("novox/dep", work, fork, map[string]string{ diff --git a/modules/mesh-build-go/cmd/mesh-build-go/pins_test.go b/modules/mesh-build-go/cmd/mesh-build-go/pins_test.go index e48d3e83..bbdc0ff7 100644 --- a/modules/mesh-build-go/cmd/mesh-build-go/pins_test.go +++ b/modules/mesh-build-go/cmd/mesh-build-go/pins_test.go @@ -42,8 +42,12 @@ func newPinHarness(t *testing.T, gomod string, vendor bool, pulls string) (*pinH for i, a := range args { if a == "-C" { os.MkdirAll(args[i+1], 0o755) - os.WriteFile(filepath.Join(args[i+1], "go.mod"), []byte(gomod), 0o644) - os.WriteFile(filepath.Join(args[i+1], "go.sum"), []byte("sum\n"), 0o644) + for _, g := range strings.Fields(f.gomods) { + d := filepath.Join(args[i+1], filepath.Dir(g)) + os.MkdirAll(d, 0o755) + os.WriteFile(filepath.Join(d, "go.mod"), []byte(gomod), 0o644) + os.WriteFile(filepath.Join(d, "go.sum"), []byte("sum\n"), 0o644) + } if vendor { os.MkdirAll(filepath.Join(args[i+1], "vendor"), 0o755) os.WriteFile(filepath.Join(args[i+1], "vendor", "modules.txt"), nil, 0o644) @@ -95,7 +99,7 @@ func TestAPinOnAPullHeadIsPlacedBesideAndHandedToGoByAModfile(t *testing.T) { if st.State != delivery.StepPassed { t.Fatalf("the step ended %s: %s", st.State, st.Reason) } - placed := filepath.Join(h.m.Work, res.Workspace, "pinned", "beside", "mesh-sdk") + placed := filepath.Join(h.m.Work, res.Workspace, "pinned", "mesh-sdk@"+commitSHA, "beside", "mesh-sdk") if !h.ran("-C " + placed + " checkout -q --detach " + commitSHA) { t.Errorf("mesh-sdk was not placed at %s at the commit:\n%s", placed, strings.Join(h.f.lines(), "\n")) } @@ -159,7 +163,8 @@ func TestAPinOnNoRefFailsNamingTheModuleAndTheCommit(t *testing.T) { if st.State != delivery.StepFailed { t.Fatalf("the step ended %s, want failed", st.State) } - if !strings.Contains(st.Reason, "git.novox.be/novox/mesh-sdk/go") || !strings.Contains(st.Reason, pinShort) { + if !strings.Contains(st.Reason, "no pull request head") || !strings.Contains(st.Reason, "unknown revision") || + !strings.Contains(st.Reason, "git.novox.be/novox/mesh-sdk/go") || !strings.Contains(st.Reason, pinShort) { t.Errorf("the reason does not name the module and the commit: %q", st.Reason) } if !h.ran("ls-remote") { @@ -178,3 +183,64 @@ func TestAVendoringDirectoryResolvesNothing(t *testing.T) { t.Errorf("a vendoring directory was resolved:\n%s", strings.Join(h.f.lines(), "\n")) } } + +// 1148/AC1 (a repository with several go.mod directories, as mesh-catalog has): two directories pinning the same pull +// head share one placement and both are handed a modfile; the fetch does not fail placing it twice. +func TestTwoDirectoriesPinningOneHeadShareOnePlacement(t *testing.T) { + h, states := newPinHarness(t, pinnedGoMod, false, pullList) + h.f.gomods = "go.mod\nsub/go.mod\n" + res, st := h.fetch(states) + if st.State != delivery.StepPassed { + t.Fatalf("the step ended %s: %s", st.State, st.Reason) + } + checkouts := 0 + for _, c := range h.cmds { + if strings.Contains(c.line, "pinned") && strings.Contains(c.line, " checkout ") { + checkouts++ + } + } + if checkouts != 1 { + t.Errorf("the repository was placed %d times, want once:\n%s", checkouts, strings.Join(h.f.lines(), "\n")) + } + for _, d := range []string{"tree", "tree/sub"} { + if _, ok := h.m.pinModfile(filepath.Join(h.m.Work, res.Workspace, d)); !ok { + t.Errorf("%s was not handed a modfile", d) + } + } +} + +// 1148/AC1: two directories pinning two different commits of one repository get two placements, each its own. +func TestTwoCommitsOfOneRepositoryGetTwoPlacements(t *testing.T) { + other := "fedcba9876543210fedcba9876543210fedcba98" + h, states := newPinHarness(t, pinnedGoMod, false, commitSHA+"\trefs/pull/7/head\n"+other+"\trefs/pull/8/head\n") + h.f.gomods = "go.mod\nsub/go.mod\n" + base := h.m.Run + h.m.Run = func(ctx context.Context, env []string, name string, args ...string) forgekey.Ran { + if name == "git" && strings.Contains(strings.Join(args, " "), "pinned/mesh-sdk@"+other+"/") && strings.Contains(strings.Join(args, " "), " rev-parse HEAD") { + return forgekey.Ran{Stdout: other + "\n"} + } + // the second directory pins the other commit + if name == "git" && strings.Contains(strings.Join(args, " "), " checkout ") && !strings.Contains(strings.Join(args, " "), "pinned") { + r := base(ctx, env, name, args...) + for i, a := range args { + if a == "-C" { + os.WriteFile(filepath.Join(args[i+1], "sub", "go.mod"), []byte(strings.ReplaceAll(pinnedGoMod, pinShort, other[:12])), 0o644) + } + } + return r + } + if name == "go" && strings.Contains(strings.Join(args, " "), "/sub mod download") && !strings.Contains(strings.Join(env, " "), "-modfile=") { + return forgekey.Ran{Status: 1, Stderr: "go: git.novox.be/novox/mesh-sdk/go@v0.1.15-0.20261022120000-" + other[:12] + ": invalid version: unknown revision " + other[:12] + "\n"} + } + return base(ctx, env, name, args...) + } + _, st := h.fetch(states) + if st.State != delivery.StepPassed { + t.Fatalf("the step ended %s: %s", st.State, st.Reason) + } + for _, c := range []string{commitSHA, other} { + if !h.ran("pinned/mesh-sdk@" + c) { + t.Errorf("no placement of mesh-sdk at %s", c) + } + } +}