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

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

6c5bd6c→main· gtrrz-victor·1w ago·7 files·+1,148 added/-25 removed

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

Changes

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
}
// 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)
}
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