From 959e7e32d8db25052d7e0c3abb42cfa4a9948912 Mon Sep 17 00:00:00 2001 From: Daniel Vydra Date: Thu, 26 Feb 2026 12:52:37 +1100 Subject: [PATCH 1/6] fix: enable creates local metadata branch from remote when available When `entire enable` runs in a cloned repo that already has origin/entire/checkpoints/v1, it was creating an empty orphan branch that shadowed the remote data. This caused `entire explain` (and other checkpoint lookups) to fail with "checkpoint not found" because getSessionsBranchTree() found the empty local branch first. Now EnsureMetadataBranch() checks for the remote-tracking branch and creates the local branch from it, preserving existing checkpoint data. Co-Authored-By: Claude Opus 4.6 Entire-Checkpoint: 2865d24ae758 --- cmd/entire/cli/strategy/common.go | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/cmd/entire/cli/strategy/common.go b/cmd/entire/cli/strategy/common.go index 253e8a2c78..2045020e12 100644 --- a/cmd/entire/cli/strategy/common.go +++ b/cmd/entire/cli/strategy/common.go @@ -286,18 +286,32 @@ func resolveAgentType(ctxAgentType agent.AgentType, state *SessionState) agent.A } // ensureMetadataBranch creates the orphan entire/checkpoints/v1 branch if it doesn't exist. -// This branch has no parent and starts with an empty tree. +// If the remote-tracking branch (origin/entire/checkpoints/v1) exists, creates the local +// branch from it to preserve existing checkpoint data. Otherwise creates an empty orphan. func EnsureMetadataBranch(repo *git.Repository) error { refName := plumbing.NewBranchReferenceName(paths.MetadataBranchName) - // Check if branch already exists + // Check if local branch already exists _, err := repo.Reference(refName, true) if err == nil { // Branch already exists return nil } - // Create empty tree (no files) + // Check if remote-tracking branch exists (e.g., after clone) + remoteRefName := plumbing.NewRemoteReferenceName("origin", paths.MetadataBranchName) + remoteRef, remoteErr := repo.Reference(remoteRefName, true) + if remoteErr == nil { + // Remote branch exists — create local branch pointing to the same commit + ref := plumbing.NewHashReference(refName, remoteRef.Hash()) + if err := repo.Storer.SetReference(ref); err != nil { + return fmt.Errorf("failed to create metadata branch from remote: %w", err) + } + fmt.Fprintf(os.Stderr, "✓ Created local branch '%s' from origin\n", paths.MetadataBranchName) + return nil + } + + // No local or remote branch — create empty orphan emptyTree := &object.Tree{Entries: []object.TreeEntry{}} obj := repo.Storer.NewEncodedObject() if err := emptyTree.Encode(obj); err != nil { From 5efaeeeeda1c071415f43972b7d25f9aed62be88 Mon Sep 17 00:00:00 2001 From: Daniel Vydra Date: Thu, 26 Feb 2026 15:04:09 +1100 Subject: [PATCH 2/6] fix: update empty local metadata branch from remote on enable When enable creates an empty orphan entire/checkpoints/v1 branch, it shadows the remote-tracking branch that getSessionsBranchTree() would otherwise fall back to, breaking commands like explain. Fix EnsureMetadataBranch to detect when the local branch is an empty orphan and the remote has real checkpoint data, and update accordingly. Co-Authored-By: Claude Opus 4.6 Entire-Checkpoint: d9f7d656b38d --- cmd/entire/cli/strategy/common.go | 48 ++- .../strategy/ensure_metadata_branch_test.go | 363 ++++++++++++++++++ 2 files changed, 402 insertions(+), 9 deletions(-) create mode 100644 cmd/entire/cli/strategy/ensure_metadata_branch_test.go diff --git a/cmd/entire/cli/strategy/common.go b/cmd/entire/cli/strategy/common.go index 2045020e12..721dbe3739 100644 --- a/cmd/entire/cli/strategy/common.go +++ b/cmd/entire/cli/strategy/common.go @@ -285,24 +285,41 @@ func resolveAgentType(ctxAgentType agent.AgentType, state *SessionState) agent.A return DefaultAgentType } -// ensureMetadataBranch creates the orphan entire/checkpoints/v1 branch if it doesn't exist. -// If the remote-tracking branch (origin/entire/checkpoints/v1) exists, creates the local -// branch from it to preserve existing checkpoint data. Otherwise creates an empty orphan. +// EnsureMetadataBranch creates or updates the local entire/checkpoints/v1 branch. +// +// Three scenarios: +// 1. Local branch doesn't exist, remote does → create local from remote +// 2. Local branch exists but is empty, remote has data → update local from remote +// 3. No local or remote branch → create empty orphan func EnsureMetadataBranch(repo *git.Repository) error { refName := plumbing.NewBranchReferenceName(paths.MetadataBranchName) + // Check if remote-tracking branch exists (e.g., after clone/fetch) + remoteRefName := plumbing.NewRemoteReferenceName("origin", paths.MetadataBranchName) + remoteRef, remoteErr := repo.Reference(remoteRefName, true) + // Check if local branch already exists - _, err := repo.Reference(refName, true) + localRef, err := repo.Reference(refName, true) if err == nil { - // Branch already exists + // Local branch exists — check if it's an empty orphan that should be + // updated from remote. This handles the case where enable was run before + // the remote had checkpoint data (creating an empty orphan), and now the + // remote has been fetched with real data. + if remoteErr == nil && localRef.Hash() != remoteRef.Hash() { + localIsEmpty, checkErr := isEmptyMetadataBranch(repo, localRef) + if checkErr == nil && localIsEmpty { + ref := plumbing.NewHashReference(refName, remoteRef.Hash()) + if err := repo.Storer.SetReference(ref); err != nil { + return fmt.Errorf("failed to update metadata branch from remote: %w", err) + } + fmt.Fprintf(os.Stderr, "✓ Updated local branch '%s' from origin\n", paths.MetadataBranchName) + } + } return nil } - // Check if remote-tracking branch exists (e.g., after clone) - remoteRefName := plumbing.NewRemoteReferenceName("origin", paths.MetadataBranchName) - remoteRef, remoteErr := repo.Reference(remoteRefName, true) + // Local branch doesn't exist — create from remote if available if remoteErr == nil { - // Remote branch exists — create local branch pointing to the same commit ref := plumbing.NewHashReference(refName, remoteRef.Hash()) if err := repo.Storer.SetReference(ref); err != nil { return fmt.Errorf("failed to create metadata branch from remote: %w", err) @@ -358,6 +375,19 @@ func EnsureMetadataBranch(repo *git.Repository) error { return nil } +// isEmptyMetadataBranch returns true if the branch ref points to a commit with an empty tree. +func isEmptyMetadataBranch(repo *git.Repository, ref *plumbing.Reference) (bool, error) { + commit, err := repo.CommitObject(ref.Hash()) + if err != nil { + return false, fmt.Errorf("failed to get commit: %w", err) + } + tree, err := commit.Tree() + if err != nil { + return false, fmt.Errorf("failed to get tree: %w", err) + } + return len(tree.Entries) == 0, nil +} + // readCheckpointMetadata reads metadata.json from a checkpoint path on entire/checkpoints/v1. // With the new format, root metadata.json is a CheckpointSummary with Agents array. // This function reads the summary and extracts relevant fields into CheckpointInfo, diff --git a/cmd/entire/cli/strategy/ensure_metadata_branch_test.go b/cmd/entire/cli/strategy/ensure_metadata_branch_test.go new file mode 100644 index 0000000000..2813526e2c --- /dev/null +++ b/cmd/entire/cli/strategy/ensure_metadata_branch_test.go @@ -0,0 +1,363 @@ +package strategy + +import ( + "context" + "os" + "os/exec" + "path/filepath" + "testing" + + "github.com/entireio/cli/cmd/entire/cli/paths" + + "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/plumbing" + "github.com/go-git/go-git/v5/plumbing/object" +) + +// createRemoteWithCheckpoints sets up a bare repo with a main branch and an +// entire/checkpoints/v1 branch containing checkpoint data. Returns the bare +// repo dir and the commit hash on the metadata branch. +func createRemoteWithCheckpoints(t *testing.T) (string, plumbing.Hash) { + t.Helper() + + bareDir := t.TempDir() + bareRepo, err := git.PlainInit(bareDir, true) + if err != nil { + t.Fatalf("failed to init bare repo: %v", err) + } + + sig := object.Signature{Name: "Test", Email: "test@test.com"} + + // Create checkpoint data on entire/checkpoints/v1 + checkpointContent := []byte(`{"checkpoint_id": "test123"}`) + blob := bareRepo.Storer.NewEncodedObject() + blob.SetType(plumbing.BlobObject) + blob.SetSize(int64(len(checkpointContent))) + w, err := blob.Writer() + if err != nil { + t.Fatalf("failed to get blob writer: %v", err) + } + if _, err := w.Write(checkpointContent); err != nil { + t.Fatalf("failed to write blob: %v", err) + } + if err := w.Close(); err != nil { + t.Fatalf("failed to close blob writer: %v", err) + } + blobHash, err := bareRepo.Storer.SetEncodedObject(blob) + if err != nil { + t.Fatalf("failed to store blob: %v", err) + } + + tree := &object.Tree{ + Entries: []object.TreeEntry{ + {Name: "metadata.json", Mode: 0o100644, Hash: blobHash}, + }, + } + treeObj := bareRepo.Storer.NewEncodedObject() + if err := tree.Encode(treeObj); err != nil { + t.Fatalf("failed to encode tree: %v", err) + } + treeHash, err := bareRepo.Storer.SetEncodedObject(treeObj) + if err != nil { + t.Fatalf("failed to store tree: %v", err) + } + + commit := &object.Commit{ + TreeHash: treeHash, Author: sig, Committer: sig, + Message: "Checkpoint: test123\n", + } + commitObj := bareRepo.Storer.NewEncodedObject() + if err := commit.Encode(commitObj); err != nil { + t.Fatalf("failed to encode commit: %v", err) + } + commitHash, err := bareRepo.Storer.SetEncodedObject(commitObj) + if err != nil { + t.Fatalf("failed to store commit: %v", err) + } + + metadataRef := plumbing.NewHashReference( + plumbing.NewBranchReferenceName(paths.MetadataBranchName), commitHash, + ) + if err := bareRepo.Storer.SetReference(metadataRef); err != nil { + t.Fatalf("failed to set metadata branch ref: %v", err) + } + + // Create main branch + mainBlobHash := createBlobObject(t, bareRepo, []byte("test")) + mainTree := &object.Tree{ + Entries: []object.TreeEntry{ + {Name: "README.md", Mode: 0o100644, Hash: mainBlobHash}, + }, + } + mainTreeHash := createTreeObject(t, bareRepo, mainTree) + + mainCommit := &object.Commit{ + TreeHash: mainTreeHash, Author: sig, Committer: sig, + Message: "Initial commit\n", + } + mainCommitHash := createCommitObject(t, bareRepo, mainCommit) + + if err := bareRepo.Storer.SetReference(plumbing.NewHashReference(plumbing.NewBranchReferenceName("main"), mainCommitHash)); err != nil { + t.Fatalf("failed to set main ref: %v", err) + } + if err := bareRepo.Storer.SetReference(plumbing.NewSymbolicReference(plumbing.HEAD, plumbing.NewBranchReferenceName("main"))); err != nil { + t.Fatalf("failed to set HEAD: %v", err) + } + + return bareDir, commitHash +} + +func createBlobObject(t *testing.T, repo *git.Repository, content []byte) plumbing.Hash { + t.Helper() + blob := repo.Storer.NewEncodedObject() + blob.SetType(plumbing.BlobObject) + blob.SetSize(int64(len(content))) + w, err := blob.Writer() + if err != nil { + t.Fatalf("failed to get blob writer: %v", err) + } + if _, err := w.Write(content); err != nil { + t.Fatalf("failed to write blob: %v", err) + } + if err := w.Close(); err != nil { + t.Fatalf("failed to close blob writer: %v", err) + } + hash, err := repo.Storer.SetEncodedObject(blob) + if err != nil { + t.Fatalf("failed to store blob: %v", err) + } + return hash +} + +func createTreeObject(t *testing.T, repo *git.Repository, tree *object.Tree) plumbing.Hash { + t.Helper() + obj := repo.Storer.NewEncodedObject() + if err := tree.Encode(obj); err != nil { + t.Fatalf("failed to encode tree: %v", err) + } + hash, err := repo.Storer.SetEncodedObject(obj) + if err != nil { + t.Fatalf("failed to store tree: %v", err) + } + return hash +} + +func createCommitObject(t *testing.T, repo *git.Repository, commit *object.Commit) plumbing.Hash { + t.Helper() + obj := repo.Storer.NewEncodedObject() + if err := commit.Encode(obj); err != nil { + t.Fatalf("failed to encode commit: %v", err) + } + hash, err := repo.Storer.SetEncodedObject(obj) + if err != nil { + t.Fatalf("failed to store commit: %v", err) + } + return hash +} + +// cloneAndOpen clones bareDir via native git and opens with go-git. +func cloneAndOpen(t *testing.T, bareDir string) (*git.Repository, string) { + t.Helper() + cloneDir := filepath.Join(t.TempDir(), "clone") + cmd := exec.CommandContext(context.Background(), "git", "clone", bareDir, cloneDir) + out, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("failed to clone: %v\noutput: %s", err, out) + } + repo, err := git.PlainOpenWithOptions(cloneDir, &git.PlainOpenOptions{ + EnableDotGitCommonDir: true, + }) + if err != nil { + t.Fatalf("failed to open cloned repo: %v", err) + } + return repo, cloneDir +} + +// assertMetadataBranchHasData verifies the local metadata branch has non-empty tree +// data matching the expected commit hash. +func assertMetadataBranchHasData(t *testing.T, repo *git.Repository, expectedHash plumbing.Hash) { + t.Helper() + refName := plumbing.NewBranchReferenceName(paths.MetadataBranchName) + localRef, err := repo.Reference(refName, true) + if err != nil { + t.Fatalf("local metadata branch not found: %v", err) + } + commit, err := repo.CommitObject(localRef.Hash()) + if err != nil { + t.Fatalf("failed to get commit: %v", err) + } + tree, err := commit.Tree() + if err != nil { + t.Fatalf("failed to get tree: %v", err) + } + if len(tree.Entries) == 0 { + t.Error("local metadata branch has empty tree — checkpoint data was NOT preserved from remote") + } + if localRef.Hash() != expectedHash { + t.Errorf("local branch hash %s != expected %s", localRef.Hash(), expectedHash) + } +} + +// TestEnsureMetadataBranch_FromRemote tests that EnsureMetadataBranch creates +// the local branch from the remote-tracking branch when available (fresh clone). +func TestEnsureMetadataBranch_FromRemote(t *testing.T) { + bareDir, commitHash := createRemoteWithCheckpoints(t) + repo, cloneDir := cloneAndOpen(t, bareDir) + + if err := EnsureMetadataBranch(repo); err != nil { + t.Fatalf("EnsureMetadataBranch() failed: %v", err) + } + + assertMetadataBranchHasData(t, repo, commitHash) + + // Verify via native git too + cmd := exec.CommandContext(context.Background(), "git", "ls-tree", "refs/heads/"+paths.MetadataBranchName) + cmd.Dir = cloneDir + out, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("git ls-tree failed: %v\n%s", err, out) + } + if len(out) == 0 { + t.Error("native git shows empty tree for local metadata branch") + } +} + +// TestEnsureMetadataBranch_EmptyOrphanUpdatedFromRemote is the critical test: +// if the local branch was previously created as an empty orphan (e.g., enable ran +// before the remote had data), and now the remote has data, ensure the local +// branch is updated from the remote. +func TestEnsureMetadataBranch_EmptyOrphanUpdatedFromRemote(t *testing.T) { + bareDir, commitHash := createRemoteWithCheckpoints(t) + repo, _ := cloneAndOpen(t, bareDir) + + // Simulate a pre-existing empty orphan (as if old enable created it) + refName := plumbing.NewBranchReferenceName(paths.MetadataBranchName) + emptyTreeHash := createTreeObject(t, repo, &object.Tree{Entries: []object.TreeEntry{}}) + + sig := object.Signature{Name: "Test", Email: "test@test.com"} + orphanHash := createCommitObject(t, repo, &object.Commit{ + TreeHash: emptyTreeHash, Author: sig, Committer: sig, + Message: "Initialize metadata branch\n", + }) + if err := repo.Storer.SetReference(plumbing.NewHashReference(refName, orphanHash)); err != nil { + t.Fatalf("failed to set orphan ref: %v", err) + } + + // Verify the local branch is currently empty + localRef, err := repo.Reference(refName, true) + if err != nil { + t.Fatalf("failed to read local ref: %v", err) + } + localCommit, err := repo.CommitObject(localRef.Hash()) + if err != nil { + t.Fatalf("failed to get commit: %v", err) + } + localTree, err := localCommit.Tree() + if err != nil { + t.Fatalf("failed to get tree: %v", err) + } + if len(localTree.Entries) != 0 { + t.Fatal("pre-condition failed: local branch should be empty") + } + + // Now call EnsureMetadataBranch — it should detect the empty local and update from remote + if err := EnsureMetadataBranch(repo); err != nil { + t.Fatalf("EnsureMetadataBranch() failed: %v", err) + } + + assertMetadataBranchHasData(t, repo, commitHash) +} + +// TestEnsureMetadataBranch_NoRemote tests that EnsureMetadataBranch creates +// an empty orphan when no remote branch exists. +func TestEnsureMetadataBranch_NoRemote(t *testing.T) { + t.Parallel() + + dir := t.TempDir() + repo, err := git.PlainInit(dir, false) + if err != nil { + t.Fatalf("failed to init repo: %v", err) + } + + testFile := filepath.Join(dir, "test.txt") + if err := os.WriteFile(testFile, []byte("test"), 0o644); err != nil { + t.Fatalf("failed to write file: %v", err) + } + wt, err := repo.Worktree() + if err != nil { + t.Fatalf("failed to get worktree: %v", err) + } + if _, err := wt.Add("test.txt"); err != nil { + t.Fatalf("failed to add: %v", err) + } + if _, err := wt.Commit("Initial", &git.CommitOptions{ + Author: &object.Signature{Name: "Test", Email: "test@test.com"}, + }); err != nil { + t.Fatalf("failed to commit: %v", err) + } + + if err := EnsureMetadataBranch(repo); err != nil { + t.Fatalf("EnsureMetadataBranch() failed: %v", err) + } + + refName := plumbing.NewBranchReferenceName(paths.MetadataBranchName) + ref, err := repo.Reference(refName, true) + if err != nil { + t.Fatalf("metadata branch not found: %v", err) + } + commit, err := repo.CommitObject(ref.Hash()) + if err != nil { + t.Fatalf("failed to get commit: %v", err) + } + tree, err := commit.Tree() + if err != nil { + t.Fatalf("failed to get tree: %v", err) + } + if len(tree.Entries) != 0 { + t.Errorf("expected empty tree, got %d entries", len(tree.Entries)) + } + + // Calling again should be a no-op + if err := EnsureMetadataBranch(repo); err != nil { + t.Fatalf("second EnsureMetadataBranch() failed: %v", err) + } +} + +// TestEnsureMetadataBranch_LocalWithDataNotOverwritten verifies that if the +// local branch already has checkpoint data, it is NOT overwritten by the remote. +func TestEnsureMetadataBranch_LocalWithDataNotOverwritten(t *testing.T) { + bareDir, _ := createRemoteWithCheckpoints(t) + repo, _ := cloneAndOpen(t, bareDir) + + // Create a local branch with different data (simulating local checkpoints) + refName := plumbing.NewBranchReferenceName(paths.MetadataBranchName) + localBlobHash := createBlobObject(t, repo, []byte(`{"checkpoint_id": "local456"}`)) + localTreeHash := createTreeObject(t, repo, &object.Tree{ + Entries: []object.TreeEntry{ + {Name: "local_data.json", Mode: 0o100644, Hash: localBlobHash}, + }, + }) + + sig := object.Signature{Name: "Test", Email: "test@test.com"} + localCommitHash := createCommitObject(t, repo, &object.Commit{ + TreeHash: localTreeHash, Author: sig, Committer: sig, + Message: "Local checkpoint\n", + }) + if err := repo.Storer.SetReference(plumbing.NewHashReference(refName, localCommitHash)); err != nil { + t.Fatalf("failed to set local ref: %v", err) + } + + // EnsureMetadataBranch should NOT overwrite the local data + if err := EnsureMetadataBranch(repo); err != nil { + t.Fatalf("EnsureMetadataBranch() failed: %v", err) + } + + // Verify local branch still has its own data + localRef, err := repo.Reference(refName, true) + if err != nil { + t.Fatalf("failed to read local ref: %v", err) + } + if localRef.Hash() != localCommitHash { + t.Errorf("local branch was modified: got %s, want %s", localRef.Hash(), localCommitHash) + } +} From ab6ad18659ad741a47c7d1fabf633561e9783a53 Mon Sep 17 00:00:00 2001 From: Daniel Vydra Date: Thu, 26 Feb 2026 15:12:08 +1100 Subject: [PATCH 3/6] test: move EnsureMetadataBranch tests into common_test.go Use git CLI for test setup instead of manual plumbing objects. Reuse initTestRepo helper for the no-remote case. Co-Authored-By: Claude Opus 4.6 Entire-Checkpoint: 80e4ad299ce1 --- cmd/entire/cli/strategy/common_test.go | 161 ++++++++ .../strategy/ensure_metadata_branch_test.go | 363 ------------------ 2 files changed, 161 insertions(+), 363 deletions(-) delete mode 100644 cmd/entire/cli/strategy/ensure_metadata_branch_test.go diff --git a/cmd/entire/cli/strategy/common_test.go b/cmd/entire/cli/strategy/common_test.go index 34bd284897..aef67e69ff 100644 --- a/cmd/entire/cli/strategy/common_test.go +++ b/cmd/entire/cli/strategy/common_test.go @@ -992,6 +992,167 @@ func TestIsProtectedPath(t *testing.T) { } } +// initBareWithMetadataBranch creates a bare repo with a main branch and an +// entire/checkpoints/v1 branch containing checkpoint data via git CLI. +func initBareWithMetadataBranch(t *testing.T) string { + t.Helper() + bareDir := t.TempDir() + + // Init bare, create main branch with a commit + workDir := t.TempDir() + run := func(dir string, args ...string) { + cmd := exec.CommandContext(context.Background(), "git", args...) + cmd.Dir = dir + if out, err := cmd.CombinedOutput(); err != nil { + t.Fatalf("git %v failed: %v\n%s", args, err, out) + } + } + run(bareDir, "init", "--bare", "-b", "main") + run(workDir, "clone", bareDir, ".") + if err := os.WriteFile(filepath.Join(workDir, "README.md"), []byte("# Test"), 0o644); err != nil { + t.Fatalf("failed to write file: %v", err) + } + run(workDir, "add", ".") + run(workDir, "commit", "-m", "init") + run(workDir, "push", "origin", "main") + + // Create orphan entire/checkpoints/v1 with data + run(workDir, "checkout", "--orphan", paths.MetadataBranchName) + run(workDir, "rm", "-rf", ".") + if err := os.WriteFile(filepath.Join(workDir, "metadata.json"), []byte(`{"checkpoint_id":"test123"}`), 0o644); err != nil { + t.Fatalf("failed to write file: %v", err) + } + run(workDir, "add", ".") + run(workDir, "commit", "-m", "Checkpoint: test123") + run(workDir, "push", "origin", paths.MetadataBranchName) + + return bareDir +} + +func TestEnsureMetadataBranch(t *testing.T) { + t.Parallel() + + t.Run("creates from remote on fresh clone", func(t *testing.T) { + bareDir := initBareWithMetadataBranch(t) + cloneDir := filepath.Join(t.TempDir(), "clone") + cmd := exec.CommandContext(context.Background(), "git", "clone", bareDir, cloneDir) + if out, err := cmd.CombinedOutput(); err != nil { + t.Fatalf("clone failed: %v\n%s", err, out) + } + + repo, err := git.PlainOpenWithOptions(cloneDir, &git.PlainOpenOptions{EnableDotGitCommonDir: true}) + if err != nil { + t.Fatalf("failed to open repo: %v", err) + } + + if err := EnsureMetadataBranch(repo); err != nil { + t.Fatalf("EnsureMetadataBranch() failed: %v", err) + } + + // Local branch should exist with data (not empty) + ref, err := repo.Reference(plumbing.NewBranchReferenceName(paths.MetadataBranchName), true) + if err != nil { + t.Fatalf("local branch not found: %v", err) + } + commit, err := repo.CommitObject(ref.Hash()) + if err != nil { + t.Fatalf("failed to get commit: %v", err) + } + tree, err := commit.Tree() + if err != nil { + t.Fatalf("failed to get tree: %v", err) + } + if len(tree.Entries) == 0 { + t.Error("local branch has empty tree — remote data was not preserved") + } + }) + + t.Run("updates empty orphan from remote", func(t *testing.T) { + bareDir := initBareWithMetadataBranch(t) + cloneDir := filepath.Join(t.TempDir(), "clone") + cmd := exec.CommandContext(context.Background(), "git", "clone", bareDir, cloneDir) + if out, err := cmd.CombinedOutput(); err != nil { + t.Fatalf("clone failed: %v\n%s", err, out) + } + + repo, err := git.PlainOpenWithOptions(cloneDir, &git.PlainOpenOptions{EnableDotGitCommonDir: true}) + if err != nil { + t.Fatalf("failed to open repo: %v", err) + } + + // Create an empty orphan locally (simulates old enable behavior) + emptyTree := &object.Tree{Entries: []object.TreeEntry{}} + treeObj := repo.Storer.NewEncodedObject() + if err := emptyTree.Encode(treeObj); err != nil { + t.Fatalf("failed to encode tree: %v", err) + } + treeHash, err := repo.Storer.SetEncodedObject(treeObj) + if err != nil { + t.Fatalf("failed to store tree: %v", err) + } + orphan := &object.Commit{ + TreeHash: treeHash, + Author: object.Signature{Name: "Test", Email: "test@test.com"}, + Message: "Initialize metadata branch\n", + } + orphanObj := repo.Storer.NewEncodedObject() + if err := orphan.Encode(orphanObj); err != nil { + t.Fatalf("failed to encode commit: %v", err) + } + orphanHash, err := repo.Storer.SetEncodedObject(orphanObj) + if err != nil { + t.Fatalf("failed to store commit: %v", err) + } + refName := plumbing.NewBranchReferenceName(paths.MetadataBranchName) + if err := repo.Storer.SetReference(plumbing.NewHashReference(refName, orphanHash)); err != nil { + t.Fatalf("failed to set ref: %v", err) + } + + if err := EnsureMetadataBranch(repo); err != nil { + t.Fatalf("EnsureMetadataBranch() failed: %v", err) + } + + // Should have been updated from remote — no longer empty + ref, err := repo.Reference(refName, true) + if err != nil { + t.Fatalf("local branch not found: %v", err) + } + if ref.Hash() == orphanHash { + t.Error("local branch still points to empty orphan — was not updated from remote") + } + }) + + t.Run("creates empty orphan when no remote", func(t *testing.T) { + t.Parallel() + dir := t.TempDir() + initTestRepo(t, dir) + repo, err := git.PlainOpen(dir) + if err != nil { + t.Fatalf("failed to open repo: %v", err) + } + + if err := EnsureMetadataBranch(repo); err != nil { + t.Fatalf("EnsureMetadataBranch() failed: %v", err) + } + + ref, err := repo.Reference(plumbing.NewBranchReferenceName(paths.MetadataBranchName), true) + if err != nil { + t.Fatalf("branch not found: %v", err) + } + commit, err := repo.CommitObject(ref.Hash()) + if err != nil { + t.Fatalf("failed to get commit: %v", err) + } + tree, err := commit.Tree() + if err != nil { + t.Fatalf("failed to get tree: %v", err) + } + if len(tree.Entries) != 0 { + t.Errorf("expected empty tree, got %d entries", len(tree.Entries)) + } + }) +} + func TestIsEmptyRepository(t *testing.T) { t.Parallel() t.Run("empty repo returns true", func(t *testing.T) { diff --git a/cmd/entire/cli/strategy/ensure_metadata_branch_test.go b/cmd/entire/cli/strategy/ensure_metadata_branch_test.go deleted file mode 100644 index 2813526e2c..0000000000 --- a/cmd/entire/cli/strategy/ensure_metadata_branch_test.go +++ /dev/null @@ -1,363 +0,0 @@ -package strategy - -import ( - "context" - "os" - "os/exec" - "path/filepath" - "testing" - - "github.com/entireio/cli/cmd/entire/cli/paths" - - "github.com/go-git/go-git/v5" - "github.com/go-git/go-git/v5/plumbing" - "github.com/go-git/go-git/v5/plumbing/object" -) - -// createRemoteWithCheckpoints sets up a bare repo with a main branch and an -// entire/checkpoints/v1 branch containing checkpoint data. Returns the bare -// repo dir and the commit hash on the metadata branch. -func createRemoteWithCheckpoints(t *testing.T) (string, plumbing.Hash) { - t.Helper() - - bareDir := t.TempDir() - bareRepo, err := git.PlainInit(bareDir, true) - if err != nil { - t.Fatalf("failed to init bare repo: %v", err) - } - - sig := object.Signature{Name: "Test", Email: "test@test.com"} - - // Create checkpoint data on entire/checkpoints/v1 - checkpointContent := []byte(`{"checkpoint_id": "test123"}`) - blob := bareRepo.Storer.NewEncodedObject() - blob.SetType(plumbing.BlobObject) - blob.SetSize(int64(len(checkpointContent))) - w, err := blob.Writer() - if err != nil { - t.Fatalf("failed to get blob writer: %v", err) - } - if _, err := w.Write(checkpointContent); err != nil { - t.Fatalf("failed to write blob: %v", err) - } - if err := w.Close(); err != nil { - t.Fatalf("failed to close blob writer: %v", err) - } - blobHash, err := bareRepo.Storer.SetEncodedObject(blob) - if err != nil { - t.Fatalf("failed to store blob: %v", err) - } - - tree := &object.Tree{ - Entries: []object.TreeEntry{ - {Name: "metadata.json", Mode: 0o100644, Hash: blobHash}, - }, - } - treeObj := bareRepo.Storer.NewEncodedObject() - if err := tree.Encode(treeObj); err != nil { - t.Fatalf("failed to encode tree: %v", err) - } - treeHash, err := bareRepo.Storer.SetEncodedObject(treeObj) - if err != nil { - t.Fatalf("failed to store tree: %v", err) - } - - commit := &object.Commit{ - TreeHash: treeHash, Author: sig, Committer: sig, - Message: "Checkpoint: test123\n", - } - commitObj := bareRepo.Storer.NewEncodedObject() - if err := commit.Encode(commitObj); err != nil { - t.Fatalf("failed to encode commit: %v", err) - } - commitHash, err := bareRepo.Storer.SetEncodedObject(commitObj) - if err != nil { - t.Fatalf("failed to store commit: %v", err) - } - - metadataRef := plumbing.NewHashReference( - plumbing.NewBranchReferenceName(paths.MetadataBranchName), commitHash, - ) - if err := bareRepo.Storer.SetReference(metadataRef); err != nil { - t.Fatalf("failed to set metadata branch ref: %v", err) - } - - // Create main branch - mainBlobHash := createBlobObject(t, bareRepo, []byte("test")) - mainTree := &object.Tree{ - Entries: []object.TreeEntry{ - {Name: "README.md", Mode: 0o100644, Hash: mainBlobHash}, - }, - } - mainTreeHash := createTreeObject(t, bareRepo, mainTree) - - mainCommit := &object.Commit{ - TreeHash: mainTreeHash, Author: sig, Committer: sig, - Message: "Initial commit\n", - } - mainCommitHash := createCommitObject(t, bareRepo, mainCommit) - - if err := bareRepo.Storer.SetReference(plumbing.NewHashReference(plumbing.NewBranchReferenceName("main"), mainCommitHash)); err != nil { - t.Fatalf("failed to set main ref: %v", err) - } - if err := bareRepo.Storer.SetReference(plumbing.NewSymbolicReference(plumbing.HEAD, plumbing.NewBranchReferenceName("main"))); err != nil { - t.Fatalf("failed to set HEAD: %v", err) - } - - return bareDir, commitHash -} - -func createBlobObject(t *testing.T, repo *git.Repository, content []byte) plumbing.Hash { - t.Helper() - blob := repo.Storer.NewEncodedObject() - blob.SetType(plumbing.BlobObject) - blob.SetSize(int64(len(content))) - w, err := blob.Writer() - if err != nil { - t.Fatalf("failed to get blob writer: %v", err) - } - if _, err := w.Write(content); err != nil { - t.Fatalf("failed to write blob: %v", err) - } - if err := w.Close(); err != nil { - t.Fatalf("failed to close blob writer: %v", err) - } - hash, err := repo.Storer.SetEncodedObject(blob) - if err != nil { - t.Fatalf("failed to store blob: %v", err) - } - return hash -} - -func createTreeObject(t *testing.T, repo *git.Repository, tree *object.Tree) plumbing.Hash { - t.Helper() - obj := repo.Storer.NewEncodedObject() - if err := tree.Encode(obj); err != nil { - t.Fatalf("failed to encode tree: %v", err) - } - hash, err := repo.Storer.SetEncodedObject(obj) - if err != nil { - t.Fatalf("failed to store tree: %v", err) - } - return hash -} - -func createCommitObject(t *testing.T, repo *git.Repository, commit *object.Commit) plumbing.Hash { - t.Helper() - obj := repo.Storer.NewEncodedObject() - if err := commit.Encode(obj); err != nil { - t.Fatalf("failed to encode commit: %v", err) - } - hash, err := repo.Storer.SetEncodedObject(obj) - if err != nil { - t.Fatalf("failed to store commit: %v", err) - } - return hash -} - -// cloneAndOpen clones bareDir via native git and opens with go-git. -func cloneAndOpen(t *testing.T, bareDir string) (*git.Repository, string) { - t.Helper() - cloneDir := filepath.Join(t.TempDir(), "clone") - cmd := exec.CommandContext(context.Background(), "git", "clone", bareDir, cloneDir) - out, err := cmd.CombinedOutput() - if err != nil { - t.Fatalf("failed to clone: %v\noutput: %s", err, out) - } - repo, err := git.PlainOpenWithOptions(cloneDir, &git.PlainOpenOptions{ - EnableDotGitCommonDir: true, - }) - if err != nil { - t.Fatalf("failed to open cloned repo: %v", err) - } - return repo, cloneDir -} - -// assertMetadataBranchHasData verifies the local metadata branch has non-empty tree -// data matching the expected commit hash. -func assertMetadataBranchHasData(t *testing.T, repo *git.Repository, expectedHash plumbing.Hash) { - t.Helper() - refName := plumbing.NewBranchReferenceName(paths.MetadataBranchName) - localRef, err := repo.Reference(refName, true) - if err != nil { - t.Fatalf("local metadata branch not found: %v", err) - } - commit, err := repo.CommitObject(localRef.Hash()) - if err != nil { - t.Fatalf("failed to get commit: %v", err) - } - tree, err := commit.Tree() - if err != nil { - t.Fatalf("failed to get tree: %v", err) - } - if len(tree.Entries) == 0 { - t.Error("local metadata branch has empty tree — checkpoint data was NOT preserved from remote") - } - if localRef.Hash() != expectedHash { - t.Errorf("local branch hash %s != expected %s", localRef.Hash(), expectedHash) - } -} - -// TestEnsureMetadataBranch_FromRemote tests that EnsureMetadataBranch creates -// the local branch from the remote-tracking branch when available (fresh clone). -func TestEnsureMetadataBranch_FromRemote(t *testing.T) { - bareDir, commitHash := createRemoteWithCheckpoints(t) - repo, cloneDir := cloneAndOpen(t, bareDir) - - if err := EnsureMetadataBranch(repo); err != nil { - t.Fatalf("EnsureMetadataBranch() failed: %v", err) - } - - assertMetadataBranchHasData(t, repo, commitHash) - - // Verify via native git too - cmd := exec.CommandContext(context.Background(), "git", "ls-tree", "refs/heads/"+paths.MetadataBranchName) - cmd.Dir = cloneDir - out, err := cmd.CombinedOutput() - if err != nil { - t.Fatalf("git ls-tree failed: %v\n%s", err, out) - } - if len(out) == 0 { - t.Error("native git shows empty tree for local metadata branch") - } -} - -// TestEnsureMetadataBranch_EmptyOrphanUpdatedFromRemote is the critical test: -// if the local branch was previously created as an empty orphan (e.g., enable ran -// before the remote had data), and now the remote has data, ensure the local -// branch is updated from the remote. -func TestEnsureMetadataBranch_EmptyOrphanUpdatedFromRemote(t *testing.T) { - bareDir, commitHash := createRemoteWithCheckpoints(t) - repo, _ := cloneAndOpen(t, bareDir) - - // Simulate a pre-existing empty orphan (as if old enable created it) - refName := plumbing.NewBranchReferenceName(paths.MetadataBranchName) - emptyTreeHash := createTreeObject(t, repo, &object.Tree{Entries: []object.TreeEntry{}}) - - sig := object.Signature{Name: "Test", Email: "test@test.com"} - orphanHash := createCommitObject(t, repo, &object.Commit{ - TreeHash: emptyTreeHash, Author: sig, Committer: sig, - Message: "Initialize metadata branch\n", - }) - if err := repo.Storer.SetReference(plumbing.NewHashReference(refName, orphanHash)); err != nil { - t.Fatalf("failed to set orphan ref: %v", err) - } - - // Verify the local branch is currently empty - localRef, err := repo.Reference(refName, true) - if err != nil { - t.Fatalf("failed to read local ref: %v", err) - } - localCommit, err := repo.CommitObject(localRef.Hash()) - if err != nil { - t.Fatalf("failed to get commit: %v", err) - } - localTree, err := localCommit.Tree() - if err != nil { - t.Fatalf("failed to get tree: %v", err) - } - if len(localTree.Entries) != 0 { - t.Fatal("pre-condition failed: local branch should be empty") - } - - // Now call EnsureMetadataBranch — it should detect the empty local and update from remote - if err := EnsureMetadataBranch(repo); err != nil { - t.Fatalf("EnsureMetadataBranch() failed: %v", err) - } - - assertMetadataBranchHasData(t, repo, commitHash) -} - -// TestEnsureMetadataBranch_NoRemote tests that EnsureMetadataBranch creates -// an empty orphan when no remote branch exists. -func TestEnsureMetadataBranch_NoRemote(t *testing.T) { - t.Parallel() - - dir := t.TempDir() - repo, err := git.PlainInit(dir, false) - if err != nil { - t.Fatalf("failed to init repo: %v", err) - } - - testFile := filepath.Join(dir, "test.txt") - if err := os.WriteFile(testFile, []byte("test"), 0o644); err != nil { - t.Fatalf("failed to write file: %v", err) - } - wt, err := repo.Worktree() - if err != nil { - t.Fatalf("failed to get worktree: %v", err) - } - if _, err := wt.Add("test.txt"); err != nil { - t.Fatalf("failed to add: %v", err) - } - if _, err := wt.Commit("Initial", &git.CommitOptions{ - Author: &object.Signature{Name: "Test", Email: "test@test.com"}, - }); err != nil { - t.Fatalf("failed to commit: %v", err) - } - - if err := EnsureMetadataBranch(repo); err != nil { - t.Fatalf("EnsureMetadataBranch() failed: %v", err) - } - - refName := plumbing.NewBranchReferenceName(paths.MetadataBranchName) - ref, err := repo.Reference(refName, true) - if err != nil { - t.Fatalf("metadata branch not found: %v", err) - } - commit, err := repo.CommitObject(ref.Hash()) - if err != nil { - t.Fatalf("failed to get commit: %v", err) - } - tree, err := commit.Tree() - if err != nil { - t.Fatalf("failed to get tree: %v", err) - } - if len(tree.Entries) != 0 { - t.Errorf("expected empty tree, got %d entries", len(tree.Entries)) - } - - // Calling again should be a no-op - if err := EnsureMetadataBranch(repo); err != nil { - t.Fatalf("second EnsureMetadataBranch() failed: %v", err) - } -} - -// TestEnsureMetadataBranch_LocalWithDataNotOverwritten verifies that if the -// local branch already has checkpoint data, it is NOT overwritten by the remote. -func TestEnsureMetadataBranch_LocalWithDataNotOverwritten(t *testing.T) { - bareDir, _ := createRemoteWithCheckpoints(t) - repo, _ := cloneAndOpen(t, bareDir) - - // Create a local branch with different data (simulating local checkpoints) - refName := plumbing.NewBranchReferenceName(paths.MetadataBranchName) - localBlobHash := createBlobObject(t, repo, []byte(`{"checkpoint_id": "local456"}`)) - localTreeHash := createTreeObject(t, repo, &object.Tree{ - Entries: []object.TreeEntry{ - {Name: "local_data.json", Mode: 0o100644, Hash: localBlobHash}, - }, - }) - - sig := object.Signature{Name: "Test", Email: "test@test.com"} - localCommitHash := createCommitObject(t, repo, &object.Commit{ - TreeHash: localTreeHash, Author: sig, Committer: sig, - Message: "Local checkpoint\n", - }) - if err := repo.Storer.SetReference(plumbing.NewHashReference(refName, localCommitHash)); err != nil { - t.Fatalf("failed to set local ref: %v", err) - } - - // EnsureMetadataBranch should NOT overwrite the local data - if err := EnsureMetadataBranch(repo); err != nil { - t.Fatalf("EnsureMetadataBranch() failed: %v", err) - } - - // Verify local branch still has its own data - localRef, err := repo.Reference(refName, true) - if err != nil { - t.Fatalf("failed to read local ref: %v", err) - } - if localRef.Hash() != localCommitHash { - t.Errorf("local branch was modified: got %s, want %s", localRef.Hash(), localCommitHash) - } -} From 4642328d0c86f04cb3819a8ac92b71228ff5e610 Mon Sep 17 00:00:00 2001 From: Daniel Vydra Date: Thu, 26 Feb 2026 15:27:01 +1100 Subject: [PATCH 4/6] refactor: simplify EnsureMetadataBranch to only handle fresh clone case MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove scenario 2 (updating empty orphan from remote) — that will be a separate PR. This PR now only adds the remote-tracking branch check for repos where the local branch doesn't exist yet. Co-Authored-By: Claude Opus 4.6 Entire-Checkpoint: ca672efb0556 --- cmd/entire/cli/strategy/common.go | 44 +++------------------ cmd/entire/cli/strategy/common_test.go | 55 -------------------------- 2 files changed, 6 insertions(+), 93 deletions(-) diff --git a/cmd/entire/cli/strategy/common.go b/cmd/entire/cli/strategy/common.go index 721dbe3739..6e842d6665 100644 --- a/cmd/entire/cli/strategy/common.go +++ b/cmd/entire/cli/strategy/common.go @@ -285,40 +285,21 @@ func resolveAgentType(ctxAgentType agent.AgentType, state *SessionState) agent.A return DefaultAgentType } -// EnsureMetadataBranch creates or updates the local entire/checkpoints/v1 branch. -// -// Three scenarios: -// 1. Local branch doesn't exist, remote does → create local from remote -// 2. Local branch exists but is empty, remote has data → update local from remote -// 3. No local or remote branch → create empty orphan +// EnsureMetadataBranch creates the local entire/checkpoints/v1 branch if it doesn't exist. +// If the remote-tracking branch (origin/entire/checkpoints/v1) exists, creates the local +// branch from it to preserve existing checkpoint data. Otherwise creates an empty orphan. func EnsureMetadataBranch(repo *git.Repository) error { refName := plumbing.NewBranchReferenceName(paths.MetadataBranchName) - // Check if remote-tracking branch exists (e.g., after clone/fetch) - remoteRefName := plumbing.NewRemoteReferenceName("origin", paths.MetadataBranchName) - remoteRef, remoteErr := repo.Reference(remoteRefName, true) - // Check if local branch already exists - localRef, err := repo.Reference(refName, true) + _, err := repo.Reference(refName, true) if err == nil { - // Local branch exists — check if it's an empty orphan that should be - // updated from remote. This handles the case where enable was run before - // the remote had checkpoint data (creating an empty orphan), and now the - // remote has been fetched with real data. - if remoteErr == nil && localRef.Hash() != remoteRef.Hash() { - localIsEmpty, checkErr := isEmptyMetadataBranch(repo, localRef) - if checkErr == nil && localIsEmpty { - ref := plumbing.NewHashReference(refName, remoteRef.Hash()) - if err := repo.Storer.SetReference(ref); err != nil { - return fmt.Errorf("failed to update metadata branch from remote: %w", err) - } - fmt.Fprintf(os.Stderr, "✓ Updated local branch '%s' from origin\n", paths.MetadataBranchName) - } - } return nil } // Local branch doesn't exist — create from remote if available + remoteRefName := plumbing.NewRemoteReferenceName("origin", paths.MetadataBranchName) + remoteRef, remoteErr := repo.Reference(remoteRefName, true) if remoteErr == nil { ref := plumbing.NewHashReference(refName, remoteRef.Hash()) if err := repo.Storer.SetReference(ref); err != nil { @@ -375,19 +356,6 @@ func EnsureMetadataBranch(repo *git.Repository) error { return nil } -// isEmptyMetadataBranch returns true if the branch ref points to a commit with an empty tree. -func isEmptyMetadataBranch(repo *git.Repository, ref *plumbing.Reference) (bool, error) { - commit, err := repo.CommitObject(ref.Hash()) - if err != nil { - return false, fmt.Errorf("failed to get commit: %w", err) - } - tree, err := commit.Tree() - if err != nil { - return false, fmt.Errorf("failed to get tree: %w", err) - } - return len(tree.Entries) == 0, nil -} - // readCheckpointMetadata reads metadata.json from a checkpoint path on entire/checkpoints/v1. // With the new format, root metadata.json is a CheckpointSummary with Agents array. // This function reads the summary and extracts relevant fields into CheckpointInfo, diff --git a/cmd/entire/cli/strategy/common_test.go b/cmd/entire/cli/strategy/common_test.go index aef67e69ff..5461410976 100644 --- a/cmd/entire/cli/strategy/common_test.go +++ b/cmd/entire/cli/strategy/common_test.go @@ -1067,61 +1067,6 @@ func TestEnsureMetadataBranch(t *testing.T) { } }) - t.Run("updates empty orphan from remote", func(t *testing.T) { - bareDir := initBareWithMetadataBranch(t) - cloneDir := filepath.Join(t.TempDir(), "clone") - cmd := exec.CommandContext(context.Background(), "git", "clone", bareDir, cloneDir) - if out, err := cmd.CombinedOutput(); err != nil { - t.Fatalf("clone failed: %v\n%s", err, out) - } - - repo, err := git.PlainOpenWithOptions(cloneDir, &git.PlainOpenOptions{EnableDotGitCommonDir: true}) - if err != nil { - t.Fatalf("failed to open repo: %v", err) - } - - // Create an empty orphan locally (simulates old enable behavior) - emptyTree := &object.Tree{Entries: []object.TreeEntry{}} - treeObj := repo.Storer.NewEncodedObject() - if err := emptyTree.Encode(treeObj); err != nil { - t.Fatalf("failed to encode tree: %v", err) - } - treeHash, err := repo.Storer.SetEncodedObject(treeObj) - if err != nil { - t.Fatalf("failed to store tree: %v", err) - } - orphan := &object.Commit{ - TreeHash: treeHash, - Author: object.Signature{Name: "Test", Email: "test@test.com"}, - Message: "Initialize metadata branch\n", - } - orphanObj := repo.Storer.NewEncodedObject() - if err := orphan.Encode(orphanObj); err != nil { - t.Fatalf("failed to encode commit: %v", err) - } - orphanHash, err := repo.Storer.SetEncodedObject(orphanObj) - if err != nil { - t.Fatalf("failed to store commit: %v", err) - } - refName := plumbing.NewBranchReferenceName(paths.MetadataBranchName) - if err := repo.Storer.SetReference(plumbing.NewHashReference(refName, orphanHash)); err != nil { - t.Fatalf("failed to set ref: %v", err) - } - - if err := EnsureMetadataBranch(repo); err != nil { - t.Fatalf("EnsureMetadataBranch() failed: %v", err) - } - - // Should have been updated from remote — no longer empty - ref, err := repo.Reference(refName, true) - if err != nil { - t.Fatalf("local branch not found: %v", err) - } - if ref.Hash() == orphanHash { - t.Error("local branch still points to empty orphan — was not updated from remote") - } - }) - t.Run("creates empty orphan when no remote", func(t *testing.T) { t.Parallel() dir := t.TempDir() From e5562b215616b039ccdbd43b4f4fefdcc9501190 Mon Sep 17 00:00:00 2001 From: Daniel Vydra Date: Thu, 26 Feb 2026 16:13:12 +1100 Subject: [PATCH 5/6] fix: set git identity in test for CI environments CI runners don't have global git user.name/user.email configured, causing the test commit to fail. Co-Authored-By: Claude Opus 4.6 Entire-Checkpoint: 35b128464ad9 --- cmd/entire/cli/strategy/common_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cmd/entire/cli/strategy/common_test.go b/cmd/entire/cli/strategy/common_test.go index 5461410976..d02e15136e 100644 --- a/cmd/entire/cli/strategy/common_test.go +++ b/cmd/entire/cli/strategy/common_test.go @@ -1009,6 +1009,8 @@ func initBareWithMetadataBranch(t *testing.T) string { } run(bareDir, "init", "--bare", "-b", "main") run(workDir, "clone", bareDir, ".") + run(workDir, "config", "user.email", "test@test.com") + run(workDir, "config", "user.name", "Test User") if err := os.WriteFile(filepath.Join(workDir, "README.md"), []byte("# Test"), 0o644); err != nil { t.Fatalf("failed to write file: %v", err) } From 901778797ccc9fd3af0eade67a38026144d09ed0 Mon Sep 17 00:00:00 2001 From: Daniel Vydra Date: Thu, 26 Feb 2026 16:29:42 +1100 Subject: [PATCH 6/6] fix: distinguish ref-not-found from other errors in EnsureMetadataBranch Return wrapped errors for unexpected failures when checking local and remote metadata branch refs, instead of silently falling through. Only fall back to orphan creation when refs are genuinely absent. Co-Authored-By: Claude Opus 4.6 Entire-Checkpoint: 5cf83f1be508 --- cmd/entire/cli/strategy/common.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/cmd/entire/cli/strategy/common.go b/cmd/entire/cli/strategy/common.go index dcab0e9f0d..9dcc8b6b5c 100644 --- a/cmd/entire/cli/strategy/common.go +++ b/cmd/entire/cli/strategy/common.go @@ -296,10 +296,16 @@ func EnsureMetadataBranch(repo *git.Repository) error { if err == nil { return nil } + if !errors.Is(err, plumbing.ErrReferenceNotFound) { + return fmt.Errorf("failed to check metadata branch: %w", err) + } // Local branch doesn't exist — create from remote if available remoteRefName := plumbing.NewRemoteReferenceName("origin", paths.MetadataBranchName) remoteRef, remoteErr := repo.Reference(remoteRefName, true) + if remoteErr != nil && !errors.Is(remoteErr, plumbing.ErrReferenceNotFound) { + return fmt.Errorf("failed to check remote metadata branch: %w", remoteErr) + } if remoteErr == nil { ref := plumbing.NewHashReference(refName, remoteRef.Hash()) if err := repo.Storer.SetReference(ref); err != nil {