# Add `entire doctor migrate-checkpoints` (git-branch → git-refs)

`5ded4db`→[main](/content/gh/entireio/cli/commits/main/index.html)·
Soph·2w ago·5 files·+437 added/-24 removed

Convert checkpoints on the entire/checkpoints/v1 branch into per-checkpoint refs (refs/entire/checkpoints/<shard>/<id>), the git-refs store's layout.

checkpoint.MigrateBranchToRefs walks the v1 branch tip and, for each checkpoint, wraps its CURRENT subtree object in a fresh commit and points the ref at it — existing branch commits are not remapped. Since the git-refs ref tree is the branch's <shard>/<id> subtree byte-for-byte, a migrated checkpoint reads identically under either backend. It is idempotent: a checkpoint whose ref already carries the same tree is skipped, and a changed checkpoint re-migrates by fast-forward (new commit parents on the existing ref, so no history is lost). New/advanced refs are enqueued for push; the function itself never pushes.

The `doctor migrate-checkpoints` command reports migrated/skipped/total and, per the requested policy, only pushes when it can prompt: interactively it asks whether to push now; non-interactively it never pushes (refs stay queued and flush on the next push once git-refs is primary). `--dry-run` reports what would change without writing refs.

Push reuse: the git-refs pre-push queue-flush is extracted into strategy.flushCheckpointRefsQueue (shared by the fail-soft pre-push path and the new error-surfacing strategy.PushMigratedCheckpointRefs), so the "push now" option goes through the exact same fast-forward + fetch/replay logic.

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

## Sessions

ceed5d17e529View transcript

## Changes

5

- cmd/entire/cli

- checkpoint

- Amigrate.go+110

- Amigrate_test.go+149

- Mdoctor.go+1

- Adoctor_migrate.go+114

- strategy

- Mmanual_commit_push.go+63/-24

```go
package checkpoint

import (
	"context"
	"errors"
	"fmt"
	"log/slog"

git "github.com/go-git/go-git/v6"
	"github.com/go-git/go-git/v6/plumbing"

"github.com/entireio/cli/cmd/entire/cli/checkpoint/id"
	"github.com/entireio/cli/cmd/entire/cli/logging"
)

// MigrateResult summarizes a git-branch → git-refs checkpoint migration.
type MigrateResult struct {
	// Total is the number of checkpoints found on the v1 branch.
	Total int
	// Migrated lists the checkpoints whose ref was newly written or advanced.
	Migrated []id.CheckpointID
	// Skipped counts checkpoints already up to date (idempotent no-ops).
	Skipped int
}

// MigrateBranchToRefs converts every checkpoint stored on the git-branch v1
// branch (entire/checkpoints/v1) into a per-checkpoint ref under
// refs/entire/checkpoints/<shard>/<id> — the layout the git-refs store uses.
func MigrateBranchToRefs(ctx context.Context, repo *git.Repository, dryRun bool) (MigrateResult, error) {
	var result MigrateResult

branch := NewGitStore(repo, DefaultV1Refs())
tree, err := branch.getSessionsBranchTree()
	if err != nil {
		if errors.Is(err, plumbing.ErrReferenceNotFound) {
			// No v1 branch locally or on origin → nothing to migrate.
			return result, nil
		}
		return result, fmt.Errorf("read v1 checkpoint branch: %w", err)
	}

refsStore := newGitRefsStore(repo)
	authorName, authorEmail := GetGitAuthorFromRepo(repo)

walkErr := WalkCheckpointShards(ctx, repo, tree, func(cid id.CheckpointID, cpTreeHash plumbing.Hash) error {
		if err := ctx.Err(); err != nil {
			return err //nolint:wrapcheck // propagate context cancellation
		}
		result.Total++

refName, err := RefName(cid)
		if err != nil {
			// A malformed id on the branch can't map to a ref; skip it rather
			// than aborting the whole migration.
			logging.Warn(ctx, "migrate: skipping checkpoint with unmappable id",
				slog.String("id", cid.String()), slog.String("error", err.Error()))
			return nil
		}

// Resolve the existing ref once: it drives both the idempotency check and
		// the parent of the new commit.
		parent := plumbing.ZeroHash
		if existing, err := repo.Reference(refName, true); err == nil {
			parent = existing.Hash()
			if commit, cerr := repo.CommitObject(parent); cerr == nil && commit.TreeHash == cpTreeHash {
				result.Skipped++
				return nil
			}
		}

if dryRun {
			result.Migrated = append(result.Migrated, cid)
			return nil
		}

// Wrap the checkpoint's current subtree in a fresh commit — parenting on
		// the existing ref when present (re-migration fast-forwards) or as an
		// orphan for a brand-new ref — then point the ref at it and enqueue it.
		msg := fmt.Sprintf("Import checkpoint %s (migrated from git-branch)", cid)
		commitHash, err := CreateCommit(ctx, repo, cpTreeHash, parent, msg, authorName, authorEmail)
		if err != nil {
			return fmt.Errorf("commit checkpoint %s: %w", cid, err)
		}
		if err := refsStore.setRef(ctx, cid, commitHash); err != nil {
			return fmt.Errorf("set ref for checkpoint %s: %w", cid, err)
		}
		result.Migrated = append(result.Migrated, cid)
		return nil
	})
	if walkErr != nil {
		return result, fmt.Errorf("walk v1 checkpoints: %w", walkErr)
	}
	return result, nil
}
```

```go
func TestMigrateBranchToRefs(t *testing.T) {
	t.Parallel()
	repo, _ := setupBranchTestRepo(t)
	ctx := context.Background()
	branch := NewGitStore(repo, DefaultV1Refs())

cid1 := id.MustCheckpointID("a1b2c3d4e5f6")
	cid2 := id.MustCheckpointID("b2c3d4e5f6a1")
	seedBranchCheckpoint(t, branch, cid1, "s1")
	seedBranchCheckpoint(t, branch, cid2, "s2")

result, err := MigrateBranchToRefs(ctx, repo, false)
	require.NoError(t, err)
	assert.Equal(t, 2, result.Total)
	assert.Len(t, result.Migrated, 2)
	assert.Equal(t, 0, result.Skipped)

// Each checkpoint now has a ref whose commit tree IS the branch subtree
	// (byte-identical), and it reads back through the git-refs store.
	branchTree, err := branch.getSessionsBranchTree()
	require.NoError(t, err)
	refsStore := newGitRefsStore(repo)
	for _, cid := range []id.CheckpointID{cid1, cid2} {
		commit, err := repo.CommitObject(refHash(t, repo, cid))
		require.NoError(t, err)

branchSub, err := refsStore.subtreeObjAt(branchTree.Hash, cid.Path())
		require.NoError(t, err)
		require.NotNil(t, branchSub)
		assert.Equal(t, branchSub.Hash, commit.TreeHash,
			"ref tree must be the branch subtree, byte-identical")

// A migration commit wraps the tree with no parent (orphan).
		assert.Empty(t, commit.ParentHashes, "first migration commit is an orphan")

summary, err := refsStore.Read(ctx, cid)
		require.NoError(t, err)
		require.NotNil(t, summary, "migrated checkpoint should read via git-refs")
		assert.Equal(t, cid, summary.CheckpointID)
	}

// Idempotent: a second run skips everything and leaves the refs untouched.
	before := map[string]plumbing.Hash{cid1.String(): refHash(t, repo, cid1), cid2.String(): refHash(t, repo, cid2)}
	result2, err := MigrateBranchToRefs(ctx, repo, false)
	require.NoError(t, err)
	assert.Equal(t, 2, result2.Total)
	assert.Empty(t, result2.Migrated, "nothing to migrate on a repeat run")
	assert.Equal(t, 2, result2.Skipped)
	assert.Equal(t, before[cid1.String()], refHash(t, repo, cid1), "idempotent re-run must not move refs")
	assert.Equal(t, before[cid2.String()], refHash(t, repo, cid2))
}
```

```go
func TestMigrateBranchToRefs_AdvancesOnBranchChange(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)
	first := refHash(t, repo, cid)

// The branch checkpoint gains a second session (its subtree changes).
	seedBranchCheckpoint(t, branch, cid, "s2")

result, err := MigrateBranchToRefs(ctx, repo, false)
	require.NoError(t, err)
	assert.Len(t, result.Migrated, 1, "changed checkpoint is re-migrated")
	assert.Equal(t, 0, result.Skipped)

second := refHash(t, repo, cid)
	assert.NotEqual(t, first, second, "ref advances to the new tree")

// The advance is a fast-forward: the prior migration commit is the parent,
	// so no history is lost.
	commit, err := repo.CommitObject(second)
	require.NoError(t, err)
	require.Len(t, commit.ParentHashes, 1)
	assert.Equal(t, first, commit.ParentHashes[0])
}
```

```go
func TestMigrateBranchToRefs_DryRunWritesNothing(t *testing.T) {
	t.Parallel()
	repo, _ := setupBranchTestRepo(t)
	ctx := context.Background()
	branch := NewGitStore(repo, DefaultV1Refs())
	cid := id.MustCheckpointID("a1b2c3d4e5f6")
	seedBranchCheckpoint(t, branch, cid, "s1")

result, err := MigrateBranchToRefs(ctx, repo, true)
	require.NoError(t, err)
	assert.Equal(t, 1, result.Total)
	assert.Len(t, result.Migrated, 1, "dry-run reports what would migrate")

refName, err := RefName(cid)
	require.NoError(t, err)
	_, err = repo.Reference(refName, true)
	assert.ErrorIs(t, err, plumbing.ErrReferenceNotFound, "dry-run must not write refs")
}
```

```go
func TestMigrateBranchToRefs_NoBranchIsNoop(t *testing.T) {
	t.Parallel()
	repo, _ := setupBranchTestRepo(t) // initial commit only; no v1 checkpoint branch yet
	result, err := MigrateBranchToRefs(context.Background(), repo, false)
	require.NoError(t, err)
	assert.Equal(t, 0, result.Total)
	assert.Empty(t, result.Migrated)
}
```
