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
- cmd/entire/cli
- checkpoint
- Amigrate.go+325
- Amigrate_test.go+484
- checkpoint
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
- The implementation involves significant changes in how checkpoints are handled in the Git repository, migrating them to a new storage structure under
refswhile preserving their metadata.