Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 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