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 91fd3176..92b64dd5 100644 --- a/modules/mesh-build-go/cmd/mesh-build-go/fetch.go +++ b/modules/mesh-build-go/cmd/mesh-build-go/fetch.go @@ -184,6 +184,12 @@ func (m *Module) fetchPhases(ctx context.Context, req buildstep.FetchRequest, id } remote := m.remote(repository) opts := m.fetchOptions(ctx, repository, commit, ref) + mergeOpts := m.FetchOptions + if base != "" { + // The mirror is brought to the trunk commit too, so the merge preview deepens the tree from it and asks the forge + // for no history the mirror holds (issue 1921, try speed S6). + mergeOpts = m.fetchOptions(ctx, repository, base, "") + } clock.lap(&took.Mirror) if err := buildstep.FetchCommit(ctx, m.Run, tree, remote, commit, ref, opts...); err != nil { return FetchResult{}, err @@ -191,7 +197,7 @@ func (m *Module) fetchPhases(ctx context.Context, req buildstep.FetchRequest, id clock.lap(&took.Fetch) res := FetchResult{Workspace: id, Commit: commit, Took: took} if base != "" { - if err := m.merge(ctx, &res, tree, remote, base, commit); err != nil { + if err := m.merge(ctx, &res, tree, remote, base, commit, mergeOpts); err != nil { return FetchResult{}, err } } 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 a6734ac3..e3589532 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 @@ -349,7 +349,8 @@ func TestTheFetchSaysTheTimeOfEachPhaseAndTheyAddUpToTheTotal(t *testing.T) { t.Fatalf("the result says no phase times: %s", st.Result) } got := *res.Took - want := FetchTook{Mirror: 1, Fetch: 2, Merge: 3, Dissociate: 4, Modules: 5, Total: 15} + // the mirror is brought to the head and to the base (1 s each), and the preview deepens from it (1 s) before merging + want := FetchTook{Mirror: 2, Fetch: 2, Merge: 4, Dissociate: 4, Modules: 5, Total: 17} if got != want { t.Errorf("phases = %+v, want %+v", got, want) } diff --git a/modules/mesh-build-go/cmd/mesh-build-go/mirror_test.go b/modules/mesh-build-go/cmd/mesh-build-go/mirror_test.go index 6721155e..2a431c33 100644 --- a/modules/mesh-build-go/cmd/mesh-build-go/mirror_test.go +++ b/modules/mesh-build-go/cmd/mesh-build-go/mirror_test.go @@ -12,6 +12,7 @@ import ( "time" "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-sdk/go/forgekey" ) func fetchOnce(t *testing.T, m *Module, states *keptStates, args map[string]any) FetchResult { @@ -188,3 +189,61 @@ func TestTheWorkspaceIsSelfContainedAfterAFetchThroughTheMirror(t *testing.T) { t.Errorf("the workspace is not whole: %v %s", err, out) } } + +// 1921/S6: with a base, the mirror is brought to the trunk commit too, and the merge preview deepens the tree from the +// mirror: it asks the forge for no history the mirror holds. +func TestTheMirrorIsBroughtToTheBaseAndThePreviewFetchesFromIt(t *testing.T) { + f := &fakeStep{gomods: "go.mod\n", rule: previewRules(false)} + m, states := fetchModule(t, f) + mirror := filepath.Join(m.Cache, "mirror", "novox", "mesh-sdk.git") + f.files = map[string]string{"modules/m/module.json": "{}\n", "modules/m/go.sum": "sum\n", "go.mod": "module x\n", "go.sum": ""} + fetchOnce(t, m, states, map[string]any{"repository": "novox/mesh-sdk", "commit": commitSHA, "base": baseSHA}) + lines := strings.Join(f.lines(), "\n") + if !strings.Contains(lines, "-C "+mirror+" fetch --no-tags origin "+baseSHA) { + t.Errorf("the mirror was not brought to the base:\n%s", lines) + } + if !strings.Contains(lines, "fetch --no-tags --unshallow "+mirror+" "+baseSHA) || strings.Contains(lines, "--unshallow origin") { + t.Errorf("the preview fetched the trunk's history from the forge:\n%s", lines) + } +} + +// 1921/S6: the siblings are placed side by side: each sibling's fetch has started before any of them ends. +func TestTheSiblingsArePlacedAtOnce(t *testing.T) { + var mu sync.Mutex + started := map[string]bool{} + both := make(chan struct{}) + f := &fakeStep{gomods: "go.mod\n"} + f.rule = func(line string) (forgekey.Ran, bool) { + if !strings.Contains(line, "/beside/") || !strings.Contains(line, " fetch --no-tags --depth 1 ") { + return forgekey.Ran{}, false + } + mu.Lock() + for _, name := range []string{"mesh-catalog", "mesh-host"} { + if strings.Contains(line, "/beside/"+name) { + started[name] = true + } + } + if len(started) == 2 { + select { + case <-both: + default: + close(both) + } + } + mu.Unlock() + select { + case <-both: + case <-time.After(2 * time.Second): + return forgekey.Ran{Status: 1, Stderr: "the other sibling's fetch never started"}, true + } + return forgekey.Ran{}, false + } + m, states := fetchModule(t, f) + res := fetchOnce(t, m, states, map[string]any{"repository": "novox/mesh-controller", "commit": commitSHA, "beside": []any{ + map[string]any{"repository": "novox/mesh-host", "commit": commitSHA}, + map[string]any{"repository": "novox/mesh-catalog", "commit": commitSHA}, + }}) + if len(res.Beside) != 2 || res.Beside[0].Dir != "mesh-catalog" || res.Beside[1].Dir != "mesh-host" { + t.Errorf("beside = %+v, want both, in name order", res.Beside) + } +} diff --git a/modules/mesh-build-go/cmd/mesh-build-go/preview.go b/modules/mesh-build-go/cmd/mesh-build-go/preview.go index 067dcfff..f5fa5b9f 100644 --- a/modules/mesh-build-go/cmd/mesh-build-go/preview.go +++ b/modules/mesh-build-go/cmd/mesh-build-go/preview.go @@ -13,6 +13,7 @@ import ( "path/filepath" "sort" "strings" + "sync" "git.novox.be/novox/mesh-sdk/go/buildstep" ) @@ -34,8 +35,9 @@ func (m *Module) remote(repository string) string { } // merge makes the merge preview of the head onto base and writes the answer into res. -func (m *Module) merge(ctx context.Context, res *FetchResult, tree, remote, base, commit string) error { - mr, err := buildstep.MergeInto(ctx, m.Run, tree, remote, base, commit, m.FetchOptions...) +// opts are the fetch's options, with the mirror when it holds base. +func (m *Module) merge(ctx context.Context, res *FetchResult, tree, remote, base, commit string, opts []buildstep.Option) error { + mr, err := buildstep.MergeInto(ctx, m.Run, tree, remote, base, commit, opts...) if err != nil { return err } @@ -89,23 +91,22 @@ func (m *Module) besideDir(workspace string) string { // placeSiblings fetches each sibling into beside/ of the workspace root, as a plain directory. A symlink anywhere // in a placed tree fails the step naming it: the mesh creates none, and a test must not be led out of its sibling. func (m *Module) placeSiblings(ctx context.Context, root string, siblings []siblingArg) ([]Sibling, error) { - var placed []Sibling - for _, s := range siblings { - name := s.repository[strings.Index(s.repository, "/")+1:] - dir := filepath.Join(root, "beside", name) - if err := os.MkdirAll(dir, 0o700); err != nil { + // The siblings are placed side by side (issue 1921, try speed S6); the first failure in the order given is the step's. + placed := make([]Sibling, len(siblings)) + errs := make([]error, len(siblings)) + var wg sync.WaitGroup + for i, s := range siblings { + wg.Add(1) + go func() { + defer wg.Done() + placed[i], errs[i] = m.placeSibling(ctx, root, s) + }() + } + wg.Wait() + for _, err := range errs { + if err != nil { return nil, err } - if err := buildstep.FetchCommit(ctx, m.Run, dir, m.remote(s.repository), s.commit, s.ref, m.fetchOptions(ctx, s.repository, s.commit, s.ref)...); err != nil { - return nil, fmt.Errorf("beside %s: %w", s.repository, err) - } - if err := m.dissociate(ctx, dir); err != nil { - return nil, fmt.Errorf("beside %s: %w", s.repository, err) - } - if link := firstSymlink(dir); link != "" { - return nil, fmt.Errorf("beside %s holds a symlink, %s", s.repository, link) - } - placed = append(placed, Sibling{Repository: s.repository, Commit: s.commit, Dir: name}) } sort.Slice(placed, func(i, j int) bool { return placed[i].Dir < placed[j].Dir }) if len(placed) > 0 { @@ -121,6 +122,25 @@ func (m *Module) placeSiblings(ctx context.Context, root string, siblings []sibl return placed, nil } +// placeSibling fetches one sibling into beside/ of the workspace root. +func (m *Module) placeSibling(ctx context.Context, root string, s siblingArg) (Sibling, error) { + name := s.repository[strings.Index(s.repository, "/")+1:] + dir := filepath.Join(root, "beside", name) + if err := os.MkdirAll(dir, 0o700); err != nil { + return Sibling{}, err + } + if err := buildstep.FetchCommit(ctx, m.Run, dir, m.remote(s.repository), s.commit, s.ref, m.fetchOptions(ctx, s.repository, s.commit, s.ref)...); err != nil { + return Sibling{}, fmt.Errorf("beside %s: %w", s.repository, err) + } + if err := m.dissociate(ctx, dir); err != nil { + return Sibling{}, fmt.Errorf("beside %s: %w", s.repository, err) + } + if link := firstSymlink(dir); link != "" { + return Sibling{}, fmt.Errorf("beside %s holds a symlink, %s", s.repository, link) + } + return Sibling{Repository: s.repository, Commit: s.commit, Dir: name}, nil +} + // besideRecord is the file in a workspace's root naming the siblings the fetch placed beside it. const besideRecord = "beside.json" diff --git a/modules/mesh-build-go/cmd/mesh-build-go/preview_test.go b/modules/mesh-build-go/cmd/mesh-build-go/preview_test.go index 7771689a..e1612833 100644 --- a/modules/mesh-build-go/cmd/mesh-build-go/preview_test.go +++ b/modules/mesh-build-go/cmd/mesh-build-go/preview_test.go @@ -85,7 +85,8 @@ func TestFetchWithABaseMergesAndAnswersThePreview(t *testing.T) { t.Errorf("trees = %+v", res.Trees) } lines := strings.Join(f.lines(), "\n") - for _, want := range []string{"fetch --no-tags --unshallow origin " + baseSHA, "checkout -q --detach " + baseSHA, + mirror := filepath.Join(m.Cache, "mirror", "novox", "mesh-sdk.git") + for _, want := range []string{"fetch --no-tags --unshallow " + mirror + " " + baseSHA, "checkout -q --detach " + baseSHA, "core.hooksPath=/dev/null", "merge --no-ff --no-edit --no-verify"} { if !strings.Contains(lines, want) { t.Errorf("git was not asked %q:\n%s", want, lines) diff --git a/modules/mesh-build-go/go.mod b/modules/mesh-build-go/go.mod index ebde537b..339169cc 100644 --- a/modules/mesh-build-go/go.mod +++ b/modules/mesh-build-go/go.mod @@ -3,7 +3,7 @@ module meshbuildgo go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261024225846-cb548a6a588c + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261024230839-4a12ccb78d19 github.com/distribution/distribution/v3 v3.1.2 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 diff --git a/modules/mesh-build-go/go.sum b/modules/mesh-build-go/go.sum index 6df07410..3eb57f08 100644 --- a/modules/mesh-build-go/go.sum +++ b/modules/mesh-build-go/go.sum @@ -2,6 +2,8 @@ git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261024214154-bc16740feb0d h1:oJREw0gg git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261024214154-bc16740feb0d/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261024225846-cb548a6a588c h1:GWHP9ozfkFXHVEL2gE4Xa5kc5e8uPO+UL9+mau6iaIs= git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261024225846-cb548a6a588c/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261024230839-4a12ccb78d19 h1:NqU4hkk/jHdTEcGFnnXvdOJwD8dH5u+qWaCdQQ+JYMc= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261024230839-4a12ccb78d19/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/alicebob/miniredis/v2 v2.35.0 h1:QwLphYqCEAo1eu1TqPRN2jgVMPBweeQcR21jeqDCONI= github.com/alicebob/miniredis/v2 v2.35.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=