Skip to content

Preserve diverged local refs in SafelyAdvanceLocalRef#1256

Closed
Soph wants to merge 1 commit into
mainfrom
soph/safely-advance-local-ref-skip-on-divergence
Closed

Preserve diverged local refs in SafelyAdvanceLocalRef#1256
Soph wants to merge 1 commit into
mainfrom
soph/safely-advance-local-ref-skip-on-divergence

Conversation

@Soph
Copy link
Copy Markdown
Collaborator

@Soph Soph commented May 22, 2026

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

Summary

  • The previous SafelyAdvanceLocalRef contract overwrote any local ref the target couldn't reach, which silently discarded unpushed local commits whenever a fetch landed sibling commits — e.g. checkpoint metadata produced on another machine sharing the same orphan-style refs (entire/checkpoints/v1, the V2 main ref). The objects survived in the loose-objects pool until git gc, but the branch ref no longer pointed at them.
  • Tighten the helper so it only sets the ref when the move is non-destructive:
    • missing → create
    • local at/ahead of target → no-op (existing protection)
    • local strictly behind target → fast-forward
    • diverged or unrelated history → no-op, logged at debug level
  • All three production callers (promoteRemoteTrackingMetadataBranch, FetchMetadataBranch, PromoteTmpRefSafely) sync orphan branches where "skip on divergence" is the correct semantic. In the rare divergent case, resume falls through to remote-tracking-tree reads (v1) or the V1 fallback (DualCheckpointReader → v1).
  • Update TestFetchV2MainFromURL_UpdatesExistingRef to use the existing advanceV2MainOnTop helper. The prior setup advanced the remote via a second call to createV2MainRef (which always produces an unrelated orphan commit). That setup was only "updating" the local ref via the now-removed diverged-overwrite path; a descendant commit on top of the previous tip is what the real condensation flow produces.

Companion PR

Stacks naturally with #1252 (which fixes the immediate user-visible "session log not available" bug). This PR is the broader hardening that came out of reviewing that fix — protects unpushed local checkpoint commits against destructive overwrite across all fetch paths.

Test plan

  • mise run check passes (fmt, lint, unit, integration, canary)
  • New tests in strategy/safely_advance_local_ref_test.go cover all six branches:
    • LocalMissing_SetsToTarget
    • LocalEqualsTarget_NoOp
    • LocalAhead_NoOp (existing protection)
    • LocalBehind_FastForwards
    • Diverged_PreservesLocal (new behavior — failed on main)
    • UnrelatedHistory_PreservesLocal (new behavior — failed on main)
  • Existing TestFetchMetadataBranch_DoesNotRewindLocalAhead still passes
  • Existing TestFetchV2MainFromURL_DoesNotRewindLocalAhead still passes
  • Manual: simulate a diverged metadata branch (orphan commits on two machines), confirm fetch no longer rewinds local

🤖 Generated with Claude Code


Note

Medium Risk
Changes git ref promotion logic used by metadata/v2 ref fetch paths; incorrect ancestor detection could cause refs to stop updating (or to remain diverged) and impact resume behavior, though it reduces risk of losing unpushed local commits.

Overview
Prevents fetch/promotion code from overwriting orphan-style local refs when the fetched target has diverged or unrelated history, preserving unpushed local checkpoint commits. SafelyAdvanceLocalRef now only creates, no-ops, or fast-forwards refs, and logs a debug message when it detects divergence instead of updating.

Adds a dedicated unit test suite covering missing/equal/ahead/behind/diverged/unrelated scenarios, and updates TestFetchV2MainFromURL_UpdatesExistingRef to advance the remote v2 ref via a true descendant commit so the test exercises the intended fast-forward path.

Reviewed by Cursor Bugbot for commit ca40dd0. Configure here.

The previous contract overwrote any local ref the target couldn't reach,
which silently discarded unpushed local commits whenever a fetch landed
sibling commits — e.g. checkpoint metadata produced on another machine
sharing the same orphan-style refs (entire/checkpoints/v1, the V2 main
ref). The objects survived in the loose-objects pool until git gc, but
the branch ref no longer pointed at them.

Tighten the helper so it only sets the ref when the move is
non-destructive: missing → create, local at/ahead → no-op (existing
protection), local strictly behind → fast-forward, diverged or
unrelated history → no-op with a debug log. All three production
callers (promoteRemoteTrackingMetadataBranch, FetchMetadataBranch,
PromoteTmpRefSafely) sync orphan branches where this is the correct
semantic; resume falls through to remote-tracking-tree reads in the
rare divergent case.

Update TestFetchV2MainFromURL_UpdatesExistingRef to use the existing
advanceV2MainOnTop helper — the prior setup advanced the remote via a
second call to createV2MainRef, which always produces an unrelated
orphan commit. That setup was only "updating" via the now-removed
diverged-overwrite path; a descendant commit on top of the previous
tip is what the real condensation flow produces.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 4197ff1b1da6
@Soph Soph requested a review from a team as a code owner May 22, 2026 21:19
Copilot AI review requested due to automatic review settings May 22, 2026 21:19
@Soph
Copy link
Copy Markdown
Collaborator Author

Soph commented May 22, 2026

Folding into #1252 since the changes are tightly coupled — the global helper tightening makes the resume fix safer, but each fix is small on its own. Branch will be deleted.

@Soph Soph closed this May 22, 2026
@Soph Soph deleted the soph/safely-advance-local-ref-skip-on-divergence branch May 22, 2026 21:22
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 hardens git ref promotion/advancement for Entire’s orphan-style metadata refs by ensuring SafelyAdvanceLocalRef only performs non-destructive updates (create / no-op / fast-forward), and skips updates on divergence to avoid losing unpushed local checkpoint commits.

Changes:

  • Update SafelyAdvanceLocalRef to no-op (with debug logging) when local and target refs have diverged/unrelated history.
  • Add a dedicated unit test suite covering missing/equal/ahead/behind/diverged/unrelated scenarios for SafelyAdvanceLocalRef.
  • Fix TestFetchV2MainFromURL_UpdatesExistingRef setup to advance the remote ref via a true descendant commit (fast-forward scenario).

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

File Description
cmd/entire/cli/strategy/common.go Tightens ref-advance semantics to avoid destructive overwrites and adds debug logging on divergence.
cmd/entire/cli/strategy/safely_advance_local_ref_test.go Adds focused unit coverage for the new SafelyAdvanceLocalRef decision matrix.
cmd/entire/cli/strategy/checkpoint_remote_test.go Updates the v2 /main fetch test to exercise the intended fast-forward path.
Comments suppressed due to low confidence (1)

cmd/entire/cli/strategy/common.go:151

  • repo.Reference(localRefName, true) errors other than plumbing.ErrReferenceNotFound are currently treated the same as “ref missing” (the function falls through and unconditionally sets the ref). That can mask repository I/O/corruption errors and potentially overwrite a ref when we should instead return an error. Consider explicitly handling ErrReferenceNotFound as the only create-path, and returning a wrapped error for all other localErr values.
func SafelyAdvanceLocalRef(ctx context.Context, repo *git.Repository, localRefName plumbing.ReferenceName, targetHash plumbing.Hash) error {
	currentLocal, localErr := repo.Reference(localRefName, true)
	if localErr == nil {
		if currentLocal.Hash() == targetHash {

Comment on lines 146 to +159
@@ -143,6 +154,14 @@ func SafelyAdvanceLocalRef(ctx context.Context, repo *git.Repository, localRefNa
if IsAncestorOf(ctx, repo, targetHash, currentLocal.Hash()) {
return nil
}
if !IsAncestorOf(ctx, repo, currentLocal.Hash(), targetHash) {
logging.Debug(ctx, "skipping advance: local ref has diverged from target",
slog.String("ref", string(localRefName)),
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