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)

This commit is contained in:
2026-10-23 14:09:34 +02:00
parent 3ff6a8b502
commit 22f7a78503
4 changed files with 82 additions and 6 deletions
@@ -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
}
@@ -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)
}
@@ -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)
}
}
@@ -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)
}