Skip to content

Add entire doctor migrate-checkpoints (git-branch → git-refs)#1611

Merged
gtrrz-victor merged 16 commits into
mainfrom
feat/checkpoint-migrate
Jul 9, 2026
Merged

Add entire doctor migrate-checkpoints (git-branch → git-refs)#1611
gtrrz-victor merged 16 commits into
mainfrom
feat/checkpoint-migrate

Conversation

@Soph

@Soph Soph commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

https://entire.io/gh/entireio/cli/trails/735

Adds a migration that converts checkpoints on the entire/checkpoints/v1 branch into per-checkpoint refs under refs/entire/checkpoints/<shard>/<id> — the layout the git-refs store uses — so a repo on the git-branch store can move to git-refs.

How it works

checkpoint.MigrateBranchToRefs(ctx, repo, dryRun) walks the v1 branch tip and, for each checkpoint:

  • Takes the checkpoint's current subtree object and wraps it in a fresh commit (existing branch commits are not remapped), then points the ref at it.
  • Because the git-refs ref tree is the branch's <shard>/<id> subtree byte-for-byte, a migrated checkpoint reads identically under either backend.

Idempotent: a checkpoint whose ref already carries the same tree is skipped; a changed checkpoint re-migrates by fast-forward (the new commit parents on the existing ref, so nothing is lost from history). Safe to re-run after more branch activity.

The command

entire doctor migrate-checkpoints — reports migrated / already-up-to-date / total.

Push behavior (as requested): the function only enqueues; the command decides whether to push:

  • Interactive → prompts "Push N migrated checkpoint ref(s) now?"
  • Non-interactive → never pushes; refs stay queued and flush on the next git push once git-refs is the primary store.
  • --dry-run reports what would migrate without writing refs.

Push reuse

The git-refs pre-push queue-flush is extracted into strategy.flushCheckpointRefsQueue, shared by the fail-soft pre-push path and the new error-surfacing strategy.PushMigratedCheckpointRefs. So "push now" goes through the exact same fast-forward + fetch/replay recovery logic (no force). No behavior change to pre-push.

Testing

  • New migrate_test.go: basic migration + byte-identical ref trees + read-back via the git-refs store; idempotent re-run (all skipped, refs unmoved); advance-on-branch-change (fast-forward parenting); dry-run writes nothing; no-branch no-op.
  • Build clean, lint 0 issues (the flush extraction keeps dupl quiet). Checkpoint + strategy unit suites pass; integration 392; git-refs canary 58/58 +1 skip, roger 4/4.

Not included (follow-ups)

  • ULID emission, and pushing a git-branch mirror for downgrade safety — unchanged from the git-refs store's existing scope.

Refs: #1471

🤖 Generated with Claude Code


Note

Medium Risk
Migration rewrites local git refs and checkpoint data layout; push behavior differs between interactive opt-in and fail-soft pre-push, though logic is shared and tested.

Overview
Adds entire doctor migrate-checkpoints so repos on the git-branch checkpoint store can move to the git-refs layout (refs/entire/checkpoints/<shard>/<id>).

checkpoint.MigrateBranchToRefs walks checkpoints on entire/checkpoints/v1, wraps each checkpoint’s current subtree in a new commit, and points the per-checkpoint ref at it (enqueueing push like normal git-refs writes). It is idempotent (same tree → skip; changed tree → fast-forward parent) and supports --dry-run. Unmappable IDs are skipped with a warning.

The doctor subcommand reports migrated / skipped / total counts. Interactive runs can opt into an immediate push; non-interactive runs leave refs queued for the next git push.

strategy.flushCheckpointRefsQueue extracts the git-refs push-queue drain logic from pre-push; pre-push still fail-soft on errors, while PushMigratedCheckpointRefs surfaces errors for the migration “push now” path. migrate_test.go covers migration fidelity, idempotency, branch updates, dry-run, and no-branch no-op.

Reviewed by Cursor Bugbot for commit 856c7ca. Configure here.

Copilot AI review requested due to automatic review settings July 2, 2026 18:45
@Soph Soph requested a review from a team as a code owner July 2, 2026 18:45

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Want fixes drafted automatically? Bugbot Autofix can create code changes for findings. A team admin can enable Autofix in the Cursor dashboard.

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 856c7ca. Configure here.

Comment thread cmd/entire/cli/doctor_migrate.go
Comment thread cmd/entire/cli/doctor_migrate.go Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a doctor-time migration path to move existing checkpoints from the legacy single branch store (entire/checkpoints/v1) into the per-checkpoint ref layout used by the git-refs checkpoint store (refs/entire/checkpoints/<shard>/<id>), enabling repos to transition storage backends without rewriting branch history.

Changes:

  • Add entire doctor migrate-checkpoints to migrate v1-branch checkpoint subtrees into per-checkpoint ref commits, with --dry-run and optional interactive “push now”.
  • Implement checkpoint.MigrateBranchToRefs plus unit tests validating byte-identical trees, idempotency, fast-forward advancement, and dry-run behavior.
  • Extract/refactor checkpoint-ref queue flushing in manual_commit_push.go to share logic between pre-push (fail-soft) and migration “push now” (error-surfacing).

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
cmd/entire/cli/strategy/manual_commit_push.go Extracts queued checkpoint-ref flush into a shared helper and adds an error-surfacing push entrypoint for migration.
cmd/entire/cli/doctor.go Registers the new doctor migrate-checkpoints subcommand.
cmd/entire/cli/doctor_migrate.go Implements the doctor migrate-checkpoints command, including interactive “push now” prompt and dry-run flag.
cmd/entire/cli/checkpoint/migrate.go Adds the core migration routine to convert v1-branch checkpoints into per-checkpoint refs.
cmd/entire/cli/checkpoint/migrate_test.go Adds tests covering migration correctness, idempotency, advancement, dry-run, and no-branch no-op.

Comment thread cmd/entire/cli/checkpoint/migrate.go
Comment thread cmd/entire/cli/strategy/manual_commit_push.go Outdated
Comment thread cmd/entire/cli/doctor_migrate.go Outdated
Comment thread cmd/entire/cli/checkpoint/migrate.go Outdated
Comment thread cmd/entire/cli/checkpoint/migrate.go Outdated
Soph and others added 3 commits July 3, 2026 15:25
Convert checkpoints on the entire/checkpoints/v1 branch into per-checkpoint refs
(refs/entire/checkpoints/<shard>/<id>), the git-refs store's layout.

checkpoint.MigrateBranchToRefs walks the v1 branch tip and, for each checkpoint,
wraps its CURRENT subtree object in a fresh commit and points the ref at it —
existing branch commits are not remapped. Since the git-refs ref tree is the
branch's <shard>/<id> subtree byte-for-byte, a migrated checkpoint reads
identically under either backend. It is idempotent: a checkpoint whose ref
already carries the same tree is skipped, and a changed checkpoint re-migrates by
fast-forward (new commit parents on the existing ref, so no history is lost).
New/advanced refs are enqueued for push; the function itself never pushes.

The `doctor migrate-checkpoints` command reports migrated/skipped/total and, per
the requested policy, only pushes when it can prompt: interactively it asks
whether to push now; non-interactively it never pushes (refs stay queued and
flush on the next push once git-refs is primary). `--dry-run` reports what would
change without writing refs.

Push reuse: the git-refs pre-push queue-flush is extracted into
strategy.flushCheckpointRefsQueue (shared by the fail-soft pre-push path and the
new error-surfacing strategy.PushMigratedCheckpointRefs), so the "push now"
option goes through the exact same fast-forward + fetch/replay logic.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: ceed5d17e529
…ntRefs

- doctor migrate-checkpoints now reuses the existing confirmDoctorFix helper
  instead of a near-duplicate confirm form — picking up its context-cancellation
  guard (huh opens the TTY during startup regardless of ctx state) and dropping
  the huh/errors/context imports.
- PushMigratedCheckpointRefs takes the *git.Repository the command already
  opened rather than re-opening one, matching the strategy entry-point pattern
  and removing a redundant repo open per invocation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 01f324c13852
…remote, parent validation)

- Map context.Canceled to NewSilentError so Ctrl-C exits quietly instead of
  printing a noisy Cobra error (matches the clean.go convention). [cursor]
- When the user confirms push but checkpoint pushing is disabled in settings,
  report that the refs stay queued instead of "Pushed 0 checkpoint ref(s)".
  [cursor]
- Add a --remote flag (default origin) instead of hardcoding "origin", so repos
  that push to a different remote work. [copilot]
- MigrateBranchToRefs now only uses an existing ref as the new commit's parent
  when it resolves to a real commit; a ref at an unreadable/non-commit object is
  treated as absent (orphan) rather than parenting on a bad hash, which would
  corrupt the commit graph for fetch+replay. [copilot]

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 5d352fa1b1bb
@Soph Soph force-pushed the feat/checkpoint-migrate branch 2 times, most recently from 06b3c99 to 96537c1 Compare July 4, 2026 08:48
pfleidi added 6 commits July 6, 2026 16:45
PushMigratedCheckpointRefs flushed the queue without the policy check
both pre-push paths enforce, so a policy that blocks checkpoint pushes
(diverged or requiring a newer CLI) could be bypassed via the doctor's
opt-in push. Apply the same sync-and-gate, returning an error with the
refs left queued.

Cover the push entry point: success returns the pushed count and clears
the queue; a blocked policy and a failed push both leave refs queued.

Entire-Checkpoint: 0c43e61ea113
Migrated checkpoints carried their v1-branch metadata verbatim: the
legacy checkpoint_version stamp ("branch-v1") and sessions[] file
paths relative to the sharded branch root, both wrong for a
per-checkpoint ref whose tree root is the checkpoint itself.

The migration now rewrites the root metadata.json per checkpoint:
checkpoint_version is dropped and any session string value under the
branch prefix is rebased to the ref's tree root, matching native
git-refs writes without hardcoding the SessionFilePaths field names.
The JSON object is edited in place so fields this CLI doesn't model
survive. Session subtrees carry over byte-identical (the metadata.json
swap goes through ApplyTreeChanges, so sibling subtree hashes are
untouched structurally), and idempotency compares against the
normalized tree.

See #1611 (comment)

Entire-Checkpoint: 4e6e24afb3e3
Resolve the existing ref through the refs store's refBase instead of
an inline Reference/CommitObject chain, keeping the same semantics: a
ref that doesn't resolve to a readable commit is treated as absent and
the checkpoint is re-migrated as an orphan (pinned by a new test).

Drop the unreachable malformed-id skip — WalkCheckpointShards only
yields validated IDs, and refBase would surface the error anyway.

Rename PushMigratedCheckpointRefs to PushQueuedCheckpointRefs: it
flushes the whole queue, not just migrated refs.

Entire-Checkpoint: 57d756e084fd
# Conflicts:
#	cmd/entire/cli/strategy/manual_commit_push.go
A re-run compared only the ref tip tree, so a ref that advanced past
the migration snapshot through refs-store writes (summary/transcript
backfills) would be "re-migrated": a new commit wrapping the stale
branch tree, fast-forwarding the tip backwards content-wise and
propagating on the next push.

Idempotency now walks the ref's first-parent chain — a snapshot found
anywhere in history is skipped. The doctor command additionally
refuses to run when git-refs is already the primary store, where the
v1 branch may lag the refs and re-importing could only regress them.

Entire-Checkpoint: 01KWX0X0VSAQD8ZCC4Q8QRDAP6
MigrateBranchToRefs promised refs are queued for push, but setRef's
enqueue is best-effort — a failed enqueue left migrated refs written
locally and silently never pushed. The migration now enqueues
explicitly and fails on error; duplicates collapse on Drain.

PushQueuedCheckpointRefs now runs post-push shadow-branch cleanup
after a successful flush, matching the pre-push paths.

Entire-Checkpoint: 01KWX19DEHT2AEBYYGZQY33HV7
@pfleidi

pfleidi commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Quick summary of the recent changes:

Changes on feat/checkpoint-migrate since 96537c1

  • Policy gate on the opt-in push (36563bc7) — PushMigratedCheckpointRefs now syncs and honors the checkpoint policy like both pre-push paths; a blocked policy returns an error with the refs left queued. Added tests for the push entry point (success clears the queue; blocked policy and failed pushes leave refs queued).
  • Metadata normalization during migration (befdcf4f) — addresses the PR thread: each checkpoint's root metadata.json is rewritten for the refs layout. The legacy checkpoint_version field is dropped entirely and sessions[] paths are rebased from the sharded branch root to the ref's tree root, matching native git-refs writes. The JSON is edited in place as a raw object, so fields newer CLIs may have written survive. Session subtrees still carry over byte-identical; idempotency compares against the normalized tree.
  • Simplify pass (b9159edd) — ref resolution goes through the store's refBase helper (keeping the orphan-on-unreadable-ref semantics from 96537c1, now pinned by a test), the unreachable malformed-ID skip is gone, and PushMigratedCheckpointRefs is renamed to PushQueuedCheckpointRefs since it flushes the whole queue.
  • Merge of origin/main (4e07984b) — the branch was ~100 commits behind; one conflict in manual_commit_push.go, resolved to keep the progress-dot output from main inside the extracted flush helper.
  • Re-run guard (5edbdf07) — a re-run used to compare only the ref tip tree, so refs that advanced past the migration snapshot (e.g. summary backfills) would be "re-migrated" back to stale branch content and pushed. Idempotency now walks the ref's first-parent chain, and the doctor command refuses outright when git-refs is already the primary store.
  • Remaining PR feedback (4501fc9b) — the migration now enqueues refs for push explicitly and fails on enqueue errors (previously best-effort via setRef, so a failure silently left refs unpushed), and the opt-in push runs shadow-branch cleanup after a successful flush, matching pre-push.

All seven open review threads on #1611 are replied to and resolved. Known deliberate calls: OPF is not re-applied during migration (branch content copies as-is), and a corrupt/unreadable checkpoint still fails the whole run as a readability signal.

Soph and others added 7 commits July 7, 2026 13:51
MigrateBranchToRefs collapsed *every* refBase error to parent=ZeroHash,
so a momentary read failure — a concurrent repack, lock contention —
made the migration treat a still-valid checkpoint ref as absent, wrap
the branch snapshot in a fresh orphan commit, and setRef over the good
ref (discarding its history; the follow-up fast-forward-only push then
rejects it).

Split the cases: an absent ref stays an orphan, a ref whose commit
object is genuinely missing (ErrObjectNotFound: corrupt/pruned) is still
re-imported as an orphan, but any other read error now aborts the
checkpoint so an idempotent re-run retries rather than rewriting a ref
that was only transiently unreadable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 01KWY6NRP1P3QH7DA54EYCM61W
The idempotency skip keyed only on "snapshot already on the ref," so a
ref written by a prior run that then failed to enqueue it — an Enqueue
error, or a crash between setRef and Enqueue — was skipped on every
later run and never queued for push. The migrated ref then existed
locally but its checkpoint data never reached the remote, silently
defeating the guaranteed-enqueue contract on exactly the retry it was
meant to enable.

Compute the ref name up front and enqueue on the skip path too (never in
dry-run). Enqueue is idempotent — duplicates collapse on Drain and an
already-pushed ref is a no-op on the next push — so re-enqueuing a
skipped checkpoint is safe and closes the gap. Dry-run now accounts an
already-imported checkpoint as a skip rather than reaching the old
would-migrate branch, and writes/enqueues nothing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 01KWY6VAT1V2Z6NHCKZ3BNNJY2
migratedCheckpointTree ran before the dry-run guard and, for any
checkpoint whose metadata needs normalizing, called CreateBlobFromContent
+ ApplyTreeChanges — both persist via SetEncodedObject. So a "preview"
that promises to report "without writing" left a dangling blob + tree per
checkpoint in the object store (thousands on a large v1 branch, until a
future git gc).

Thread the dry-run flag into migratedCheckpointTree: when not persisting,
compute the normalized blob and tree hashes in memory (git hashes are
content-addressed, so an object encoded but not stored hashes identically
to a persisted one) and return without touching the object store. The
persisting path is unchanged. A new test asserts the object count is
unchanged across a dry-run, and another migrates for real then dry-runs
again and asserts the checkpoint is recognized as already-migrated —
pinning the in-memory hash to the persisted tree hash.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 01KWY7CS85ZMCCM8RQEGKHPQAF
normalizeMigratedMetadata rebases session paths by string-stripping the
"/<shard>/<id>" prefix rather than round-tripping through the metadata
model the git-refs writer uses, and nothing tied the two code paths
together. The rebasing is correct for today's layout, but if the native
layout ever drifts — a renamed session dir, a new path field, a field
that isn't simply prefix-relative — the migration would silently ship
checkpoints whose paths a native reader can't resolve.

Add a regression test that writes the same checkpoint content two ways —
natively via the git-refs store and migrated from the git-branch store —
and asserts their root-metadata session paths are identical. Any future
divergence between the writer and the migration now fails loudly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 01KWY7PAG1KG98TP2RHZX9RP6K
PushQueuedCheckpointRefs returned before cleanupPushedShadowBranches
whenever flush returned an error — but the diverged path returns an error
even when some refs pushed successfully. So a "push now" that partially
succeeds left the landed refs' shadow branches uncleaned, unlike the
pre-push path, which always runs cleanup after flush. Run cleanup
unconditionally after flush for parity; it is best-effort and a no-op
when nothing pushed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 01KWY7RMYDFVJART4FKV3FT0KS
…bled"

PushQueuedCheckpointRefs returned (0, nil) both when push_sessions is
disabled and when the queue is empty, so the doctor command reported
"Checkpoint pushing is disabled in settings" even when pushing was
enabled and the refs had simply already been pushed — e.g. a concurrent
git push flushed the just-migrated refs while the confirm prompt blocked.

Return an explicit pushDisabled flag alongside the pushed count so the
two cases are distinguishable. The command now prints the disabled
message only when pushing is actually disabled, and a distinct "no queued
refs to push" message otherwise. Adds a push-disabled test and updates
the existing entry-point tests for the new signature.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 01KWY7XFF3PTNHZTXKW66FGRF1
The new dry-run tests added a third "branch-v1" literal, tripping
goconst. Name it once as legacyCheckpointVersion.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 01KWY80X77KHP4HV0Z9EJHFK16
@gtrrz-victor gtrrz-victor merged commit 6c5bd6c into main Jul 9, 2026
10 checks passed
@gtrrz-victor gtrrz-victor deleted the feat/checkpoint-migrate branch July 9, 2026 11:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

4 participants