Clear stale bootstrap temp refs instead of failing on resume mismatch · Entire

Clear stale bootstrap temp refs instead of failing on resume mismatch

1af29eamain·

Soph·3mo ago·2 files·+36 added/-40 removed

When a temp ref from a previous run doesn't match any planned checkpoint (e.g., the user changed --target-max-pack-bytes between runs, or a previous subdivide created midpoints that no longer appear in the new plan), bootstrap now deletes the stale temp ref and starts the branch fresh rather than failing with "does not match any planned checkpoint".

Temp refs are an internal implementation detail of batched bootstrap, not user-visible state. Treating a mismatch as fatal forced users to manually delete refs on the target before retrying — unnecessary friction for a routine parameter change.

Co-Authored-By: Claude Opus 4.6 (1M context) noreply@anthropic.com

Sessions

432a6b170dacView transcript

[?
what test coverage do we have now for replicate?Claude Code·Opus 4.6[1m]·1 step](/content/gh/entireio/git-sync/session/7b2777b1-8075-41f4-a62a-cbfc1b76c01b#timeline-432a6b170dac/index.html)

Changes

2

218 unmodified lines

219
220
221
222
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236

218 unmodified lines

current := batch.ResumeHash
    startIdx, err := planner.BootstrapResumeIndex(batch.Checkpoints, batch.ResumeHash)
    if err != nil {
        return result, fmt.Errorf("resume bootstrap batch for %s: %w", batch.Plan.TargetRef, err)
        // Stale temp ref from a previous run with different parameters.
        // Delete it and start the branch fresh.
        p.log("bootstrap batch clearing stale temp ref",
            "branch", batch.Plan.TargetRef.String(),
            "temp_ref", batch.TempRef.String(),
            "stale_hash", planner.ShortHash(batch.ResumeHash))
        delCmds := []gitproto.PushCommand{{Name: batch.TempRef, Old: batch.ResumeHash, Delete: true}}
        if delErr := p.TargetPusher.PushCommands(ctx, delCmds); delErr != nil {
            return result, fmt.Errorf("delete stale temp ref %s: %w (original: %w)", batch.TempRef, delErr, err)
        }
        current = plumbing.ZeroHash
        startIdx = 0
    }

// Manual index loop: subdivide may insert checkpoints at the current

Minternal/strategy/bootstrap/bootstrap.go+12/-1

30 unmodified lines

31
32
33
34
34
35
36
619 unmodified lines

656
657
658
660
659
660
661
662
663
664
662
665
666
667
668
9 unmodified lines

678
679
680
678
681
680
681
682
683
684
685
686
687
688
689
690
691
689
690
691
692
692
693
694
695
696
694
695
696
697
698
698
699
700
700
701
702
701
702
703
704
705
706
707
708
709
710
711
704
705
706
713
714
715
716
717
718
719
720
707
722
723
724
725
726
727
708
709
710
711
712
713
714
715

30 unmodified lines

"github.com/soph/git-sync/internal/auth"
    "github.com/soph/git-sync/internal/gitproto"
    "github.com/soph/git-sync/internal/planner"
    bstrap "github.com/soph/git-sync/internal/strategy/bootstrap"
    "github.com/soph/git-sync/internal/syncertest"
)

619 unmodified lines

}
}

func TestBootstrap_IntegrationBatchedResumeMismatchFails(t *testing.T) {
func TestBootstrap_IntegrationBatchedResumeMismatchClearsAndRetries(t *testing.T) {
    // When a temp ref from a previous run doesn't match any planned checkpoint
    // (e.g., the user changed --target-max-pack-bytes between runs), bootstrap
    // should delete the stale temp ref and start the branch fresh rather than
    // failing with a resume mismatch error.
    sourceRepo, sourceFS := newSourceRepo(t)
    makeLargeCommits(t, sourceRepo, sourceFS, 5, 200_000)
    makeLargeCommits(t, sourceRepo, sourceFS, 100, 5_000)

unrelatedRepo, unrelatedFS := newSourceRepo(t)
    makeCommits(t, unrelatedRepo, unrelatedFS, 1)
9 unmodified lines

if err := copyRefsAndObjects(unrelatedRepo.Storer, targetRepo.Storer, nil); err != nil {
        t.Fatalf("copy unrelated objects: %v", err)
    }
    targetHead := unrelatedHead
    tempRef := planner.BootstrapTempRef(plumbing.NewBranchReferenceName(testBranch))
    if err := targetRepo.Storer.SetReference(plumbing.NewHashReference(tempRef, targetHead.Hash())); err != nil {
        t.Fatalf("set temp ref: %v", err)
    }
    if err := targetRepo.Storer.SetReference(plumbing.NewHashReference(tempRef, unrelatedHead.Hash())); err != nil {
        t.Fatalf("set stale temp ref: %v", err)
    }

sourceServer := newSmartHTTPRepoServerV2(t, sourceRepo)
    targetServer := newSmartHTTPRepoServer(t, targetRepo)
    targetServer.receivePackThinCap = true
    defer sourceServer.Close()
    defer targetServer.Close()

cfg := Config{
        Source:            Endpoint{URL: sourceServer.RepoURL()},
        Target:            Endpoint{URL: targetServer.RepoURL()},
        ProtocolMode:      protocolModeAuto,
    }
    result, err := Bootstrap(context.Background(), Config{
        Source:             Endpoint{URL: sourceServer.RepoURL()},
        Target:             Endpoint{URL: targetServer.RepoURL()},
        ProtocolMode:       protocolModeAuto,
        TargetMaxPackBytes: 350_000,
    })

s, err := newSession(context.Background(), cfg, false)
    })
    if err != nil {
        t.Fatalf("new session: %v", err)
        t.Fatalf("expected bootstrap to clear stale temp ref and succeed, got: %v", err)
    }
    desired, _, err := planner.BuildDesiredRefs(s.sourceRefMap, planConfig(cfg))
    if err != nil {
        t.Fatalf("build desired refs: %v", err)
    }
    if !result.Batching {
        t.Fatalf("expected batched bootstrap, got %+v", result)
    }
    ref := desired[plumbing.NewBranchReferenceName(testBranch)]
    checkpoints, err := bstrap.PlanCheckpoints(context.Background(), bstrap.Params{
        SourceConn:    s.sourceConn,
        SourceService: s.sourceService,
        TargetMaxPack:  cfg.TargetMaxPackBytes,
    }, ref)
    if err != nil {
        t.Fatalf("plan checkpoints: %v", err)
    }
    if result.Pushed == 0 {
        t.Fatalf("expected pushed > 0, got %+v", result)
    }
    if len(checkpoints) == 0 {
        t.Fatal("expected checkpoints for batched bootstrap")
    }
    for _, checkpoint := range checkpoints {
        if checkpoint == targetHead.Hash() {
        t.Fatalf("expected unrelated temp ref hash, got checkpoint collision at %s", checkpoint)
        }
    }

_, err = Bootstrap(context.Background(), cfg)
    if err == nil {
        t.Fatal("expected batched bootstrap resume mismatch to fail")
    }
    if !strings.Contains(err.Error(), "does not match any planned checkpoint") {
        t.Fatalf("unexpected error: %v", err)
    }

assertHeadsMatch(t, sourceRepo, targetRepo, testBranch)

// Stale temp ref should have been cleaned up.
    if _, err := targetRepo.Reference(tempRef, true); err != plumbing.ErrReferenceNotFound {
        t.Fatalf("expected stale temp ref to be deleted, got err=%v", err)
    }
}