Skip to content

feat(checkpoint): push checkpoint refs to one destination, and say where - #1905

Merged
gtrrz-victor merged 3 commits into
mainfrom
soph/refs-first-push-url
Aug 7, 2026
Merged

feat(checkpoint): push checkpoint refs to one destination, and say where#1905
gtrrz-victor merged 3 commits into
mainfrom
soph/refs-first-push-url

Conversation

@Soph

@Soph Soph commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

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

Stacked on #1897 (base branch is soph/multi-pushurl-tests) — it carries the test helpers this builds on. Merge #1897 first; the diff here is then just the behavior change.

Why refs need one destination

The push-discovery queue records only a ref — {"ref": …}, no per-destination state — so "this ref is pushed" has to mean one place. git's fan-out across a remote's several push URLs can't provide that, in two distinct ways:

  1. One failing URL fails the whole invocation, so no ref unqueues even though other URLs took it. A dead mirror wedges the queue indefinitely, retried on every push.
  2. git's iteration isn't uniform. A ref rejection returns and git carries on to later URLs; a transport failure (missing repo, auth, bad host) die()s. Verified:
First URL fails with… Later URLs Exit
non-fast-forward rejection ✅ still pushed 1
unreachable / transport skipped entirely 128

So an unreachable mirror in first position meant nothing was pushed anywhere, while the same mirror last was harmless. Position silently decided the outcome.

What changes

When a remote carries more than one push URL, checkpoint refs target its first push URL directly and the rest are skipped. The recovery fetch uses the same target, which also fixes a real mismatch: fan-out reconciled the remote's fetch url while pushing to its pushurls, so with a pushurl configured we were reconciling a repository nothing had been pushed to.

A single push URL keeps the remote NAME as the target, so the overwhelmingly common topology is byte-identical to before — remote-tracking refs still update, output still says origin, no URL-keyed promisor config appears. Only multi-URL remotes are affected.

git-branch deliberately keeps fan-out. Its v1 branch is a single shared ref with no queue to keep coherent, and mirroring it to every push URL is what people configure those remotes for (guarded by ..._Branch_RepeatedPushesKeepBothURLsInSync in #1897). The backends therefore differ on multi-URL remotes; that's documented at the decision point in refs_push_destination.go rather than left to be discovered.

Trade, accepted deliberately

  • Checkpoint refs live in exactly one repository. Clone that repository and reads resolve (its url becomes the clone's fetch URL); clone a different mirror of the same code and they don't. checkpoint_remote remains the way to name a checkpoint repository explicitly.
  • A first push URL that rejects a ref no longer lets later URLs receive it. git would have carried on; we stop. That's the price of a destination the queue can reason about.

Saying where — three surfaces

Since the choice is otherwise invisible:

  • pre-push warns on stderr, naming the chosen URL and how many were skipped;
  • entire enable notes it (both the first-time setup path and re-enable);
  • entire doctor reports it under Checkpoint destination: REVIEW.

All three also cover the other ambiguous topology — several remotes, where checkpoints follow whichever remote you push to while resume/explain always read via origin, so checkpoints pushed elsewhere aren't found again. All are silent on an ordinary repo and whenever checkpoint_remote already pins a destination.

Two message fixes on the way past

  • The batch-rejection retry claimed "Some checkpoint refs diverged" for every failure, including an unreachable destination — pointing users at the wrong problem. It now says the push was rejected without asserting why.
  • printCheckpointRemoteHint ("a checkpoint remote is configured in Entire settings") fired for any URL target, and would now fire for a URL we picked ourselves. Gated on an actual checkpoint_remote.
  • RedactURL renders a plain filesystem path as :///path/to/repo; display now passes paths through and only redacts URL-shaped values.

Tests

Refs tests from #1897 change meaning, so the diff shows the behavior flip:

  • ..._Refs_FanOutToBothPushURLs..._Refs_GoesToFirstPushURLOnly: first URL receives them, second does not, and they unqueue — the property the whole rule exists to enable.
  • ..._Refs_UnreachableSecondPushURL_RefsStayQueued..._IsIgnored: a broken later mirror is now irrelevant; refs land and unqueue. Previously this topology wedged the queue forever.
  • New ..._Refs_UnreachableFirstPushURL_ReachesNothing: the die() case, previously uncovered.
  • New ..._DestinationNoteSurfaces: doctor reports both ambiguous topologies and stays silent on an ordinary repo.

mise run check green (lint 0 issues, canary 59 + 4).

Note for reviewers

gitremote.GetPushURLs is also added by #1898. Whichever lands first, the other rebases and the duplicate drops out — trivial either way.

🤖 Generated with Claude Code


Note

Medium Risk
Changes pre-push checkpoint sync for multi-push-URL remotes (git-refs only); wrong first URL or user expecting mirrors could miss checkpoints until they set checkpoint_remote.

Overview
Git-refs checkpoint ref pushes no longer rely on git’s multi-URL fan-out. When a remote has more than one push URL, refs go to the first push URL only (single-URL remotes still push by remote name, unchanged). Recovery fetch uses the same target so push and reconcile stay aligned.

Adds gitremote.GetPushURLs / remote.GetPushURLs, resolveRefsPushDestination, and remote_topology so entire doctor and entire enable explain ambiguous setups (multi-push-URL remotes and multiple remotes vs origin for reads). Pre-push stderr warns when extra push URLs are skipped; checkpoint_remote still pins an explicit repo and suppresses those notes.

Also fixes misleading “refs diverged” retry messaging and limits printCheckpointRemoteHint to configured checkpoint_remote.

Integration tests flip from fan-out expectations to first-URL-only behavior and add doctor coverage.

Reviewed by Cursor Bugbot for commit 7b9968b. Configure here.

Copilot AI lite review requested due to automatic review settings August 5, 2026 15:36
@Soph
Soph requested a review from a team as a code owner August 5, 2026 15:36

@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 1 potential issue.

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 7b9968b. Configure here.

Comment thread cmd/entire/cli/strategy/manual_commit_push.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 changes how the git-refs checkpoint backend selects a push destination on remotes with multiple push URLs, ensuring checkpoint refs are pushed to one deterministic destination (the first push URL) so the ref-only push queue can reason about “pushed” as a single place. It also adds user-facing surfacing of the chosen destination (pre-push warning, entire enable, and entire doctor) and updates integration coverage for the new behavior.

Changes:

  • Add resolveRefsPushDestination to target the first push URL only for multi-push-URL remotes (git-refs), while keeping single-URL remotes byte-identical to prior behavior.
  • Surface ambiguous checkpoint topologies via entire enable and entire doctor, and warn during pre-push when push URLs are being skipped.
  • Add gitremote.GetPushURLs and update integration tests to validate the new single-destination behavior and destination reporting.

Reviewed changes

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

Show a summary per file
File Description
cmd/entire/cli/strategy/refs_push_destination.go Introduces deterministic destination resolution for git-refs checkpoint ref pushes, including multi-push-URL handling and warning/logging.
cmd/entire/cli/strategy/manual_commit_push.go Wires destination selection into the pre-push and manual push paths; updates progress/error messaging and gates the checkpoint-remote hint.
cmd/entire/cli/setup.go Adds a checkpoint destination note to entire enable output when topology is ambiguous.
cmd/entire/cli/remote_topology.go New helper to inspect remotes/push URLs and describe ambiguous checkpoint destinations for enable/doctor.
cmd/entire/cli/integration_test/multi_pushurl.go Adds a test helper to configure an unreachable first push URL.
cmd/entire/cli/integration_test/multi_pushurl_test.go Updates git-refs expectations to “first push URL only”, adds unreachable-first coverage, and validates doctor destination surfacing.
cmd/entire/cli/gitremote/gitremote.go Adds GetPushURLs to enumerate push destinations as git will use them.
cmd/entire/cli/checkpoint/remote/util.go Wraps gitremote.GetPushURLs for checkpoint/remote callers.
cmd/entire/cli/doctor.go Adds a doctor check that reports checkpoint destination ambiguity.
Suppressed comments (1)

cmd/entire/cli/strategy/manual_commit_push.go:314

  • Same as the pre-push path: emitting warnIgnoredPushURLs before checking whether the queue is non-empty can print warnings even when there is nothing to push (and can duplicate output across multiple hook invocations on multi-push-URL remotes).
	dest := resolveRefsPushDestination(ctx, ps)
	dest.warnIgnoredPushURLs(ctx, os.Stderr)
	pushed, err = flushCheckpointRefsQueue(ctx, repo, dest)

Comment thread cmd/entire/cli/strategy/manual_commit_push.go Outdated
Comment thread cmd/entire/cli/strategy/manual_commit_push.go Outdated
Soph added a commit that referenced this pull request Aug 5, 2026
Review feedback on #1905: "rejected" still implies the remote answered, but
batchPushRefs fails on transport errors too (unreachable host, auth). Same
mistake as the "diverged" wording it replaced, one step milder. Says "failed".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 01KZ9FZCHDJD1625C8XGTEAK6F
@Soph
Soph force-pushed the soph/multi-pushurl-tests branch from cbcee40 to bed532b Compare August 5, 2026 18:53
Soph and others added 3 commits August 5, 2026 20:53
The git-refs push-discovery queue records only a ref (`{"ref": …}`) with no
per-destination state, so "this ref is pushed" has to mean one place. Relying on
git's fan-out across a remote's several push URLs cannot provide that:

- one failing URL fails the whole `git push` invocation, so no ref unqueues even
  though other URLs took it — a dead mirror wedges the queue indefinitely;
- and git's iteration is not even uniform. A ref REJECTION returns and git
  carries on to later URLs, but a transport failure (missing repo, auth, bad
  host) die()s — so an unreachable FIRST push URL means nothing is pushed
  anywhere, while the same mirror in last position is harmless. Position
  silently decided the outcome.

So when a remote carries more than one push URL, checkpoint refs now target its
first push URL directly and the rest are skipped. The recovery fetch uses the
same target, which also fixes a mismatch: fan-out reconciled the remote's FETCH
url while pushing to its pushurls, so with a pushurl set we reconciled a repo
nothing was pushed to.

A single push URL keeps the remote NAME as the target, so the overwhelmingly
common topology is byte-identical to before — remote-tracking refs still update,
output still says "origin", no URL-keyed promisor config appears.

The git-branch backend deliberately keeps git's fan-out: its v1 branch is a
single shared ref with no queue to keep coherent, and mirroring it to every push
URL is what users configure those remotes for. The two backends therefore differ
on multi-URL remotes; that is documented at the decision point rather than left
to be discovered.

Trade accepted: checkpoint refs live in exactly one repository, so cloning a
different mirror of the same code will not find them, and a rejecting first URL
no longer lets later URLs receive the refs. checkpoint_remote remains the way to
name a checkpoint repository explicitly.

Because that is invisible otherwise, it is now announced in three places:
pre-push warns on stderr naming the chosen URL and how many were skipped;
`entire enable` notes it; `entire doctor` reports it under "Checkpoint
destination: REVIEW". All three also cover the other ambiguous topology —
several remotes, where checkpoints follow whichever remote you push to while
resume/explain always read via origin. All are silent on an ordinary repo and
whenever checkpoint_remote already pins a destination.

Two message fixes on the way past:

- the batch-rejection retry claimed "Some checkpoint refs diverged" for every
  failure, including an unreachable destination — sending users after the wrong
  problem. It now says the push was rejected without asserting why.
- printCheckpointRemoteHint ("a checkpoint remote is configured in Entire
  settings") fired for any URL target and would now fire for a URL we picked
  ourselves. It is gated on an actual checkpoint_remote.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 01KZ992TB3ZNJJR9H9CHG01JCR
Cleanups from a four-angle review (reuse, simplification, efficiency, altitude).
No intended behavior change except where noted.

Efficiency — the one that mattered:

- resolveRefsPushDestination (and its `git remote get-url --push --all` spawn)
  ran before flushCheckpointRefsQueue's early returns, so EVERY push on the
  git-refs default backend paid a fork/exec for a destination that goes unused
  when nothing is queued — and multi-URL users got the "checkpoints go to one
  repository" warning on no-op pushes. It now resolves after both early returns,
  inside the existing push_checkpoint_refs perf span. Behavior change, and the
  intended one: the warning no longer fires on a push with an empty queue.
- inspectRemoteTopology replaced `git remote` + one `get-url --push --all` per
  remote (N+1 spawns) with a single `git remote -v`, which already applies git's
  pushurl-replaces-url rule and lists push URLs in order. Also removes an
  inconsistency where remote listing ran in repoRoot while the URL lookups
  inherited process CWD.

Correctness of what we tell the user:

- inspectRemoteTopology treated a checkpoint_remote *present in settings* as a
  pinned destination and went silent. But PushURL only honors it when it
  resolves — owner mismatch, unparseable URL and non-derivable protocol all fall
  back — so doctor stayed quiet while pushes really did go first-URL-only. It now
  asks remote.PushURL per remote, the "ask the client, don't re-resolve" rule
  CLAUDE.md documents for CoreOrigin().
- It also reported the first *sorted* remote that fans out, so a repo with a
  fanning-out `backup` remote that only ever pushes to origin got warned about a
  rule that never applied to it. Ambiguity is now per remote, and each is named.
- The git-branch explanation described the git-refs failure behavior ("retries on
  the next push"). Corrected: those URLs are reported and left behind, and only
  the fetch URL is ever reconciled.
- refsPushDestination.display was set from displayPushTarget, which maps ANY URL
  to the literal words "checkpoint remote" — so a self-picked push URL claimed to
  be a checkpoint remote while the struct's own checkpointRemote said false.
  display is now a method that names a resolved URL by its redacted URL.

Reuse and simplification:

- "redact if URL-shaped, else pass the path through" existed in three copies
  (two added by the previous commit, one pre-existing in redactGitArgs). Now one
  gitremote.RedactURLOrPath, forwarded through the checkpoint/remote facade.
- refsPushDestination drops its `display` field (a method), and the resolver's
  five branches collapse to three.
- checkCheckpointDestination and printCheckpointDestinationNote were the same
  four-step body; one function takes the header. `indent` was speculative
  generality (one value at both call sites) and is gone, as is the third
  redundant ambiguity check.
- warnIgnoredPushURLs uses the package's injectable stderrWriter instead of an
  io.Writer parameter both call sites hardcoded.
- Dropped the `pushTarget := dest.target` alias.
- Test helpers: one setPushURLs(remote, urls...) primitive under all three
  push-URL helpers, with position as the parameter — AddUnreachableFirstPushURL
  was a verbatim copy of the Second variant with the slice order flipped. The
  three doctor subtests became a table.

Stale rationale the previous commit invalidated: both file headers still said the
CLI "has no per-URL control at all", true only for git-branch now. Both carve out
git-refs.

Also fixed while in the file: checkCodexHookTrust's doc comment has been detached
from its function since before this branch (it sat above checkClaudeCodeHookDrift);
moved down to the function it documents.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 01KZ9CNB8FYGCX05XV4V0D8NNX
Review feedback on #1905: "rejected" still implies the remote answered, but
batchPushRefs fails on transport errors too (unreachable host, auth). Same
mistake as the "diverged" wording it replaced, one step milder. Says "failed".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 01KZ9FZCHDJD1625C8XGTEAK6F
@Soph
Soph force-pushed the soph/refs-first-push-url branch from af5efcc to 1210d38 Compare August 5, 2026 19:04
Base automatically changed from soph/multi-pushurl-tests to main August 6, 2026 09:30
@gtrrz-victor
gtrrz-victor merged commit 313463b into main Aug 7, 2026
12 checks passed
@gtrrz-victor
gtrrz-victor deleted the soph/refs-first-push-url branch August 7, 2026 10:36
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.

3 participants