From 22f7a785035e304503185e4d100bec80832ae19a Mon Sep 17 00:00:00 2001 From: jochen Date: Fri, 23 Oct 2026 14:09:34 +0200 Subject: [PATCH] Dissociate a workspace from the mirror after the fetch, so a shut-off step finds every object in the tree (issue 890 C2 follow-up) --- .../mesh-build-go/cmd/mesh-build-go/fetch.go | 3 + .../mesh-build-go/cmd/mesh-build-go/mirror.go | 14 ++++ .../cmd/mesh-build-go/mirror_test.go | 68 +++++++++++++++++-- .../cmd/mesh-build-go/preview.go | 3 + 4 files changed, 82 insertions(+), 6 deletions(-) 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 37cb8b85..e5a1b49b 100644 --- a/modules/mesh-build-go/cmd/mesh-build-go/fetch.go +++ b/modules/mesh-build-go/cmd/mesh-build-go/fetch.go @@ -126,6 +126,9 @@ func (m *Module) fetchInto(ctx context.Context, req buildstep.FetchRequest, id s return nil, err } } + if err := m.dissociate(ctx, tree); err != nil { + return nil, err + } if res.Beside, err = m.placeSiblings(ctx, root, siblings); err != nil { return nil, err } diff --git a/modules/mesh-build-go/cmd/mesh-build-go/mirror.go b/modules/mesh-build-go/cmd/mesh-build-go/mirror.go index 555e58c6..0ad17fda 100644 --- a/modules/mesh-build-go/cmd/mesh-build-go/mirror.go +++ b/modules/mesh-build-go/cmd/mesh-build-go/mirror.go @@ -57,3 +57,17 @@ func (m *Module) updateMirror(ctx context.Context, mirror, remote, commit, ref s defer syscall.Flock(int(lock.Fd()), syscall.LOCK_UN) return buildstep.UpdateMirror(ctx, m.Run, mirror, remote, commit, ref, m.FetchOptions...) } + +// dissociate makes the repository in dir self-contained again, as git clone --dissociate does: the objects it borrowed +// from the mirror are packed into it and its alternates file goes, so a step shut off with the workspace alone (the +// mirror is not bound into it) finds every object. The network saving stays: the borrowing was for the transfer. +func (m *Module) dissociate(ctx context.Context, dir string) error { + alternates := filepath.Join(dir, ".git", "objects", "info", "alternates") + if _, err := os.Stat(alternates); err != nil { + return nil // no mirror was used + } + if r := m.Run(ctx, []string{"GIT_TERMINAL_PROMPT=0"}, "git", "-C", dir, "repack", "-a", "-d", "-q"); r.Status != 0 || r.Err != "" { + return fmt.Errorf("git repack in %s: %s", dir, firstLine(r.Err+" "+r.Stderr)) + } + return os.Remove(alternates) +} 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 dabf67eb..6721155e 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 @@ -2,7 +2,9 @@ package main import ( "encoding/json" + "git.novox.be/novox/mesh-sdk/go/buildstep" "os" + "os/exec" "path/filepath" "strings" "sync" @@ -50,10 +52,15 @@ func TestTwoFetchesOfARepositoryUseOneMirrorAndReferenceIt(t *testing.T) { if updates != 2 { t.Errorf("the mirror %s was updated %d times, want 2:\n%s", mirror, updates, strings.Join(f.lines(), "\n")) } + // The fake git writes nothing, so the alternates file the SDK wrote stands until the fetch dissociates: it is gone + // at the end and git repack -a -d ran in each tree. for _, ws := range workspaces { - raw, err := os.ReadFile(filepath.Join(m.Work, ws, "tree", ".git", "objects", "info", "alternates")) - if err != nil || strings.TrimSpace(string(raw)) != filepath.Join(mirror, "objects") { - t.Errorf("the alternates of %s are %q (%v)", ws, raw, err) + tree := filepath.Join(m.Work, ws, "tree") + if _, err := os.Stat(filepath.Join(tree, ".git", "objects", "info", "alternates")); err == nil { + t.Errorf("the workspace %s still borrows from the mirror", ws) + } + if !strings.Contains(strings.Join(f.lines(), "\n"), "-C "+tree+" repack -a -d") { + t.Errorf("git repack -a -d did not run in %s", tree) } } } @@ -71,9 +78,8 @@ func TestASiblingIsPlacedThroughItsOwnMirror(t *testing.T) { t.Errorf("the mirror of %s was not updated", repo) } } - raw, _ := os.ReadFile(filepath.Join(m.Work, ws, "beside", "mesh-sdk", ".git", "objects", "info", "alternates")) - if strings.TrimSpace(string(raw)) != filepath.Join(m.Cache, "mirror", "novox", "mesh-sdk.git", "objects") { - t.Errorf("the sibling's alternates are %q", raw) + if _, err := os.Stat(filepath.Join(m.Work, ws, "beside", "mesh-sdk", ".git", "objects", "info", "alternates")); err == nil { + t.Errorf("the sibling still borrows from the mirror") } } @@ -132,3 +138,53 @@ func TestAMirrorThatCannotBeUpdatedLetsTheFetchGoOnWithoutIt(t *testing.T) { } func sleepABit() { time.Sleep(30 * time.Millisecond) } + +// The workspace is self-contained after the fetch: no alternates file, and git finds the commit with the mirror gone, +// as a step shut off with the workspace alone needs. Real git, a local repository as the forge. +func TestTheWorkspaceIsSelfContainedAfterAFetchThroughTheMirror(t *testing.T) { + if _, err := exec.LookPath("git"); err != nil { + t.Skip("no git") + } + forge := t.TempDir() + src := filepath.Join(forge, "novox", "mesh-sdk.git") + if err := os.MkdirAll(src, 0o755); err != nil { + t.Fatal(err) + } + git := func(dir string, args ...string) string { + cmd := exec.Command("git", append([]string{"-C", dir, "-c", "user.name=t", "-c", "user.email=t@example.org", "-c", "commit.gpgsign=false"}, args...)...) + cmd.Env = append(os.Environ(), "GIT_CONFIG_GLOBAL=/dev/null") + out, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("git %v: %v %s", args, err, out) + } + return strings.TrimSpace(string(out)) + } + git(src, "init", "-q") + if err := os.WriteFile(filepath.Join(src, "f.txt"), []byte("one\n"), 0o644); err != nil { + t.Fatal(err) + } + git(src, "add", ".") + git(src, "commit", "-q", "-m", "one") + commit := git(src, "rev-parse", "HEAD") + + states := newStates() + m := moduleWith(states, "go1.27.1") + m.Home, m.Work, m.Cache = t.TempDir(), t.TempDir(), t.TempDir() + m.Run = StepRunner(m.Home) + m.RemoteBase = "file://" + forge + "/" + m.FetchOptions = []buildstep.Option{buildstep.AllowFileRemote()} + ws := fetchOnce(t, m, states, map[string]any{"repository": "novox/mesh-sdk", "commit": commit}).Workspace + tree := filepath.Join(m.Work, ws, "tree") + if _, err := os.Stat(filepath.Join(tree, ".git", "objects", "info", "alternates")); err == nil { + t.Errorf("the workspace still has an alternates file") + } + if err := os.RemoveAll(filepath.Join(m.Cache, "mirror")); err != nil { + t.Fatal(err) + } + if out, err := exec.Command("git", "-C", tree, "cat-file", "-e", commit+"^{commit}").CombinedOutput(); err != nil { + t.Errorf("git does not find the commit with the mirror gone: %v %s", err, out) + } + if out, err := exec.Command("git", "-C", tree, "fsck", "--connectivity-only").CombinedOutput(); err != nil { + t.Errorf("the workspace is not whole: %v %s", err, out) + } +} 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 192b8cbc..067dcfff 100644 --- a/modules/mesh-build-go/cmd/mesh-build-go/preview.go +++ b/modules/mesh-build-go/cmd/mesh-build-go/preview.go @@ -99,6 +99,9 @@ func (m *Module) placeSiblings(ctx context.Context, root string, siblings []sibl 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) }