Guard checkpoint migration re-runs against ref regression · Entire
Guard checkpoint migration re-runs against ref regression
5edbdf0→main·
pfleidi·1w ago·4 files·+108 added/-4 removed
A re-run compared only the ref tip tree, so a ref that advanced past the migration snapshot through refs-store writes (summary/transcript backfills) would be "re-migrated": a new commit wrapping the stale branch tree, fast-forwarding the tip backwards content-wise and propagating on the next push.
Idempotency now walks the ref's first-parent chain — a snapshot found anywhere in history is skipped. The doctor command additionally refuses to run when git-refs is already the primary store, where the v1 branch may lag the refs and re-importing could only regress them.
Sessions
01KWX0X0VSAQD8ZCC4Q8QRDAP6View transcript
[?
Fix Merge Conflicts and Checkpoint MigrationClaude Code·Fable 5·9 steps](/content/gh/entireio/cli/session/bd0430d2-6e5a-41d2-a42e-bce6f4b55711#timeline-01KWX0X0VSAQD8ZCC4Q8QRDAP6/index.html)
Changes
4
cmd/entire/cli
checkpoint
- Mmigrate.go+28/-4
Mmigrate_test.go+34
Mdoctor_migrate.go+9
Adoctor_migrate_test.go+37
35 unmodified lines
36
37
38
39
39
40
41
42
43
31 unmodified lines
75
76
77
77
78
79
79
80
81
81
82
83
84
85
86
87
88
89
20 unmodified lines
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
35 unmodified lines
// normalized for the refs layout (see normalizeMigratedMetadata). Existing
// branch commits are not remapped.
//
// It is idempotent: a ref already carrying the normalized tree is skipped, and
// It is idempotent: a ref whose history already contains the normalized tree
// is skipped (even when refs-store writes have advanced the tip past it), and
// a re-run after more branch activity fast-forwards the ref (parenting on the
// existing commit).
//
31 unmodified lines
// unreadable ref state is treated as absent (orphan) rather than
// parenting the new commit on a bad hash, which would corrupt the commit
// graph for fetch+replay.
parent, existingTree, err := refsStore.refBase(cid)
parent, _, err := refsStore.refBase(cid)
if err != nil {
parent, existingTree = plumbing.ZeroHash, nil
parent = plumbing.ZeroHash
}
if existingTree != nil && existingTree.Hash == migratedTree {
// Skip when this snapshot was already imported anywhere on the ref's
// first-parent chain: the ref may have advanced past it through
// refs-store writes, and re-wrapping the old snapshot would regress
// the tip.
if treeInRefHistory(repo, parent, migratedTree) {
result.Skipped++
return nil
}
}
20 unmodified lines
return result, nil
}
// treeInRefHistory reports whether any commit on the first-parent chain
// starting at tip carries the given tree.
func treeInRefHistory(repo *git.Repository, tip, tree plumbing.Hash) bool {
for h := tip; h != plumbing.ZeroHash; {
commit, err := repo.CommitObject(h)
if err != nil {
return false
}
if commit.TreeHash == tree {
return true
}
if len(commit.ParentHashes) == 0 {
return false
}
h = commit.ParentHashes[0]
}
return false
}
// migratedCheckpointTree returns the branch subtree with its root metadata.json
// normalized for the refs layout — unchanged when already normalized or absent.
func migratedCheckpointTree(ctx context.Context, repo *git.Repository, cid id.CheckpointID, cpTreeHash plumbing.Hash) (plumbing.Hash, error) {
Mcmd/entire/cli/checkpoint/migrate.go+28/-4
241 unmodified lines
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
241 unmodified lines
assert.Empty(t, queued, "dry-run must not enqueue refs for push")
}
func TestMigrateBranchToRefs_SkipsRefAdvancedPastBranchSnapshot(t *testing.T) {
t.Parallel()
repo, _ := setupBranchTestRepo(t)
ctx := context.Background()
branch := NewGitStore(repo, DefaultV1Refs())
cid := id.MustCheckpointID("a1b2c3d4e5f6")
seedBranchCheckpoint(t, branch, cid, "s1")
_, err := MigrateBranchToRefs(ctx, repo, false)
require.NoError(t, err)
imported := refHash(t, repo, cid)
// The ref advances past the migration snapshot, as a refs-store write
// (e.g. a summary backfill) would: a new commit with a different tree,
// parented on the imported commit.
head, err := repo.Head()
require.NoError(t, err)
headCommit, err := repo.CommitObject(head.Hash())
require.NoError(t, err)
advanced, err := CreateCommit(ctx, repo, headCommit.TreeHash, imported, "refs-store write", "Test", "test@test.com")
require.NoError(t, err)
refName, err := RefName(cid)
require.NoError(t, err)
require.NoError(t, repo.Storer.SetReference(plumbing.NewHashReference(refName, advanced)))
// A re-run must recognize the already-imported snapshot in the ref's
// history and skip — not regress the tip to the old branch tree.
result, err := MigrateBranchToRefs(ctx, repo, false)
require.NoError(t, err)
assert.Empty(t, result.Migrated, "already-imported checkpoint must not be re-migrated")
assert.Equal(t, 1, result.Skipped)
assert.Equal(t, advanced, refHash(t, repo, cid), "ref tip must keep the newer refs-store write")
}
func TestMigrateBranchToRefs_UnreadableRefIsReplacedWithOrphan(t *testing.T) {
t.Parallel()
repo, _ := setupBranchTestRepo(t)
Mcmd/entire/cli/checkpoint/migrate_test.go+34
8 unmodified lines
9
10
11
12
13
14
15
23 unmodified lines
39
40
41
42
43
44
45
46
47
48
49
50
51
52
8 unmodified lines
"github.com/entireio/cli/cmd/entire/cli/checkpoint"
"github.com/entireio/cli/cmd/entire/cli/interactive"
"github.com/entireio/cli/cmd/entire/cli/settings"
"github.com/entireio/cli/cmd/entire/cli/strategy"
)
23 unmodified lines
ctx := cmd.Context()
out := cmd.OutOrStdout()
// Once git-refs is the primary store the refs are authoritative and
// the v1 branch may lag behind them; re-importing its snapshots
// could only regress refs, so refuse.
if cpCfg, _ := settings.LoadCheckpointsConfig(ctx); checkpoint.PrimaryIsRefs(cpCfg) { //nolint:errcheck // fail-soft: a bad checkpoints block already surfaces via Open; default to allowing migration
fmt.Fprintln(out, "The git-refs store is already the primary checkpoint store — nothing to migrate.")
return nil
}
repo, err := strategy.OpenRepository(ctx)
if err != nil {
return fmt.Errorf("open repository: %w", err)
}
Mcmd/entire/cli/doctor_migrate.go+9
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package cli
import (
"bytes"
"context"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/entireio/cli/cmd/entire/cli/paths"
"github.com/entireio/cli/cmd/entire/cli/testutil"
)
func TestDoctorMigrateCheckpoints_RefusesWhenRefsPrimary(t *testing.T) {
tmpDir := t.TempDir()
testutil.InitRepo(t, tmpDir)
testutil.WriteFile(t, tmpDir, "f.txt", "init")
testutil.GitAdd(t, tmpDir, "f.txt")
testutil.GitCommit(t, tmpDir, "init")
require.NoError(t, os.MkdirAll(filepath.Join(tmpDir, ".entire"), 0o755))
require.NoError(t, os.WriteFile(filepath.Join(tmpDir, ".entire", "settings.json",
[]byte(`{"enabled": true, "checkpoints": {"primary": {"type": "git-refs"}}}`), 0o644))
t.Chdir(tmpDir)
paths.ClearWorktreeRootCache()
cmd := newDoctorMigrateCheckpointsCmd()
var out bytes.Buffer
cmd.SetOut(&out)
cmd.SetErr(&out)
cmd.SetContext(context.Background())
require.NoError(t, cmd.Execute())
assert.Contains(t, out.String(), "already the primary",
"must refuse to migrate when git-refs is already the primary store")
}
}
Acmd/entire/cli/doctor_migrate_test.go+37