Clear stale bootstrap temp refs instead of failing on resume mismatch · Entire
Clear stale bootstrap temp refs instead of failing on resume mismatch
1af29ea→main·
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
internal
strategy/bootstrap
Mbootstrap.go+12/-1
syncer
Mintegration_test.go+24/-39
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)
}
}