# test(integration): add git-refs per-checkpoint ref divergence & recovery (D2, D3)

`c1a1af8`·  
  
Soph·1w ago·1 file·+296 added/-0 removed

Drive the git-refs recovery end-to-end through the real pre-push path (drain
queue -> batch push -> per-ref fetch+replay), complementing the unit-level
coverage in strategy/refs\_push\_test.go.

## D2:
- remote ref amended by another clone (valid child commit) + a divergent
local write of the same ref: the fast-forward-only batch push is rejected,
recovery fetches the remote tip and replays the local change on top
(non-force), so both sides' files survive and the remote commit stays an
ancestor.
- queue safety: when the remote refuses the ref update (bare pre-receive
declining the checkpoint namespace), the ref stays queued and the remote
is untouched (never force-overwritten); a later push retries and lands it.
A pure content-level cherry-pick conflict is not reproducible at this layer
(tree-delta apply is last-writer), so a server-side rejection stands in.

## D3: two clones race on the SAME per-checkpoint ref from a shared base -- the
second pusher fetch+replay+retries (non-force), preserving both. The git-refs
analogue of TestConcurrentPush\_SecondPusherRebasesAndRetries (v1-only).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>  
Claude-Session: https://claude.ai/code/session\_012yi3hHGAGepwfrfjPETjGq

## Changes

1

- cmd/entire/cli/integration\_test

- Arefs\_divergence\_test.go+296

```go
//go:build integration

package integration

import (
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"testing"

"github.com/entireio/cli/cmd/entire/cli/testutil"
	"github.com/stretchr/testify/require"
)

// =============================================================================
// D2 / D3 -- git-refs per-checkpoint ref divergence & recovery (integration).
//
// The unit-level recovery is covered in strategy/refs_push_test.go
// (TestBatchPushRefs_RejectsNonFastForward, TestPushCheckpointRefWithRecovery_// MergesDivergedRef). These drive the same behaviour end-to-end through the real
// pre-push path (drain the push queue -> batch push -> per-ref fetch+replay
// recovery), where the checkpoint ref really lives on a bare remote.
// =============================================================================

// checkpointIdentityEnv is a git env with a committer/author identity, needed by
// commit-tree in a bare remote (which has no repo-local user config).
func checkpointIdentityEnv() []string {
	return append(testutil.GitIsolatedEnv(),
		"GIT_AUTHOR_NAME=Test", "GIT_AUTHOR_EMAIL=test@example.com",
		"GIT_COMMITTER_NAME=Test", "GIT_COMMITTER_EMAIL=test@example.com",
	)
}

// amendCheckpointRef advances the checkpoint ref in the repo at dir to a new
// child commit of base that adds fileName. It builds the commit with plumbing +
// a temporary index so no working tree is required (works in a bare remote too),
// then updates the ref. Returns the new commit hash. This models another clone
// (or the bare's own state) advancing a per-checkpoint ref.
func amendCheckpointRef(t *testing.T, dir, ref, base, fileName, content string) string {
	t.Helper()

idxPath := filepath.Join(t.TempDir(), "amend-index")
	run := func(stdin string, args ...string) string {
		c := exec.CommandContext(t.Context(), "git", args...)
		c.Dir = dir
		c.Env = append(checkpointIdentityEnv(), "GIT_INDEX_FILE="+idxPath)
		if stdin != "" {
			c.Stdin = strings.NewReader(stdin)
		}
		out, err := c.CombinedOutput()
		require.NoError(t, err, "git %v in %s failed: %s", args, dir, out)
		return strings.TrimSpace(string(out))
	}

run("", "read-tree", base)
	blobSHA := run(content, "hash-object", "-w", "--stdin")
	run("", "update-index", "--add", "--cacheinfo", "100644,"+blobSHA+","+fileName)
	treeSHA := run("", "write-tree")
	commitSHA := run("", "commit-tree", treeSHA, "-p", base, "-m", "amend "+fileName)
	run("", "update-ref", ref, commitSHA)
	return commitSHA
}

// enqueueCheckpointRef appends ref to the git-refs push queue in the repo's git
// common dir, so the next pre-push drains and pushes it. Mirrors what a checkpoint
// write does internally.
func enqueueCheckpointRef(t *testing.T, env *TestEnv, ref string) {
	t.Helper()

commonDir := gitCommonDir(t, env.RepoDir)
	queuePath := filepath.Join(commonDir, "entire-checkpoint-push-queue.jsonl")
	f, err := os.OpenFile(queuePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o600) //nolint:gosec // test-controlled path
	require.NoError(t, err)
	defer f.Close()
	_, err = f.WriteString(`{"ref":"` + ref + `"}` + "\n")
	require.NoError(t, err)
}

// gitCommonDir resolves the git common dir for a repo/worktree dir.
func gitCommonDir(t *testing.T, dir string) string {
	t.Helper()
	c := exec.CommandContext(t.Context(), "git", "rev-parse", "--git-common-dir")
	c.Dir = dir
	c.Env = testutil.GitIsolatedEnv()
	out, err := c.Output()
	require.NoError(t, err, "resolve git common dir")
	common := strings.TrimSpace(string(out))
	if !filepath.IsAbs(common) {
		common = filepath.Join(dir, common)
	}
	return common
}

// refHashIn returns the commit hash a ref points at in the repo at dir.
func refHashIn(t *testing.T, dir, ref string) string {
	t.Helper()
	c := exec.CommandContext(t.Context(), "git", "rev-parse", ref)
	c.Dir = dir
	c.Env = testutil.GitIsolatedEnv()
	out, err := c.Output()
	require.NoError(t, err, "rev-parse %s in %s", ref, dir)
	return strings.TrimSpace(string(out))
}

// refTreeFilesIn lists the files in the tree a ref points at in the repo at dir.
func refTreeFilesIn(t *testing.T, dir, ref string) string {
	t.Helper()
	c := exec.CommandContext(t.Context(), "git", "ls-tree", "-r", "--name-only", ref)
	c.Dir = dir
	c.Env = testutil.GitIsolatedEnv()
	out, err := c.CombinedOutput()
	require.NoError(t, err, "ls-tree %s in %s: %s", ref, dir, out)
	return string(out)
}

// isAncestorIn reports whether ancestor is an ancestor of descendant in dir.
func isAncestorIn(t *testing.T, dir, ancestor, descendant string) bool {
	t.Helper()
	c := exec.CommandContext(t.Context(), "git", "merge-base", "--is-ancestor", ancestor, descendant)
	c.Dir = dir
	c.Env = testutil.GitIsolatedEnv()
	return c.Run() == nil
}

// fetchCheckpointRefLocally fetches a single checkpoint ref from origin into the
// same ref name locally (so a clone can then amend and diverge it).
func fetchCheckpointRefLocally(t *testing.T, env *TestEnv, ref string) {
	t.Helper()
	c := exec.CommandContext(t.Context(), "git", "fetch", "--no-tags", "origin", ref+":"+ref)
	c.Dir = env.RepoDir
	c.Env = testutil.GitIsolatedEnv()
	out, err := c.CombinedOutput()
	require.NoError(t, err, "fetch checkpoint ref %s: %s", ref, out)
}

// TestGitRefsDivergence_RemoteAheadRefFetchReplaysNonForce is D2's primary case:
// the remote checkpoint ref was amended by another clone (a valid child commit).
// A local divergent write of the SAME ref then pushes through the real pre-push
// path: the fast-forward-only batch push is rejected, recovery fetches the remote
// tip and replays the local-only change on top (non-force), so BOTH sides' files
// survive and the remote's commit is preserved as an ancestor.
func TestGitRefsDivergence_RemoteAheadRefFetchReplaysNonForce(t *testing.T) {
	t.Parallel()

env := NewFeatureBranchEnv(t)
	env.CheckpointStore = StoreGitRefs
	bareDir := env.SetupBareRemote()

cp := createCheckpointedCommit(t, env, "Add module", "mod.go", "package mod", "Add module")
	require.NotEmpty(t, cp)
	ref := checkpointRefName(cp)

// Push the checkpoint ref to the remote (remote ref = C0).
	env.RunPrePush("origin")
	require.True(t, env.CheckpointExistsOnRemote(bareDir, cp))
	c0 := refHashIn(t, bareDir, ref)

// Another clone amends the same checkpoint on the remote: remote ref -> C_remote
	// (a child of C0 that adds remote_side.txt).
	cRemote := amendCheckpointRef(t, bareDir, ref, c0, "remote_side.txt", "remote change")

// Locally, diverge the same ref to a sibling child of C0 (adds local_side.txt)
	// and enqueue it for the next push.
	amendCheckpointRef(t, env.RepoDir, ref, c0, "local_side.txt", "local change")
	enqueueCheckpointRef(t, env, ref)

// Real pre-push path: batch push is non-fast-forward -> recovery fetch+replay.
	env.RunPrePush("origin")

// Both sides' files survive on the remote ref.
	files := refTreeFilesIn(t, bareDir, ref)
	require.Contains(t, files, "remote_side.txt", "remote-only change must be preserved (non-force)")
	require.Contains(t, files, "local_side.txt", "local-only change must be replayed on top")

// Non-force: the remote's amended commit remains an ancestor of the new tip.
	newRemote := refHashIn(t, bareDir, ref)
	require.True(t, isAncestorIn(t, bareDir, cRemote, newRemote),
		"the remote's amended commit must be preserved as an ancestor, never overwritten")

// The queue was drained by the successful recovery push.
	require.Empty(t, env.PushQueueRefs(), "push queue should be drained after the recovery push landed")
}

// TestGitRefsDivergence_RejectedRefStaysQueuedRemoteUntouched is D2's queue-safety
// case: when the remote refuses the checkpoint ref update, the ref is left queued
// and the remote ref is untouched (never force-overwritten); a later push, once
// the remote accepts writes again, lands it.
//
// A pure content-level cherry-pick conflict is not reproducible at this layer
// (the tree-delta apply is last-writer, not a 3-way merge), so a server-side
// rejection (a bare pre-receive hook that declines the checkpoint namespace)
// stands in for "the ref could not land" — exercising the same "stays queued,
// remote untouched, retry next time" property.
func TestGitRefsDivergence_RejectedRefStaysQueuedRemoteUntouched(t *testing.T) {
	t.Parallel()

env := NewFeatureBranchEnv(t)
	env.CheckpointStore = StoreGitRefs
	bareDir := env.SetupBareRemote()

cp := createCheckpointedCommit(t, env, "Add module", "mod.go", "package mod", "Add module")
	require.NotEmpty(t, cp)
	ref := checkpointRefName(cp)

env.RunPrePush("origin")
	require.True(t, env.CheckpointExistsOnRemote(bareDir, cp))
	remoteBefore := refHashIn(t, bareDir, ref)

// Reject any update to the checkpoint ref namespace on the remote.
	installRejectCheckpointRefsHook(t, bareDir)

// Advance the local ref (a fast-forward child) and enqueue it.
	amendCheckpointRef(t, env.RepoDir, ref, remoteBefore, "local_side.txt", "local change")
	enqueueCheckpointRef(t, env, ref)

// Pre-push: the push is rejected, recovery cannot make it land -> the ref is
	// left queued and the remote ref is unchanged (never force-overwritten).
	env.RunPrePush("origin")
	require.Contains(t, env.PushQueueRefs(), ref, "a rejected ref must stay queued for a later push")
	require.Equal(t, remoteBefore, refHashIn(t, bareDir, ref),
		"a rejected push must leave the remote ref untouched (no force overwrite)")

// Once the remote accepts writes again, the next pre-push retries and lands it.
	removeRejectCheckpointRefsHook(t, bareDir)
	env.RunPrePush("origin")
	require.Contains(t, refTreeFilesIn(t, bareDir, ref), "local_side.txt",
		"the queued ref must land on the next push once the remote accepts it")
	require.Empty(t, env.PushQueueRefs(), "queue should drain after the retry succeeds")
}

// installRejectCheckpointRefsHook writes a pre-receive hook on the bare remote
// that declines any update to refs/entire/checkpoints/*.
func installRejectCheckpointRefsHook(t *testing.T, bareDir string) {
	t.Helper()
	hooksDir := filepath.Join(bareDir, "hooks")
	require.NoError(t, os.MkdirAll(hooksDir, 0o755))
	script := "#!/bin/sh\n" +
		"while read _old _new ref; do\n" +
		"  case \"$ref\" in\n" +
		"    refs/entire/checkpoints/*) echo 'rejected: checkpoint refs are protected' >&2; exit 1;;\n" +
		"  esac\n" +
		"done\n" +
		"exit 0\n"
	require.NoError(t, os.WriteFile(filepath.Join(hooksDir, "pre-receive"), []byte(script), 0o755)) //nolint:gosec // hook must be executable
}

func removeRejectCheckpointRefsHook(t *testing.T, bareDir string) {
	t.Helper()
	require.NoError(t, os.Remove(filepath.Join(bareDir, "hooks", "pre-receive")))
}

// TestGitRefsConcurrentPush_SecondPusherReplaysAndRetries is D3: two clones race
// on the SAME per-checkpoint ref (each amends it from a shared base). The first
// push fast-forwards; the second is non-fast-forward and must fetch+replay+retry
// (non-force), so both clones' changes survive. This is the git-refs analogue of
// TestConcurrentPush_SecondPusherRebasesAndRetries (v1-only).
func TestGitRefsConcurrentPush_SecondPusherReplaysAndRetries(t *testing.T) {
	t.Parallel()

env := NewFeatureBranchEnv(t)
	env.CheckpointStore = StoreGitRefs
	bareDir := env.SetupBareRemote()

cp := createCheckpointedCommit(t, env, "Add shared module", "shared.go", "package shared", "Add shared module")
	require.NotEmpty(t, cp)
	ref := checkpointRefName(cp)
	env.RunPrePush("origin")
c0 := refHashIn(t, bareDir, ref)

// Two clones both fetch the shared checkpoint ref at C0 (before either pushes).
	cloneA := env.CloneFrom(bareDir)
	cloneA.CheckpointStore = StoreGitRefs
	fetchCheckpointRefLocally(t, cloneA, ref)

cloneB := env.CloneFrom(bareDir)
	cloneB.CheckpointStore = StoreGitRefs
	fetchCheckpointRefLocally(t, cloneB, ref)

// Each clone amends the same ref differently and enqueues it.
	amendCheckpointRef(t, cloneA.RepoDir, ref, c0, "a_side.txt", "A change")
	enqueueCheckpointRef(t, cloneA, ref)
	amendCheckpointRef(t, cloneB.RepoDir, ref, c0, "b_side.txt", "B change")
	enqueueCheckpointRef(t, cloneB, ref)

// A pushes first (fast-forward over C0).
	cloneA.RunPrePush("origin")
	require.Contains(t, refTreeFilesIn(t, bareDir, ref), "a_side.txt")

// B pushes second: non-fast-forward -> fetch+replay+retry, preserving both.
	cloneB.RunPrePush("origin")
	files := refTreeFilesIn(t, bareDir, ref)
	require.Contains(t, files, "a_side.txt", "clone A's change must be preserved (not overwritten)")
	require.Contains(t, files, "b_side.txt", "clone B's change must be replayed on top")
	require.Empty(t, cloneB.PushQueueRefs(), "clone B's queue should drain after the recovery push")
}
```
