Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,7 @@ The manual-commit strategy (`manual_commit*.go`) does not modify the active bran
- **Worktree-specific branches** - each git worktree gets its own shadow branch namespace, preventing conflicts
- **Supports multiple concurrent sessions** - checkpoints from different sessions in the same directory interleave on the same shadow branch
- Condenses session logs to permanent `entire/checkpoints/v1` branch on user commits
- When `checkpoints_version` is `1.1`, mirrors v1 metadata to the local-only `refs/entire/checkpoints/v1.1` read ref after active v1 write/fetch paths; read paths use that ref as-is
- Uses the `post-rewrite` Git hook to keep local session linkage aligned after amend/rebase rewrites
- Builds git trees in-memory using go-git plumbing APIs
- Rewind restores files from shadow branch commit tree (does not use `git reset`)
Expand Down
2 changes: 1 addition & 1 deletion cmd/entire/cli/attach.go
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,7 @@ func runAttach(ctx context.Context, w io.Writer, sessionID string, agentName typ
}

if refs := opts.committedRefs(ctx); refs.HasMirror() {
if err := mirrorToV1CustomRef(refs, repo); err != nil {
if err := strategy.MirrorCommittedMetadataRef(ctx, repo, refs); err != nil {
return fmt.Errorf("checkpoint was written to %s, but failed to mirror to %s: %w", refs.Primary, refs.Mirror, err)
}
}
Expand Down
106 changes: 0 additions & 106 deletions cmd/entire/cli/checkpoint/committed_read_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,117 +2,11 @@ package checkpoint

import (
"context"
"errors"
"log/slog"

"github.com/go-git/go-git/v6"
"github.com/go-git/go-git/v6/plumbing"

"github.com/entireio/cli/cmd/entire/cli/logging"
)

// NewCommittedReadStore returns a GitStore bound to the topology's read ref.
func NewCommittedReadStore(ctx context.Context, repo *git.Repository) *GitStore {
return NewGitStoreWithRef(repo, ResolveCommittedRefs(ctx).Read)
}

// SyncCommittedReadRef advances the mirror ref to the primary tip before a read
// so a git pull (which updates the primary but not the local-only mirror) is
// reflected. No-op without a mirror. Best-effort.
func SyncCommittedReadRef(ctx context.Context, repo *git.Repository) {
refs := ResolveCommittedRefs(ctx)
if !refs.HasMirror() {
return
}
syncMirrorForRead(ctx, repo, refs)
}

// syncMirrorForRead advances the mirror ref to the primary tip (local primary
// ref, or origin's on a fresh clone): seed when missing, advance when an
// ancestor, no-op when equal, leave a diverged ref as-is. Failures are logged.
func syncMirrorForRead(ctx context.Context, repo *git.Repository, refs CommittedRefs) {
primaryHash, ok := resolvePrimaryTip(repo, refs.Primary)
if !ok {
logging.Debug(ctx, "mirror read sync skipped: no primary tip available")
return
}

mirrorRefName := refs.Mirror
mirrorRef, err := repo.Reference(mirrorRefName, false)
if errors.Is(err, plumbing.ErrReferenceNotFound) {
setMirrorRef(ctx, repo, mirrorRefName, primaryHash) // missing — seed at primary tip
return
}
if err != nil {
// Unexpected read error — don't overwrite the ref; read it as-is.
logging.Warn(ctx, "mirror read sync skipped: mirror ref unreadable",
slog.String("ref", mirrorRefName.String()),
slog.String("error", err.Error()))
return
}

if mirrorRef.Hash() == primaryHash {
return // already current
}

mirrorCommit, err := repo.CommitObject(mirrorRef.Hash())
if err != nil {
logging.Warn(ctx, "mirror read sync skipped: mirror ref commit unreadable",
slog.String("ref", mirrorRefName.String()),
slog.String("error", err.Error()))
return
}
primaryCommit, err := repo.CommitObject(primaryHash)
if err != nil {
logging.Warn(ctx, "mirror read sync skipped: primary commit unreadable",
slog.String("error", err.Error()))
return
}

isAncestor, err := mirrorCommit.IsAncestor(primaryCommit)
if err != nil {
logging.Warn(ctx, "mirror read sync skipped: ancestry check failed",
slog.String("error", err.Error()))
return
}
if !isAncestor {
// Diverged from the primary: leave the ref untouched and read it as-is.
logging.Warn(ctx, "mirror ref diverged from primary; reading mirror ref as-is",
slog.String("ref", mirrorRefName.String()),
slog.String("mirror_hash", mirrorRef.Hash().String()),
slog.String("primary_hash", primaryHash.String()))
return
}

setMirrorRef(ctx, repo, mirrorRefName, primaryHash)
}

// resolvePrimaryTip returns the primary metadata tip, preferring the local
// primary ref and falling back to origin's remote-tracking branch (so the
// mirror can seed on a fresh clone).
func resolvePrimaryTip(repo *git.Repository, primary plumbing.ReferenceName) (plumbing.Hash, bool) {
if ref, err := repo.Reference(primary, true); err == nil {
return ref.Hash(), true
}
if primary.IsBranch() {
tracking := plumbing.NewRemoteReferenceName("origin", primary.Short())
if ref, err := repo.Reference(tracking, true); err == nil {
return ref.Hash(), true
}
}
return plumbing.ZeroHash, false
}

// setMirrorRef points refName at hash; failures are logged and swallowed so the
// read can proceed against the ref as-is.
func setMirrorRef(ctx context.Context, repo *git.Repository, refName plumbing.ReferenceName, hash plumbing.Hash) {
if err := repo.Storer.SetReference(plumbing.NewHashReference(refName, hash)); err != nil {
logging.Warn(ctx, "mirror read sync failed to advance mirror ref",
slog.String("ref", refName.String()),
slog.String("error", err.Error()))
return
}
logging.Debug(ctx, "mirror ref synced for read",
slog.String("ref", refName.String()),
slog.String("hash", hash.String()))
}
104 changes: 25 additions & 79 deletions cmd/entire/cli/checkpoint/committed_read_store_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,79 +96,12 @@ func writeSettings(t *testing.T, dir, version string) {
require.NoError(t, os.WriteFile(filepath.Join(dir, ".entire", paths.SettingsFileName), []byte(body), 0o644))
}

// blockCustomRefWrite occupies refs/entire with a file so refs/entire/* writes fail.
func blockCustomRefWrite(t *testing.T, dir string) {
t.Helper()
require.NoError(t, os.WriteFile(filepath.Join(dir, ".git", "refs", "entire"), []byte("blocked"), 0o644))
}

func TestGitStore_CommittedReadRef(t *testing.T) {
t.Parallel()
assert.Equal(t, v1BranchRef(), NewGitStore(nil).CommittedReadRef())
assert.Equal(t, customRef(), NewGitStoreWithRef(nil, customRef()).CommittedReadRef())
}

func TestSyncMirrorForRead(t *testing.T) {
t.Parallel()
tests := []struct {
name string
setup func(t *testing.T, dir string, repo *git.Repository, init plumbing.Hash) (want plumbing.Hash, exists bool)
}{
{"seeds from local v1 when missing", func(t *testing.T, _ string, repo *git.Repository, init plumbing.Hash) (plumbing.Hash, bool) {
setRef(t, repo, v1BranchRef(), init)
return init, true
}},
{"seeds from origin when local v1 missing", func(t *testing.T, _ string, repo *git.Repository, init plumbing.Hash) (plumbing.Hash, bool) {
setRef(t, repo, originV1Ref(), init)
return init, true
}},
{"no-op when equal", func(t *testing.T, _ string, repo *git.Repository, init plumbing.Hash) (plumbing.Hash, bool) {
setRef(t, repo, v1BranchRef(), init)
setRef(t, repo, customRef(), init)
return init, true
}},
{"advances when ancestor", func(t *testing.T, dir string, repo *git.Repository, init plumbing.Hash) (plumbing.Hash, bool) {
setRef(t, repo, customRef(), init)
newHash := commitFile(t, repo, dir, "f2.txt", "more", "second")
setRef(t, repo, v1BranchRef(), newHash)
return newHash, true
}},
{"leaves non-ancestor ref", func(t *testing.T, dir string, repo *git.Repository, init plumbing.Hash) (plumbing.Hash, bool) {
ahead := commitFile(t, repo, dir, "f2.txt", "more", "second")
setRef(t, repo, v1BranchRef(), init) // parent
setRef(t, repo, customRef(), ahead) // child, not an ancestor of v1
return ahead, true
}},
{"no v1 tip", func(_ *testing.T, _ string, _ *git.Repository, _ plumbing.Hash) (plumbing.Hash, bool) {
return plumbing.ZeroHash, false
}},
{"write failure leaves ref unset", func(t *testing.T, dir string, repo *git.Repository, init plumbing.Hash) (plumbing.Hash, bool) {
setRef(t, repo, v1BranchRef(), init)
blockCustomRefWrite(t, dir)
return plumbing.ZeroHash, false
}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
dir, repo, init := newTestRepo(t)
want, exists := tt.setup(t, dir, repo, init)

syncMirrorForRead(context.Background(), repo, CommittedRefs{
Primary: v1BranchRef(),
Read: customRef(),
Mirror: customRef(),
})

got, ok := customRefHash(t, repo)
require.Equal(t, exists, ok)
if exists {
assert.Equal(t, want, got)
}
})
}
}

// Not parallel: uses t.Chdir() so settings.Load resolves the test repo.
func TestNewCommittedReadStore_SelectsRefByVersion(t *testing.T) {
dir, repo, h := newTestRepo(t)
Expand All @@ -182,27 +115,35 @@ func TestNewCommittedReadStore_SelectsRefByVersion(t *testing.T) {
assert.Equal(t, customRef(), NewCommittedReadStore(context.Background(), repo).CommittedReadRef())
}

// v1.1 reads always go through the custom ref (no v1 fallback): a checkpoint is
// found when the ref can be synced to v1, and not found when it can't.
// v1.1 reads always go through the custom ref as-is (no v1 fallback, and no
// read-time seeding from v1).
// Not parallel: subtests use t.Chdir().
func TestNewCommittedReadStore_V11Reads(t *testing.T) {
tests := []struct {
name string
mutate func(t *testing.T, dir string, repo *git.Repository)
mutate func(t *testing.T, dir string, repo *git.Repository) (wantCustomHash plumbing.Hash, wantCustomExists bool)
wantFound bool
}{
{"reads v1 data via custom ref", func(_ *testing.T, _ string, _ *git.Repository) {}, true},
{"reads remote-only metadata", func(t *testing.T, _ string, repo *git.Repository) {
{"reads metadata when custom ref points at v1", func(t *testing.T, _ string, repo *git.Repository) (plumbing.Hash, bool) {
ref, err := repo.Reference(v1BranchRef(), true)
require.NoError(t, err)
setRef(t, repo, customRef(), ref.Hash())
return ref.Hash(), true
}, true},
{"does not seed missing custom ref from local v1", func(_ *testing.T, _ string, _ *git.Repository) (plumbing.Hash, bool) {
return plumbing.ZeroHash, false
}, false},
{"does not seed missing custom ref from origin v1", func(t *testing.T, _ string, repo *git.Repository) (plumbing.Hash, bool) {
ref, err := repo.Reference(v1BranchRef(), true)
require.NoError(t, err)
setRef(t, repo, originV1Ref(), ref.Hash())
require.NoError(t, repo.Storer.RemoveReference(v1BranchRef()))
}, true},
{"sync write fails", func(t *testing.T, dir string, _ *git.Repository) {
blockCustomRefWrite(t, dir)
return plumbing.ZeroHash, false
}, false},
{"custom ref diverges", func(t *testing.T, dir string, repo *git.Repository) {
setRef(t, repo, customRef(), commitFile(t, repo, dir, "other.txt", "diverged", "diverged"))
{"reads custom ref as-is when it differs from v1", func(t *testing.T, dir string, repo *git.Repository) (plumbing.Hash, bool) {
hash := commitFile(t, repo, dir, "other.txt", "diverged", "diverged")
setRef(t, repo, customRef(), hash)
return hash, true
}, false},
}
for _, tt := range tests {
Expand All @@ -211,9 +152,8 @@ func TestNewCommittedReadStore_V11Reads(t *testing.T) {
enableV11(t, dir)
cpID := id.MustCheckpointID("a1b2c3d4e5f6")
writeV1Checkpoint(t, repo, cpID)
tt.mutate(t, dir, repo)
wantCustomHash, wantCustomExists := tt.mutate(t, dir, repo)

SyncCommittedReadRef(context.Background(), repo)
store := NewCommittedReadStore(context.Background(), repo)
require.Equal(t, customRef(), store.CommittedReadRef(), "must read the custom ref, not fall back to v1")

Expand All @@ -225,6 +165,12 @@ func TestNewCommittedReadStore_V11Reads(t *testing.T) {
} else {
assert.Nil(t, summary, "must not fall back to v1")
}

gotCustomHash, gotCustomExists := customRefHash(t, repo)
require.Equal(t, wantCustomExists, gotCustomExists)
if wantCustomExists {
assert.Equal(t, wantCustomHash, gotCustomHash)
}
})
}
}
2 changes: 1 addition & 1 deletion cmd/entire/cli/checkpoint/committed_refs.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import (
type CommittedRefs struct {
Primary plumbing.ReferenceName // source of truth: written first, pushed/fetched
Read plumbing.ReferenceName // committed reads resolve against this
Mirror plumbing.ReferenceName // advanced to Primary after writes; empty = none (local-only, never pushed)
Mirror plumbing.ReferenceName // advanced to Primary after v1 writes/fetches; empty = none (local-only, never pushed)
}

// HasMirror reports whether a mirror ref is configured.
Expand Down
4 changes: 2 additions & 2 deletions cmd/entire/cli/checkpoint/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ type GitStore struct {
//
// Writes intentionally do NOT use this ref: committed writes always target
// the v1 branch (the durable source of truth) and are mirrored to the v1.1
// custom ref separately by the strategy's write-time mirror. Pointing writes
// here would let a v1.1 read store write ahead of v1 and diverge from it.
// custom ref separately by the strategy mirror paths. Pointing writes here
// would let a v1.1 read store write ahead of v1 and diverge from it.
committedReadRef plumbing.ReferenceName
}

Expand Down
1 change: 0 additions & 1 deletion cmd/entire/cli/dispatch/mode_local.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,6 @@ func enumerateRepoCandidates(ctx context.Context, repoRoot string, opts Options,
// the cwd may not be a repo at all, so scope settings resolution to this
// repo before consulting the topology.
repoCtx := settings.WithWorktreeRoot(ctx, repoRoot)
checkpoint.SyncCommittedReadRef(repoCtx, repo)
store := checkpoint.NewCommittedReadStore(repoCtx, repo)
infos, err := store.ListCommitted(ctx)
if err != nil {
Expand Down
5 changes: 1 addition & 4 deletions cmd/entire/cli/explain.go
Original file line number Diff line number Diff line change
Expand Up @@ -684,7 +684,6 @@ func runExplainCheckpointWithLookup(ctx context.Context, w, errW io.Writer, chec
}
// Reload to get the updated summary.
stopLoad = startSpinner(errW, fmt.Sprintf("Reloading checkpoint %s", fullCheckpointID))
checkpoint.SyncCommittedReadRef(ctx, lookup.repo)
lookup.store = checkpoint.NewCommittedReadStore(ctx, lookup.repo)
lookup.store.SetBlobFetcher(FetchBlobsByHash)
content, err = checkpoint.ReadLatestSessionContent(ctx, lookup.store, fullCheckpointID, summary)
Expand Down Expand Up @@ -849,7 +848,6 @@ func newExplainCheckpointLookup(ctx context.Context) (*explainCheckpointLookup,
// `git fetch` fails against partial-clone repos with "did not send all
// necessary objects"). Falls back to a full metadata-branch fetch if
// fetch-pack also can't reach the blobs.
checkpoint.SyncCommittedReadRef(ctx, repo)
store := checkpoint.NewCommittedReadStore(ctx, repo)
store.SetBlobFetcher(FetchBlobsByHash)

Expand Down Expand Up @@ -926,7 +924,7 @@ func generateCheckpointSummary(ctx context.Context, w, errW io.Writer, store *ch
}

if refs := checkpoint.ResolveCommittedRefs(ctx); refs.HasMirror() {
if err := mirrorToV1CustomRef(refs, store.Repository()); err != nil {
if err := strategy.MirrorCommittedMetadataRef(ctx, store.Repository(), refs); err != nil {
return fmt.Errorf("summary was written to %s, but failed to mirror to %s: %w", refs.Primary, refs.Mirror, err)
}
}
Expand Down Expand Up @@ -2004,7 +2002,6 @@ func getBranchCheckpoints(ctx context.Context, repo *git.Repository, limit int)
// Warn (once per process) if metadata branches are disconnected
strategy.WarnIfMetadataDisconnected()

checkpoint.SyncCommittedReadRef(ctx, repo)
store := checkpoint.NewCommittedReadStore(ctx, repo)

// Get all committed checkpoints for lookup.
Expand Down
3 changes: 2 additions & 1 deletion cmd/entire/cli/explain_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2310,6 +2310,7 @@ func TestRunExplainCheckpoint_GenerateV11ReloadsAfterV1Write(t *testing.T) {
AuthorName: "Test",
AuthorEmail: "test@example.com",
}))
require.NoError(t, strategy.MirrorCommittedMetadataRef(ctx, repo, checkpoint.ResolveCommittedRefs(ctx)))

var buf, errBuf bytes.Buffer
err = runExplainCheckpoint(ctx, &buf, &errBuf, "bbccdd", false, false, false, false, true, true, false, 0)
Expand All @@ -2325,7 +2326,7 @@ func TestRunExplainCheckpoint_GenerateV11ReloadsAfterV1Write(t *testing.T) {
require.NoError(t, err)
customRef, err := repo.Reference(plumbing.ReferenceName(paths.MetadataRefName), true)
require.NoError(t, err)
require.Equal(t, v1Ref.Hash(), customRef.Hash(), "reload should resync v1.1 to the v1 write")
require.Equal(t, v1Ref.Hash(), customRef.Hash(), "summary generation should mirror the v1 write to v1.1")
}

func TestRunExplainCheckpoint_DefaultViewUsesV1Transcript(t *testing.T) {
Expand Down
11 changes: 8 additions & 3 deletions cmd/entire/cli/git_operations.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ import (
"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/strategy"

"github.com/go-git/go-git/v6"
Expand Down Expand Up @@ -428,7 +428,11 @@ type fetchMetadataOpts struct {
}

func fetchMetadataFromOrigin(ctx context.Context, fopts fetchMetadataOpts) error {
branchName := paths.MetadataBranchName
refs := checkpoint.ResolveCommittedRefs(ctx)
if !refs.Primary.IsBranch() {
return fmt.Errorf("primary metadata ref %s is not a branch", refs.Primary)
}
branchName := refs.Primary.Short()

ctx, cancel := context.WithTimeout(ctx, 2*time.Minute)
defer cancel()
Expand Down Expand Up @@ -465,9 +469,10 @@ func fetchMetadataFromOrigin(ctx context.Context, fopts fetchMetadataOpts) error
if err != nil {
return fmt.Errorf("branch '%s' not found on origin: %w", branchName, err)
}
if err := strategy.SafelyAdvanceLocalRef(ctx, repo, plumbing.NewBranchReferenceName(branchName), remoteRef.Hash()); err != nil {
if err := strategy.SafelyAdvanceLocalRef(ctx, repo, refs.Primary, remoteRef.Hash()); err != nil {
return fmt.Errorf("failed to advance local %s branch: %w", branchName, err)
}
strategy.MirrorCommittedMetadataRefBestEffort(ctx, repo)
return nil
}

Expand Down
Loading
Loading