Skip to content

strategy: replay local checkpoints when fetch finds a diverged remote#1251

Open
pjbgf wants to merge 7 commits into
mainfrom
config-perm
Open

strategy: replay local checkpoints when fetch finds a diverged remote#1251
pjbgf wants to merge 7 commits into
mainfrom
config-perm

Conversation

@pjbgf
Copy link
Copy Markdown
Member

@pjbgf pjbgf commented May 22, 2026

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

SafelyAdvanceLocalRef only short-circuited when local was equal to or strictly ahead of targetHash; every other state (missing, behind, diverged, disconnected) flowed into the same SetReference call.

That was correct for "missing" (initialize) and "behind" (fast-forward), because targetHash already contains everything reachable from local. It was wrong for the diverged and disconnected cases: local-only checkpoint commits that hadn't been pushed yet were left as orphans the moment any caller (resume, doctor, the read-side metadata fetch) advanced the local ref to a remote tip that didn't contain them.

Split the post-equality branch on git-merge-base instead:

  mergeBase == targetHash  -> local ahead, no-op
  mergeBase == localHash   -> local behind, fast-forward (unchanged)
  mergeBase exists, neither -> diverged, cherryPickOnto local-only commits
  errNoMergeBase            -> disconnected, cherryPickOnto full local chain

The fast-forward path still goes through the same setRefHash call as before, so the existing "behind" behavior is preserved by construction. The diverged and disconnected paths now replay local-only commits onto targetHash via cherryPickOnto, which preserves the original Author and stamps the local user as Committer.

getMergeBase gained an errNoMergeBase sentinel so exit-1 ("no common ancestor") is distinguishable from a real git failure and can route to the disconnected-replay path instead of bubbling up as an error.

Regression tests under FetchMetadataBranch cover the diverged and disconnected cases and assert that the local-only checkpoint blob is still in the tree after the fetch.


Note

Medium Risk
Changes how local refs are advanced during checkpoint metadata fetches by cherry-picking local-only commits onto fetched tips, which affects git history manipulation and could mis-handle edge cases in unusual repo states.

Overview
Prevents FetchMetadataBranch from orphaning unpushed local checkpoints when the local metadata branch is diverged or disconnected from the fetched remote tip.

SafelyAdvanceLocalRef is updated to compute a merge-base with the fetched target and, when necessary, replay local-only commits onto the fetched tip (via cherryPickOnto) instead of overwriting the ref; it also adds a errNoMergeBase sentinel and teaches getMergeBase to surface the "no common ancestor" case distinctly.

Adds regression tests covering diverged and disconnected fetch scenarios, asserting that local-only checkpoint blobs remain present after fetch and that the resulting branch tip reflects a replay onto the remote tip.

Reviewed by Cursor Bugbot for commit e07f383. Configure here.

@pjbgf pjbgf requested a review from a team as a code owner May 22, 2026 16:47
Copilot AI review requested due to automatic review settings May 22, 2026 16:47
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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 updates the strategy layer’s ref-promotion logic so that fetching a checkpoint/metadata ref to a new remote tip won’t orphan local-only checkpoint commits when the local ref has diverged or is disconnected from the fetched tip.

Changes:

  • Introduces an errNoMergeBase sentinel and teaches getMergeBase to return it when git merge-base reports “no common ancestor”.
  • Refactors SafelyAdvanceLocalRef to branch based on merge-base outcomes and replay local-only commits onto the fetched tip via cherryPickOnto when appropriate.
  • Adds regression tests for diverged and disconnected metadata-branch fetch scenarios to ensure local-only checkpoint blobs remain present after fetch.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
cmd/entire/cli/strategy/push_common.go Updates merge-base handling to distinguish “no merge base” from other failures.
cmd/entire/cli/strategy/common.go Reworks SafelyAdvanceLocalRef to prevent dropping local-only commits by replaying them when diverged/disconnected.
cmd/entire/cli/strategy/checkpoint_remote_test.go Adds tests covering diverged and disconnected fetch promotions preserving local-only checkpoints.

Comment thread cmd/entire/cli/strategy/common.go
Comment thread cmd/entire/cli/strategy/common.go
pjbgf added 6 commits May 25, 2026 07:04
SafelyAdvanceLocalRef only short-circuited when local was equal to or
strictly ahead of targetHash; every other state (missing, behind,
diverged, disconnected) flowed into the same SetReference call.

That was correct for "missing" (initialize) and "behind" (fast-forward),
because targetHash already contains everything reachable from local. It
was wrong for the diverged and disconnected cases: local-only checkpoint
commits that hadn't been pushed yet were left as orphans the moment any
caller (resume, doctor, the read-side metadata fetch) advanced the local
ref to a remote tip that didn't contain them.

Split the post-equality branch on git-merge-base instead:

  mergeBase == targetHash  -> local ahead, no-op
  mergeBase == localHash   -> local behind, fast-forward (unchanged)
  mergeBase exists, neither -> diverged, cherryPickOnto local-only commits
  errNoMergeBase            -> disconnected, cherryPickOnto full local chain

The fast-forward path still goes through the same setRefHash call as
before, so the existing "behind" behavior is preserved by construction.
The diverged and disconnected paths now replay local-only commits onto
targetHash via cherryPickOnto, which preserves the original Author and
stamps the local user as Committer.

getMergeBase gained an errNoMergeBase sentinel so exit-1 ("no common
ancestor") is distinguishable from a real git failure and can route to
the disconnected-replay path instead of bubbling up as an error.

Regression tests under FetchMetadataBranch cover the diverged and
disconnected cases and assert that the local-only checkpoint blob is
still in the tree after the fetch.

Assisted-by: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: Paulo Gomes <paulo@entire.io>
Entire-Checkpoint: dbd08bd2b081
Signed-off-by: Paulo Gomes <pjbgf@linux.com>
Entire-Checkpoint: 4fac0fa5ffd3
Entire-Checkpoint: 5a8ec3220416
Signed-off-by: Paulo Gomes <pjbgf@linux.com>
Entire-Checkpoint: ea9e84b73618
Entire-Checkpoint: 223430e2c9ab
Entire-Checkpoint: 8f8c7f0a73c4
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.

2 participants