From aa5169fad9b12405553972ccf8ed259d03a8e9b1 Mon Sep 17 00:00:00 2001 From: Alisha Kawaguchi Date: Fri, 5 Jun 2026 14:25:10 -0700 Subject: [PATCH 1/2] fix: bootstrap metadata branch from checkpoint_remote during enable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a checkpoint_remote is configured and the repo is freshly cloned on a second device, neither the local entire/checkpoints/v1 branch nor origin's remote-tracking ref exists — the real branch lives on the checkpoint remote. EnsureSetup previously went straight to EnsureMetadataBranch, which minted an unrelated empty orphan, hiding existing checkpoints from `entire checkpoint list` and rejecting later fetches as non-fast-forward. EnsureSetup now fetches the metadata branch from the configured checkpoint_remote first (best-effort, only when no local branch exists), mirroring what `entire resume` already does. The fetch failing — e.g. the remote branch doesn't exist before the first push from any device — falls back to orphan creation as before. Fixes #1374 Co-Authored-By: Claude Opus 4.8 (1M context) --- .../cli/integration_test/http_remote_test.go | 70 +++++++++++++++++++ cmd/entire/cli/strategy/checkpoint_remote.go | 52 +++++++++++--- .../cli/strategy/checkpoint_remote_test.go | 11 ++- cmd/entire/cli/strategy/common.go | 13 ++++ 4 files changed, 135 insertions(+), 11 deletions(-) diff --git a/cmd/entire/cli/integration_test/http_remote_test.go b/cmd/entire/cli/integration_test/http_remote_test.go index 6a3da7e1f..232ebb8e4 100644 --- a/cmd/entire/cli/integration_test/http_remote_test.go +++ b/cmd/entire/cli/integration_test/http_remote_test.go @@ -446,3 +446,73 @@ func TestHTTPS_PushFailsWithoutToken(t *testing.T) { } assertRemoteHasCheckpointCommit(t, bareDir, checkpointID) } + +// TestHTTPS_EnableBootstrapsMetadataFromCheckpointRemote reproduces +// https://github.com/entireio/cli/issues/1374: when a second device clones a +// repo whose checkpoint data lives on a configured checkpoint_remote (a +// separate repo from origin), `entire enable` must populate the local +// entire/checkpoints/v1 branch from that remote instead of minting an +// unrelated empty orphan. The orphan made `entire checkpoint list` return +// nothing and caused non-fast-forward rejections on later fetches. +func TestHTTPS_EnableBootstrapsMetadataFromCheckpointRemote(t *testing.T) { + t.Parallel() + + srv := startGitHTTPSServer(t, "testorg/main-repo", "testorg/checkpoints") + env := NewFeatureBranchEnv(t) + + mainBare := srv.BareDirs["testorg/main-repo"] + checkpointBare := srv.BareDirs["testorg/checkpoints"] + httpsURL := srv.URL + "/testorg/main-repo.git" + seedBareRepo(t, env, mainBare, httpsURL) + + checkpointRemoteSettings := map[string]any{ + "strategy_options": map[string]any{ + "checkpoint_remote": map[string]any{ + "provider": "github", + "repo": "testorg/checkpoints", + }, + }, + } + + // Device A: create a checkpoint and push — metadata routes to the + // checkpoint remote, never to origin. + cloneA := cloneFromBareWithHTTPS(t, env, mainBare, httpsURL) + cloneA.ExtraEnv = srv.tokenEnv("clone-a-token") + cloneA.GitCheckoutNewBranch("feature/clone-a") + cloneA.PatchSettings(checkpointRemoteSettings) + checkpointA := createCheckpointedCommit(t, cloneA, "Work in clone A", "a.go", "package a", "Work from A") + cloneA.RunPrePush("origin") + + if !cloneA.BranchExistsOnRemote(checkpointBare, paths.MetadataBranchName) { + t.Fatal("precondition: checkpoint branch should be on checkpoint remote after push") + } + if cloneA.BranchExistsOnRemote(mainBare, paths.MetadataBranchName) { + t.Fatal("precondition: checkpoint branch should NOT be on origin") + } + + // Device B: fresh clone of origin (which has no metadata branch) with the + // same checkpoint_remote configured. No token — fetch (upload-pack) is + // unauthenticated; only push requires one. + cloneB := cloneFromBareWithHTTPS(t, env, mainBare, httpsURL) + cloneB.ExtraEnv = srv.sslEnv() + cloneB.PatchSettings(checkpointRemoteSettings) + + cloneB.RunCLI("enable", "--agent", "claude-code", "--telemetry=false") + + // The local metadata branch must match the checkpoint remote tip — not be + // a fresh empty orphan with unrelated history. + if !cloneB.BranchExists(paths.MetadataBranchName) { + t.Fatal("local metadata branch should exist after enable") + } + remoteTip := revParse(t, checkpointBare, "refs/heads/"+paths.MetadataBranchName) + localTip := revParse(t, cloneB.RepoDir, "refs/heads/"+paths.MetadataBranchName) + if localTip != remoteTip { + t.Errorf("local metadata branch tip = %s, want checkpoint remote tip %s (empty-orphan bug: enable did not fetch from checkpoint_remote)", localTip, remoteTip) + } + + // The fetched branch must contain device A's checkpoint metadata. + summaryA := CheckpointSummaryPath(checkpointA) + if _, found := cloneB.ReadFileFromBranch(paths.MetadataBranchName, summaryA); !found { + t.Errorf("local metadata branch should contain checkpoint A summary at %s", summaryA) + } +} diff --git a/cmd/entire/cli/strategy/checkpoint_remote.go b/cmd/entire/cli/strategy/checkpoint_remote.go index f54426a14..d48ffed7d 100644 --- a/cmd/entire/cli/strategy/checkpoint_remote.go +++ b/cmd/entire/cli/strategy/checkpoint_remote.go @@ -4,12 +4,14 @@ import ( "context" "fmt" "log/slog" + "os" "strings" "time" "github.com/entireio/cli/cmd/entire/cli/checkpoint" "github.com/entireio/cli/cmd/entire/cli/checkpoint/remote" "github.com/entireio/cli/cmd/entire/cli/logging" + "github.com/entireio/cli/cmd/entire/cli/paths" "github.com/entireio/cli/cmd/entire/cli/settings" "github.com/go-git/go-git/v6/plumbing" @@ -86,7 +88,7 @@ func resolvePushSettings(ctx context.Context, pushRemoteName string) pushSetting // This is a one-time operation — once the branch exists locally, subsequent pushes // skip the fetch entirely. Only fetch the metadata branch; trails are always pushed // to the user's push remote, not the checkpoint remote. - if err := fetchMetadataBranchIfMissing(ctx, checkpointURL); err != nil { + if _, err := fetchMetadataBranchIfMissing(ctx, checkpointURL); err != nil { logging.Warn(ctx, "checkpoint-remote: failed to fetch metadata branch", slog.String("error", err.Error()), ) @@ -162,29 +164,63 @@ func fetchURLIntoTmpRef(ctx context.Context, remoteURL, srcRef, tmpRef, label st return fmt.Errorf("fetch %s from %s failed: %w", label, redactedURL, fetchErr) } +// bootstrapMetadataFromCheckpointRemote populates the local metadata branch +// from the configured checkpoint_remote when it doesn't exist locally yet. +// +// On a fresh clone of a repo whose checkpoints live in a separate repo, +// neither the local branch nor origin's remote-tracking ref exists — without +// this fetch, EnsureMetadataBranch would mint an unrelated empty orphan that +// hides existing checkpoints and rejects later fetches as non-fast-forward +// (issue #1374). Best-effort: on failure the caller falls back to orphan +// creation (the remote branch legitimately doesn't exist before the first +// push from any device). +func bootstrapMetadataFromCheckpointRemote(ctx context.Context) { + if !remote.Configured(ctx) { + return + } + checkpointURL, err := remote.FetchURL(ctx) + if err != nil { + logging.Warn(ctx, "checkpoint-remote: could not resolve fetch URL for metadata branch bootstrap", + slog.String("error", err.Error()), + ) + return + } + fetched, err := fetchMetadataBranchIfMissing(ctx, checkpointURL) + if err != nil { + logging.Warn(ctx, "checkpoint-remote: metadata branch bootstrap failed", + slog.String("error", err.Error()), + ) + return + } + if fetched { + fmt.Fprintf(os.Stderr, "✓ Created local branch '%s' from checkpoint remote\n", paths.MetadataBranchName) + } +} + // fetchMetadataBranchIfMissing fetches the primary metadata ref from a URL only if it doesn't exist locally. // This avoids network calls on every push — once the branch exists locally, this is a no-op. -// Fetch failures are silently swallowed (returns nil): the push will handle creating the -// branch on the remote. Only fatal errors (opening repo, creating local branch) are returned. -func fetchMetadataBranchIfMissing(ctx context.Context, remoteURL string) error { +// Returns true when the branch was actually fetched and created locally. +// Fetch failures are silently swallowed (returns nil error): the push will handle creating +// the branch on the remote. Only fatal errors (opening repo, creating local branch) are returned. +func fetchMetadataBranchIfMissing(ctx context.Context, remoteURL string) (bool, error) { repo, err := OpenRepository(ctx) if err != nil { - return fmt.Errorf("failed to open repository: %w", err) + return false, fmt.Errorf("failed to open repository: %w", err) } defer repo.Close() // Check if branch already exists locally - if so, nothing to do refs := checkpoint.ResolveCommittedRefs(ctx) if _, err := repo.Reference(refs.Primary, true); err == nil { - return nil // Branch exists locally, skip fetch + return false, nil // Branch exists locally, skip fetch } // Branch doesn't exist locally - try to fetch it from the URL. // Fetch failures are not fatal: push will create it on the remote when it succeeds. if err := FetchMetadataBranch(ctx, remoteURL); err != nil { - return nil + return false, nil } logging.Info(ctx, "checkpoint-remote: fetched metadata branch from URL") - return nil + return true, nil } diff --git a/cmd/entire/cli/strategy/checkpoint_remote_test.go b/cmd/entire/cli/strategy/checkpoint_remote_test.go index 8a55657ab..ce0f563db 100644 --- a/cmd/entire/cli/strategy/checkpoint_remote_test.go +++ b/cmd/entire/cli/strategy/checkpoint_remote_test.go @@ -166,7 +166,9 @@ func TestFetchBranchIfMissing_CreatesLocalFromRemote(t *testing.T) { assert.False(t, testutil.BranchExists(t, localDir, "entire/checkpoints/v1")) // Fetch using the remote dir as a URL (local path) - require.NoError(t, fetchMetadataBranchIfMissing(ctx, remoteDir)) + fetched, err := fetchMetadataBranchIfMissing(ctx, remoteDir) + require.NoError(t, err) + assert.True(t, fetched, "should report the branch was fetched") // Verify the branch now exists locally assert.True(t, testutil.BranchExists(t, localDir, "entire/checkpoints/v1")) @@ -220,7 +222,9 @@ func TestFetchBranchIfMissing_NoOpWhenBranchExistsLocally(t *testing.T) { // Should be a no-op since branch exists locally (no network call). // Use a nonexistent path — if it tried to fetch, it would fail. - require.NoError(t, fetchMetadataBranchIfMissing(ctx, "/nonexistent/repo.git")) + fetched, err := fetchMetadataBranchIfMissing(ctx, "/nonexistent/repo.git") + require.NoError(t, err) + assert.False(t, fetched, "should report no fetch happened") } // Not parallel: uses t.Chdir() @@ -243,8 +247,9 @@ func TestFetchBranchIfMissing_NoOpWhenBranchNotOnRemote(t *testing.T) { t.Chdir(localDir) - err := fetchMetadataBranchIfMissing(ctx, remoteDir) + fetched, err := fetchMetadataBranchIfMissing(ctx, remoteDir) require.NoError(t, err) + assert.False(t, fetched, "should report no fetch happened") // Branch should still not exist locally assert.False(t, testutil.BranchExists(t, localDir, "entire/checkpoints/v1")) diff --git a/cmd/entire/cli/strategy/common.go b/cmd/entire/cli/strategy/common.go index 12f29a571..d0e9b58bc 100644 --- a/cmd/entire/cli/strategy/common.go +++ b/cmd/entire/cli/strategy/common.go @@ -67,6 +67,14 @@ func EnsureSetup(ctx context.Context) error { return err } + // When checkpoints live in a separate checkpoint_remote repo, a fresh + // clone has neither the local metadata branch nor an origin + // remote-tracking ref. Fetch from the checkpoint remote before opening + // the repo handle below, so EnsureMetadataBranch doesn't mint an + // unrelated empty orphan (issue #1374) and the handle sees the fetched + // packfiles. + bootstrapMetadataFromCheckpointRemote(ctx) + // Ensure the entire/checkpoints/v1 orphan branch exists for permanent session storage repo, err := OpenRepository(ctx) if err != nil { @@ -453,6 +461,11 @@ func resolveAgentType(ctxAgentType types.AgentType, state *SessionState) types.A // If the remote-tracking branch (origin/entire/checkpoints/v1) exists and the local // branch is missing or empty, creates/updates the local branch from it. // Otherwise creates an empty orphan. +// +// Note: this function only consults local refs. When a checkpoint_remote is +// configured, EnsureSetup fetches the metadata branch from it first (see +// bootstrapMetadataFromCheckpointRemote) so this function doesn't mint an +// unrelated empty orphan on a fresh clone. func EnsureMetadataBranch(ctx context.Context, repo *git.Repository) error { refName := plumbing.NewBranchReferenceName(paths.MetadataBranchName) From c43fe2a2f54f14601613824ec3fc09e431cb29bf Mon Sep 17 00:00:00 2001 From: Alisha Kawaguchi Date: Fri, 5 Jun 2026 15:45:54 -0700 Subject: [PATCH 2/2] fix: let empty metadata orphan retry checkpoint-remote bootstrap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A first `entire enable` whose checkpoint-remote fetch fails (no token, network down, remote branch not pushed yet) falls back to minting an empty local orphan. fetchMetadataBranchIfMissing skipped the fetch whenever the local branch existed, so that orphan permanently closed the bootstrap window — a later enable with working auth (or after a teammate's first checkpoint push) never recovered. The skip-on-exists gate now exempts empty branches: a local metadata branch with no checkpoint data no longer blocks the bootstrap fetch. SafelyAdvanceLocalRef discards the no-op orphan commit during the promote, so the local branch lands exactly on the remote tip. Also log a warning when the bootstrap fetch fails instead of silently swallowing it (auth/network problems were invisible in .entire/logs), and fix the stale comment claiming promote errors are returned. Follow-up to aa5169fad9 (issue #1374). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../cli/integration_test/http_remote_test.go | 65 ++++++++ cmd/entire/cli/strategy/checkpoint_remote.go | 54 ++++-- .../cli/strategy/checkpoint_remote_test.go | 155 +++++++++++------- 3 files changed, 194 insertions(+), 80 deletions(-) diff --git a/cmd/entire/cli/integration_test/http_remote_test.go b/cmd/entire/cli/integration_test/http_remote_test.go index 232ebb8e4..82b443bb7 100644 --- a/cmd/entire/cli/integration_test/http_remote_test.go +++ b/cmd/entire/cli/integration_test/http_remote_test.go @@ -516,3 +516,68 @@ func TestHTTPS_EnableBootstrapsMetadataFromCheckpointRemote(t *testing.T) { t.Errorf("local metadata branch should contain checkpoint A summary at %s", summaryA) } } + +// TestHTTPS_ReEnableRecoversFromEmptyMetadataOrphan covers the recovery path +// after a bootstrap that found nothing on the checkpoint remote: device B +// enables before any checkpoint exists (the bootstrap fetch legitimately +// finds no branch, so EnsureSetup mints the empty orphan), device A then +// pushes checkpoint data, and a second `entire enable` on device B must +// replace the empty orphan with the fetched branch — not skip the fetch +// because a local branch already exists. +func TestHTTPS_ReEnableRecoversFromEmptyMetadataOrphan(t *testing.T) { + t.Parallel() + + srv := startGitHTTPSServer(t, "testorg/main-repo", "testorg/checkpoints") + env := NewFeatureBranchEnv(t) + + mainBare := srv.BareDirs["testorg/main-repo"] + checkpointBare := srv.BareDirs["testorg/checkpoints"] + httpsURL := srv.URL + "/testorg/main-repo.git" + seedBareRepo(t, env, mainBare, httpsURL) + + checkpointRemoteSettings := map[string]any{ + "strategy_options": map[string]any{ + "checkpoint_remote": map[string]any{ + "provider": "github", + "repo": "testorg/checkpoints", + }, + }, + } + + // Device B: enable while the checkpoint remote has no metadata branch yet. + // The bootstrap fetch finds nothing and the empty orphan is minted — the + // expected fallback for a brand-new checkpoint remote. + cloneB := cloneFromBareWithHTTPS(t, env, mainBare, httpsURL) + cloneB.ExtraEnv = srv.sslEnv() + cloneB.PatchSettings(checkpointRemoteSettings) + cloneB.RunCLI("enable", "--agent", "claude-code", "--telemetry=false") + if !cloneB.BranchExists(paths.MetadataBranchName) { + t.Fatal("precondition: enable should mint a local metadata branch") + } + + // Device A: create a checkpoint and push it to the checkpoint remote. + cloneA := cloneFromBareWithHTTPS(t, env, mainBare, httpsURL) + cloneA.ExtraEnv = srv.tokenEnv("clone-a-token") + cloneA.GitCheckoutNewBranch("feature/clone-a") + cloneA.PatchSettings(checkpointRemoteSettings) + checkpointA := createCheckpointedCommit(t, cloneA, "Work in clone A", "a.go", "package a", "Work from A") + cloneA.RunPrePush("origin") + if !cloneA.BranchExistsOnRemote(checkpointBare, paths.MetadataBranchName) { + t.Fatal("precondition: checkpoint branch should be on checkpoint remote after push") + } + + // Device B: re-running enable must recover — the empty orphan must not + // permanently block the checkpoint-remote bootstrap. + cloneB.RunCLI("enable", "--agent", "claude-code", "--telemetry=false") + + remoteTip := revParse(t, checkpointBare, "refs/heads/"+paths.MetadataBranchName) + localTip := revParse(t, cloneB.RepoDir, "refs/heads/"+paths.MetadataBranchName) + if localTip != remoteTip { + t.Errorf("local metadata branch tip = %s, want checkpoint remote tip %s (empty orphan blocked re-bootstrap)", localTip, remoteTip) + } + + summaryA := CheckpointSummaryPath(checkpointA) + if _, found := cloneB.ReadFileFromBranch(paths.MetadataBranchName, summaryA); !found { + t.Errorf("local metadata branch should contain checkpoint A summary at %s", summaryA) + } +} diff --git a/cmd/entire/cli/strategy/checkpoint_remote.go b/cmd/entire/cli/strategy/checkpoint_remote.go index d48ffed7d..e416d3d5b 100644 --- a/cmd/entire/cli/strategy/checkpoint_remote.go +++ b/cmd/entire/cli/strategy/checkpoint_remote.go @@ -84,10 +84,11 @@ func resolvePushSettings(ctx context.Context, pushRemoteName string) pushSetting ps.checkpointURL = checkpointURL - // If the v1 checkpoint branch doesn't exist locally, try to fetch it from the URL. - // This is a one-time operation — once the branch exists locally, subsequent pushes - // skip the fetch entirely. Only fetch the metadata branch; trails are always pushed - // to the user's push remote, not the checkpoint remote. + // If the v1 checkpoint branch doesn't exist locally (or is only the empty + // bootstrap orphan), try to fetch it from the URL. Once the branch has + // checkpoint data, subsequent pushes skip the fetch entirely. Only fetch + // the metadata branch; trails are always pushed to the user's push + // remote, not the checkpoint remote. if _, err := fetchMetadataBranchIfMissing(ctx, checkpointURL); err != nil { logging.Warn(ctx, "checkpoint-remote: failed to fetch metadata branch", slog.String("error", err.Error()), @@ -165,7 +166,8 @@ func fetchURLIntoTmpRef(ctx context.Context, remoteURL, srcRef, tmpRef, label st } // bootstrapMetadataFromCheckpointRemote populates the local metadata branch -// from the configured checkpoint_remote when it doesn't exist locally yet. +// from the configured checkpoint_remote when it doesn't exist locally yet, +// or exists only as the empty orphan minted by a previous failed bootstrap. // // On a fresh clone of a repo whose checkpoints live in a separate repo, // neither the local branch nor origin's remote-tracking ref exists — without @@ -197,11 +199,20 @@ func bootstrapMetadataFromCheckpointRemote(ctx context.Context) { } } -// fetchMetadataBranchIfMissing fetches the primary metadata ref from a URL only if it doesn't exist locally. -// This avoids network calls on every push — once the branch exists locally, this is a no-op. -// Returns true when the branch was actually fetched and created locally. -// Fetch failures are silently swallowed (returns nil error): the push will handle creating -// the branch on the remote. Only fatal errors (opening repo, creating local branch) are returned. +// fetchMetadataBranchIfMissing fetches the primary metadata ref from a URL +// only if it doesn't exist locally with real data. This avoids network calls +// on every push — once the branch has checkpoint data, this is a no-op. +// +// An empty bootstrap orphan does not count as existing: it means a previous +// bootstrap couldn't reach the checkpoint remote (no token, network down, +// branch not pushed yet) and EnsureSetup fell back to orphan creation — +// fetching again is the recovery path, and SafelyAdvanceLocalRef discards +// the no-op orphan commit during the promote. +// +// Returns true when the branch was actually fetched. Fetch failures are +// logged but swallowed (returns nil error): the remote branch legitimately +// doesn't exist before the first push from any device, and push will create +// it. Only failures to open the repository are returned. func fetchMetadataBranchIfMissing(ctx context.Context, remoteURL string) (bool, error) { repo, err := OpenRepository(ctx) if err != nil { @@ -209,15 +220,24 @@ func fetchMetadataBranchIfMissing(ctx context.Context, remoteURL string) (bool, } defer repo.Close() - // Check if branch already exists locally - if so, nothing to do + // Skip the network call when the branch already has checkpoint data. refs := checkpoint.ResolveCommittedRefs(ctx) - if _, err := repo.Reference(refs.Primary, true); err == nil { - return false, nil // Branch exists locally, skip fetch - } - - // Branch doesn't exist locally - try to fetch it from the URL. - // Fetch failures are not fatal: push will create it on the remote when it succeeds. + if ref, refErr := repo.Reference(refs.Primary, true); refErr == nil { + empty, emptyErr := isEmptyMetadataBranch(repo, ref) + if emptyErr != nil || !empty { + return false, nil // Branch has data (or is unreadable) — skip fetch + } + // Empty bootstrap orphan — fall through and try the fetch again. + } + + // Branch is missing (or an empty orphan) — try to fetch it from the URL. + // Not fatal on failure, but log it: a silent swallow here made auth and + // network problems invisible while EnsureSetup fell back to minting an + // empty orphan. if err := FetchMetadataBranch(ctx, remoteURL); err != nil { + logging.Warn(ctx, "checkpoint-remote: metadata branch fetch failed, continuing without it", + slog.String("error", err.Error()), + ) return false, nil } diff --git a/cmd/entire/cli/strategy/checkpoint_remote_test.go b/cmd/entire/cli/strategy/checkpoint_remote_test.go index ce0f563db..e5b402139 100644 --- a/cmd/entire/cli/strategy/checkpoint_remote_test.go +++ b/cmd/entire/cli/strategy/checkpoint_remote_test.go @@ -109,49 +109,59 @@ func TestIsURL(t *testing.T) { } } -// Not parallel: uses t.Chdir() -func TestFetchBranchIfMissing_CreatesLocalFromRemote(t *testing.T) { +// createMetadataBranchWithData creates an entire/checkpoints/v1 orphan branch +// in repoDir containing fileName, then switches back to the original branch. +func createMetadataBranchWithData(t *testing.T, repoDir, fileName, content string) { + t.Helper() ctx := context.Background() - // Set up a "remote" repo with a branch - remoteDir := t.TempDir() - testutil.InitRepo(t, remoteDir) - testutil.WriteFile(t, remoteDir, "f.txt", "init") - testutil.GitAdd(t, remoteDir, "f.txt") - testutil.GitCommit(t, remoteDir, "init") - // Get the default branch name before switching branchCmd := exec.CommandContext(ctx, "git", "rev-parse", "--abbrev-ref", "HEAD") - branchCmd.Dir = remoteDir + branchCmd.Dir = repoDir branchCmd.Env = testutil.GitIsolatedEnv() branchOut, err := branchCmd.Output() require.NoError(t, err) defaultBranch := strings.TrimSpace(string(branchOut)) - // Create an orphan branch in the remote repo (simulating entire/checkpoints/v1) - cmd := exec.CommandContext(ctx, "git", "checkout", "--orphan", "entire/checkpoints/v1") - cmd.Dir = remoteDir - cmd.Env = testutil.GitIsolatedEnv() - require.NoError(t, cmd.Run()) + runGit := func(args ...string) { + cmd := exec.CommandContext(ctx, "git", args...) + cmd.Dir = repoDir + cmd.Env = testutil.GitIsolatedEnv() + require.NoError(t, cmd.Run()) + } - cmd = exec.CommandContext(ctx, "git", "rm", "-rf", ".") - cmd.Dir = remoteDir - cmd.Env = testutil.GitIsolatedEnv() - require.NoError(t, cmd.Run()) + runGit("checkout", "--orphan", "entire/checkpoints/v1") + runGit("rm", "-rf", ".") - testutil.WriteFile(t, remoteDir, "metadata.json", `{"test": true}`) - testutil.GitAdd(t, remoteDir, "metadata.json") + testutil.WriteFile(t, repoDir, fileName, content) + testutil.GitAdd(t, repoDir, fileName) - cmd = exec.CommandContext(ctx, "git", "-c", "commit.gpgsign=false", "commit", "-m", "checkpoint data") - cmd.Dir = remoteDir - cmd.Env = testutil.GitIsolatedEnv() - require.NoError(t, cmd.Run()) + runGit("-c", "commit.gpgsign=false", "commit", "-m", "checkpoint data") + runGit("checkout", defaultBranch) +} - // Go back to the default branch - cmd = exec.CommandContext(ctx, "git", "checkout", defaultBranch) - cmd.Dir = remoteDir +// revParseRef resolves ref in repoDir to a commit hash. +func revParseRef(t *testing.T, repoDir, ref string) string { + t.Helper() + cmd := exec.CommandContext(context.Background(), "git", "rev-parse", ref) + cmd.Dir = repoDir cmd.Env = testutil.GitIsolatedEnv() - require.NoError(t, cmd.Run()) + out, err := cmd.Output() + require.NoError(t, err) + return strings.TrimSpace(string(out)) +} + +// Not parallel: uses t.Chdir() +func TestFetchBranchIfMissing_CreatesLocalFromRemote(t *testing.T) { + ctx := context.Background() + + // Set up a "remote" repo with a metadata branch containing data + remoteDir := t.TempDir() + testutil.InitRepo(t, remoteDir) + testutil.WriteFile(t, remoteDir, "f.txt", "init") + testutil.GitAdd(t, remoteDir, "f.txt") + testutil.GitCommit(t, remoteDir, "init") + createMetadataBranchWithData(t, remoteDir, "metadata.json", `{"test": true}`) // Set up local repo localDir := t.TempDir() @@ -178,45 +188,13 @@ func TestFetchBranchIfMissing_CreatesLocalFromRemote(t *testing.T) { func TestFetchBranchIfMissing_NoOpWhenBranchExistsLocally(t *testing.T) { ctx := context.Background() - // Set up local repo with the branch already existing + // Set up local repo with the branch already existing (with real data) localDir := t.TempDir() testutil.InitRepo(t, localDir) testutil.WriteFile(t, localDir, "f.txt", "init") testutil.GitAdd(t, localDir, "f.txt") testutil.GitCommit(t, localDir, "init") - - // Get the default branch name before switching - branchCmd := exec.CommandContext(ctx, "git", "rev-parse", "--abbrev-ref", "HEAD") - branchCmd.Dir = localDir - branchCmd.Env = testutil.GitIsolatedEnv() - branchOut, err := branchCmd.Output() - require.NoError(t, err) - defaultBranch := strings.TrimSpace(string(branchOut)) - - // Create the branch locally - cmd := exec.CommandContext(ctx, "git", "checkout", "--orphan", "entire/checkpoints/v1") - cmd.Dir = localDir - cmd.Env = testutil.GitIsolatedEnv() - require.NoError(t, cmd.Run()) - - cmd = exec.CommandContext(ctx, "git", "rm", "-rf", ".") - cmd.Dir = localDir - cmd.Env = testutil.GitIsolatedEnv() - require.NoError(t, cmd.Run()) - - testutil.WriteFile(t, localDir, "data.json", `{"local": true}`) - testutil.GitAdd(t, localDir, "data.json") - - cmd = exec.CommandContext(ctx, "git", "-c", "commit.gpgsign=false", "commit", "-m", "local checkpoint") - cmd.Dir = localDir - cmd.Env = testutil.GitIsolatedEnv() - require.NoError(t, cmd.Run()) - - // Switch back to the default branch - cmd = exec.CommandContext(ctx, "git", "checkout", defaultBranch) - cmd.Dir = localDir - cmd.Env = testutil.GitIsolatedEnv() - require.NoError(t, cmd.Run()) + createMetadataBranchWithData(t, localDir, "data.json", `{"local": true}`) t.Chdir(localDir) @@ -255,6 +233,57 @@ func TestFetchBranchIfMissing_NoOpWhenBranchNotOnRemote(t *testing.T) { assert.False(t, testutil.BranchExists(t, localDir, "entire/checkpoints/v1")) } +// Not parallel: uses t.Chdir() +// +// A first `entire enable` whose checkpoint-remote fetch fails (no token, +// network down, remote branch not pushed yet) falls back to minting an empty +// local orphan. That orphan must not permanently close the bootstrap window: +// a later attempt with the remote reachable must still fetch the real branch +// and point the local ref at it. +func TestFetchBranchIfMissing_RecoversFromEmptyLocalOrphan(t *testing.T) { + ctx := context.Background() + + // Set up a "remote" repo with a metadata branch containing data + remoteDir := t.TempDir() + testutil.InitRepo(t, remoteDir) + testutil.WriteFile(t, remoteDir, "f.txt", "init") + testutil.GitAdd(t, remoteDir, "f.txt") + testutil.GitCommit(t, remoteDir, "init") + createMetadataBranchWithData(t, remoteDir, "metadata.json", `{"test": true}`) + + // Set up local repo + localDir := t.TempDir() + testutil.InitRepo(t, localDir) + testutil.WriteFile(t, localDir, "f.txt", "init") + testutil.GitAdd(t, localDir, "f.txt") + testutil.GitCommit(t, localDir, "init") + + t.Chdir(localDir) + + // Simulate the failed first bootstrap: the fetch is a no-op against an + // unreachable remote, then EnsureSetup falls through to + // EnsureMetadataBranch, which mints the empty orphan. + fetched, err := fetchMetadataBranchIfMissing(ctx, "/nonexistent/repo.git") + require.NoError(t, err) + require.False(t, fetched) + + repo, err := OpenRepository(ctx) + require.NoError(t, err) + defer repo.Close() + require.NoError(t, EnsureMetadataBranch(ctx, repo)) + require.True(t, testutil.BranchExists(t, localDir, "entire/checkpoints/v1")) + + // Retry with the remote reachable (e.g. a second `entire enable` after + // fixing auth, or after a teammate's first checkpoint push). + fetched, err = fetchMetadataBranchIfMissing(ctx, remoteDir) + require.NoError(t, err) + assert.True(t, fetched, "empty local orphan must not block the checkpoint-remote bootstrap") + + remoteTip := revParseRef(t, remoteDir, "refs/heads/entire/checkpoints/v1") + localTip := revParseRef(t, localDir, "refs/heads/entire/checkpoints/v1") + assert.Equal(t, remoteTip, localTip, "local metadata branch should match the checkpoint remote tip") +} + // Not parallel: uses t.Chdir() func TestResolvePushSettings_NoConfig(t *testing.T) { tmpDir := t.TempDir()