Skip to content

feat(repo): add entire repo mirror use to repoint a clone at a mirror - #1875

Open
entire[bot] wants to merge 3 commits into
mainfrom
feat/repo-mirror-use
Open

feat(repo): add entire repo mirror use to repoint a clone at a mirror#1875
entire[bot] wants to merge 3 commits into
mainfrom
feat/repo-mirror-use

Conversation

@entire

@entire entire Bot commented Jul 30, 2026

Copy link
Copy Markdown

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

Closes the last manual step in mirror onboarding. Adopting a mirror previously meant reading the clone URL out of mirror get and hand-running git remote set-url — the mirror use verb from the standalone entiredb CLI was deliberately not ported ("a git-config + git-remote-entire concern outside the control-plane API").

use is the local half of that concern and nothing more: it resolves the repo's pullable placements, picks one, and rewrites local git config. It creates no server-side state — an unmirrored repo errors with a pointer at mirror create.

Surface

entire repo mirror use [github-url] [cluster-host]
  --remote    <name>  git remote to point at the mirror (default "origin")
  --upstream  <name>  remote to preserve the replaced URL under (default "upstream"); '' discards
  --cluster   <host>  pin a cluster without naming the repo (same as [cluster-host])
  • Bare entire repo mirror use resolves the repo from the clone's own remotes, lists its placements, and prompts for the cluster when there is more than one.
  • On a terminal it then asks whether to repoint the remote (keeping the old URL as upstream) or add the mirror under a separate name.
  • Non-interactively it repoints --remote directly and preserves the replaced URL under --upstream.
  • The replaced URL is always echoed (credential-redacted via gitremote.RedactURL), and an existing upstream is never clobbered, so the previous state stays recoverable.

Notes for review

Two roles for a remote name. --remote is the write target, which need not exist yet, so repo identity resolves from the target remote first and then origin. Conflating the two was a bug during development: use --remote entire on a fresh clone failed because it tried to read coordinates from the remote it
was about to create.

Shared picker. The cluster picker is factored out of repo clone as selectPlacement + placementPicker, so both verbs share one selection path with per-verb wording. repo clone's messages and behavior are unchanged
(selectCloneTarget is now a thin wrapper).

Option order is load-bearing. huh answers an unreadable accessible prompt by writing the FIRST option's value and returning a nil error (verified by probe — it does not leave the bound value untouched and does not cancel). So the first branch of the replace-or-add prompt must produce the same writes the same flags produce non-interactively, or a Ctrl+D would silently diverge from the documented default. TestPromptMirrorRemoteChoice_FirstOptionMatchesNonInteractive pins that invariant. Worth knowing separately: repo clone's picker shares this property (closed stdin under a forced TTY clones from the first cluster); left alone as out of scope here.

Verification

  • mise run lint — 0 issues
  • mise run test — 8503 pass. The --json drift guard
    (TestControlPlaneJSONFlag_OnlyOnHonoringCommands) correctly caught the new
    command; classified false alongside the other side-effect verbs.
  • mise run test:integration — 448 pass
  • mise run test:e2e:canary — 4/4
  • Against the live API: resolved 4 real placements for gh/entireio/cli,
    repointed origin, and git ls-remote returned real refs through the
    rewritten URL. Both interactive branches driven end-to-end with ACCESSIBLE=1.

No CHANGELOG entry: entries there carry PR links and are assembled per release.

Closes the last manual step in mirror onboarding. Previously adopting a
mirror meant reading the clone URL out of `mirror get` and hand-running
`git remote set-url` — the `mirror use` verb from the standalone entiredb
CLI was deliberately not ported ("a git-config + git-remote-entire concern
outside the control-plane API").

`use` is the local half of that concern and nothing more: it resolves the
repo's pullable placements, picks one, and rewrites local git config. It
creates no server-side state — an unmirrored repo errors with a pointer at
`mirror create`.

- Bare `entire repo mirror use` resolves the repo from the clone's own
  remotes, lists its placements, and prompts for the cluster when there is
  more than one.
- On a terminal it then asks whether to repoint the remote (keeping the old
  URL as `upstream`) or add the mirror under a separate name.
- Non-interactively it repoints `--remote` (default `origin`) and preserves
  the replaced URL under `--upstream`; `--upstream ''` discards it. A
  cluster can be pinned with `--cluster` or the `[cluster-host]` positional.
- The replaced URL is always echoed (credential-redacted), and an existing
  `upstream` is never clobbered, so the previous state stays recoverable.

Repo identity and the write target are kept as separate roles: `--remote`
names what gets rewritten, which need not exist yet, so identity resolves
from the target remote first and then `origin`.

The cluster picker is factored out of `repo clone` as `selectPlacement` +
`placementPicker` so both verbs share one selection path with per-verb
wording; `repo clone`'s messages are unchanged.

Option order in the replace-or-add prompt is load-bearing and commented as
such: huh answers an unreadable accessible prompt with the first option and
a nil error, so the first branch must match what the same flags do
non-interactively. A test pins that invariant.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 01KYSGR4W1ZATZRW47J510EZGJ
@matthiaswenz
matthiaswenz marked this pull request as ready for review July 30, 2026 13:37
@matthiaswenz
matthiaswenz requested a review from a team as a code owner July 30, 2026 13:37
Copilot AI review requested due to automatic review settings July 30, 2026 13:37

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

Adds a new entire repo mirror use subcommand to complete mirror onboarding by repointing (or adding) a local git remote to an Entire mirror URL, using the same placement selection logic as entire repo clone.

Changes:

  • Add repo mirror use command to rewrite local git remotes based on resolved pullable mirror placements, with interactive/accessible prompting and a non-interactive flag-driven path.
  • Factor mirror placement selection into a shared selectPlacement helper (used by both repo clone and repo mirror use) with per-verb prompt wording.
  • Add unit tests for remote-name validation, planning/apply behavior, interactive-choice invariants, and command registration; update the JSON-flag drift guard and CLI architecture docs.

Reviewed changes

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

Show a summary per file
File Description
cmd/entire/cli/repo_mirror.go Registers the new use subcommand under repo mirror and updates subtree description.
cmd/entire/cli/repo_mirror_use.go Implements entire repo mirror use: resolves upstream repo, selects placement, and applies safe git-remote updates with reporting/prompting.
cmd/entire/cli/repo_mirror_use_test.go Adds unit tests covering validation, planning, apply behavior, prompt invariants, and error surfacing.
cmd/entire/cli/repo_clone.go Extracts shared placement selection/picker wording into selectPlacement + placementPicker.
cmd/entire/cli/corecmd_json_flag_test.go Updates command surface expectations to include repo mirror use (non-JSON side-effect verb).
CLAUDE.md Documents the mirror use exception within the repo mirror subtree behavior.

Comment thread cmd/entire/cli/repo_mirror_use.go Outdated
Comment on lines +237 to +240
default:
// Unreachable with the options above (huh always writes one of them).
// Kept so an unrecognised value can never fall through into a write.
return mirrorUseChoice{}, NewSilentError(errors.New("no remote update selected"))
Comment on lines +61 to +69
var gitRunner = func(ctx context.Context, dir string, args ...string) (string, error) {
cmd := exec.CommandContext(ctx, "git", args...)
cmd.Dir = dir
out, err := cmd.Output()
if err != nil {
return "", fmt.Errorf("git %s: %w", strings.Join(args, " "), err)
}
return strings.TrimSpace(string(out)), nil
}
matthiaswenz and others added 2 commits July 31, 2026 11:23
Addresses trail 955 finding (medium): in a fork checkout — `origin` plus
`upstream` both already configured, which is the common layout, not an edge
case — repointing `origin` found `--upstream` occupied, skipped preservation
to avoid clobbering it, and said nothing. The run printed a bare ✓ while the
forge URL left `.git/config` for good, surviving only on the `was:` line.
The absence of the "Kept the previous URL" line was the only signal, which a
reader (or an agent scanning for ✓) would miss.

Behavior is unchanged — an existing remote is still never clobbered. The skip
is now recorded on the plan (`preserveSkipped`) and reported as an explicit
stderr warning naming the occupied remote and carrying the URL needed to
recover it. `--upstream ''` stays silent: that is an explicit opt-out, not a
skipped preservation.

The recovery hint redacts credentials like the `was:` line does, and says so,
since this warning is as likely to land in a log as anything else printed.

Reproduced before the fix (old origin URL absent from .git/config with no
warning) and verified after, in a temp repo against the live API.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 01KYVQRVPDB3J6SWCNVKECHKFM
…o-message exits

Addresses both Copilot review comments on PR #1875.

Credential leak (repo_mirror_use.go:66). gitRunner echoed the raw argv into its
error, and `git remote add <preserveAs> <replacedURL>` passes the user's previous
remote URL — which can embed a token. That error is a plain (printed) error, so
it reached stderr and from there logs and pasted transcripts. Demonstrated
before the fix:

  preserve current origin URL as "origin": git remote add origin
  https://user:ghp_SUPERSECRET@github.com/octocat/hello-world: exit status 3

and after:

  ... git remote add origin https://github.com/octocat/hello-world: exit status 3

New redactGitArgs runs URL-shaped args through gitremote.RedactURL — the same
redaction reportMirrorRemotePlan already applied to the printed URL, so the
failure path is no longer the one place that leaked. Only URL-shaped args are
touched: RedactURL turns a bare word like "remote" into "://remote", so it
cannot be applied blanket-fashion. The command and host still survive for
diagnosis.

Undiagnosable silent exits. The unreachable `default` branch of the
replace-or-add prompt returned a SilentError without printing anything, so if it
were ever reached the command would exit non-zero with no message at all. Now a
plain error. The same defect existed on the analogous path in the shared picker
(`selectPlacement`'s `!ok` after a successful form run, pre-existing in
selectCloneTarget) — also converted, since silence there is never right. The two
remaining SilentError uses are correct: each follows a message already printed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 01KYVSPNJA0Z7AB5HFRH6V33BS
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