# Merge pull request #1611 from entireio/feat/checkpoint-migrate

`6c5bd6c`→[main](/content/gh/entireio/cli/commits/main/index.html)·
gtrrz-victor·1w ago·7 files·+1,148 added/-25 removed

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

## Changes

- cmd/entire/cli
  - checkpoint
    - Amigrate.go+325
    - Amigrate_test.go+484

```go
package checkpoint

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"strings"

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

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

// MigrateResult summarizes a git-branch → git-refs checkpoint migration.
type MigrateResult struct {
	Total   int
	Migrated []id.CheckpointID
	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>.
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) {
			return result, nil
		}
		return result, fmt.Errorf("read v1 checkpoint branch: %w", err)
	}
	
	refsStore := newGitRefsStore(repo)
	authorName, authorEmail := GetGitAuthorFromRepo(repo)
	queue, err := PushQueueForRepo(ctx, repo)
	if err != nil {
		return result, fmt.Errorf("resolve push queue: %w", err)
	}
	
	walkErr := WalkCheckpointShards(ctx, repo, tree, func(cid id.CheckpointID, cpTreeHash plumbing.Hash) error {
		result.Total++

if err := ctx.Err(); err != nil {
			return err
		}

msg := fmt.Sprintf("Import checkpoint %s (migrated from git-branch)", cid)
		commitHash, err := CreateCommit(ctx, repo, migratedTree, parent, msg, authorName, authorEmail)
		if err != nil {
			return fmt.Errorf("commit checkpoint %s: %w", cid, err)
		}

return nil
	})
	if walkErr != nil {
		return result, fmt.Errorf("walk v1 checkpoints: %w", walkErr)
	}
	return result, nil
}

// migratedCheckpointTree returns the branch subtree with its root metadata.json
// normalized for the refs layout.
func migratedCheckpointTree(ctx context.Context, repo *git.Repository, cid id.CheckpointID, cpTreeHash plumbing.Hash, persist bool) (plumbing.Hash, error) {
	subtree, err := repo.TreeObject(cpTreeHash)
	if err != nil {
		return plumbing.ZeroHash, fmt.Errorf("read checkpoint tree: %w", err)
	}
	
	metadataFile, err := subtree.File(paths.MetadataFileName)
	if err != nil {
		if errors.Is(err, object.ErrFileNotFound) {
			return cpTreeHash, nil
		}
		return plumbing.ZeroHash, fmt.Errorf("read metadata.json: %w", err)
	}

raw, err := metadataFile.Contents()
	if err != nil {
		return plumbing.ZeroHash, fmt.Errorf("read metadata.json: %w", err)
	}

normalized, changed, err := normalizeMigratedMetadata([]byte(raw), cid)
	if err != nil {
		return plumbing.ZeroHash, err
	}

if !changed {
		return cpTreeHash, nil
	}

blobHash, err := CreateBlobFromContent(repo, normalized)
	if err != nil {
		return plumbing.ZeroHash, fmt.Errorf("write normalized metadata.json: %w", err)
	}
	newTree, err := ApplyTreeChanges(ctx, repo, cpTreeHash, []TreeChange{{
		Path: paths.MetadataFileName,
		Entry: &object.TreeEntry{Name: paths.MetadataFileName, Mode: filemode.Regular, Hash: blobHash},
	}})
	if err != nil {
		return plumbing.ZeroHash, fmt.Errorf("build normalized checkpoint tree: %w", err)
	}
	return newTree, nil
}

// normalizeMigratedMetadata rewrites a checkpoint's root metadata.json for the refs layout.
func normalizeMigratedMetadata(raw []byte, cid id.CheckpointID) (normalized []byte, changed bool, err error) {
	var doc map[string]any
	if err := json.Unmarshal(raw, &doc); err != nil {
		return nil, false, fmt.Errorf("parse metadata.json: %w", err)
	}
	
	if _, ok := doc["checkpoint_version"]; ok {
		delete(doc, "checkpoint_version")
		changed = true
	}
	
	branchPrefix := "/" + cid.Path()
	if sessions, ok := doc["sessions"].([]any); ok {
		for _, entry := range sessions {
			session, ok := entry.(map[string]any)
			if !ok {
				continue
			}
			for field, raw := range session {
				value, ok := raw.(string)
				if !ok {
					continue
				}
				if rest, found := strings.CutPrefix(value, branchPrefix); found && strings.HasPrefix(rest, "/") {
					session[field] = rest
					changed = true
				}
			}
		}
	}
	if !changed {
		return nil, false, nil
	}

normalized, err = jsonutil.MarshalIndentWithNewline(doc, "", "  ")
	if err != nil {
		return nil, false, fmt.Errorf("encode metadata.json: %w", err)
	}
	return normalized, true, nil
}
```

```go
// TestMigrateBranchToRefs ensures migration of checkpoints works as expected.
func TestMigrateBranchToRefs(t *testing.T) {
	r := MigrateBranchToRefs(ctx, repo, false)
	assert.Equal(t, 2, r.Total)
	assert.Len(t, r.Migrated, 2)
	assert.Equal(t, 0, r.Skipped)
}
```

```go
package cli

import (
	"context"
	"testing"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestDoctorMigrateCheckpoints_RefusesWhenRefsPrimary(t *testing.T) {
	tmpDir := t.TempDir()
	// Initialize repository and configuration.
	
	...
}
```

### Summary
- The implementation involves significant changes in how checkpoints are handled in the Git repository, migrating them to a new storage structure under `refs` while preserving their metadata.
