Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,14 @@ the commands are always runnable in every build.
- `project`: control-plane project management — `create`, `list`, `get`, `delete`
- `repo`: control-plane repository lifecycle — `create`, `list`, `get`, `delete`,
`clone`, plus the `mirror` and `visibility` subtrees. Git content operations
(log, diff, …) are intentionally out of scope.
(log, diff, …) are intentionally out of scope. The `mirror` subtree is
server-side (`create`, `list`, `get`, `remove`, `collaborators`) with one
exception: `mirror use` repoints the *current clone's* git remote at a mirror
(local git config only — it creates nothing server-side). Interactively it
picks among the repo's placements and asks whether to replace the remote
(preserving the old URL under `--upstream`) or add a separate one;
non-interactively it repoints `--remote` directly. Both `use` and `clone`
choose a placement through the shared `selectPlacement` picker.
- `grant`: manage access grants and org membership — `org`, `project`, and `repo`
each support `add` / `list` / `remove`

Expand Down
22 changes: 13 additions & 9 deletions cmd/entire/cli/corecmd_json_flag_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,19 @@ func TestControlPlaneJSONFlag_OnlyOnHonoringCommands(t *testing.T) {
"project get": true,
"project delete": false,
// repo
"repo create": true,
"repo list": true,
"repo get": true,
"repo delete": false,
"repo clone": false,
"repo mirror create": false,
"repo mirror list": true,
"repo mirror get": true,
"repo mirror remove": false,
"repo create": true,
"repo list": true,
"repo get": true,
"repo delete": false,
"repo clone": false,
"repo mirror create": false,
"repo mirror list": true,
"repo mirror get": true,
"repo mirror remove": false,
// `use` writes local git config and reports what it changed; there is no
// object to render, so it stays off the --json surface like the other
// side-effect verbs.
"repo mirror use": false,
"repo mirror collaborators list": true,
"repo visibility get": true,
"repo visibility set": true,
Expand Down
68 changes: 48 additions & 20 deletions cmd/entire/cli/repo_clone.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package cli

import (
"context"
"errors"
"fmt"
"os/exec"
"regexp"
Expand Down Expand Up @@ -241,14 +240,42 @@ func resolvePullablePlacements(ctx context.Context, c *coreapi.Client, owner, re
return out.Placements, nil
}

// selectCloneTarget resolves which mirror placement to clone from. With one
// placement it returns it directly. With --cluster it picks the matching one (or
// errors listing the available hosts). With more than one and no flag it prompts
// interactively, failing fast with a --cluster pointer when there's no terminal.
// placementPicker adapts selectPlacement's messages to the calling verb. The
// picker logic is identical for every consumer (dedupe by host, honor an
// explicit selector, prompt only when there's a real choice); only the words
// differ, so they're passed in rather than duplicated per command.
type placementPicker struct {
// selector names the non-interactive way to choose a cluster, as the user
// would type it (e.g. `--cluster`). Interpolated into the no-terminal error
// so the pointer names a flag the calling command actually accepts.
selector string
// title is the interactive single-select's prompt.
title string
// action names the operation in the cancellation message, capitalized
// ("Clone", "Remote update") — handleFormCancellation prints
// "<action> cancelled."
action string
}

// selectCloneTarget resolves which mirror placement to clone from, with the
// clone verb's wording. See selectPlacement for the selection rules.
func selectCloneTarget(cmd *cobra.Command, placements []coreapi.ResolvedPlacement, clusterFlag string) (coreapi.ResolvedPlacement, error) {
// Dedupe by cluster host: one placement per cluster is what a clone targets,
return selectPlacement(cmd, placements, clusterFlag, placementPicker{
selector: "--cluster",
title: "This repo is mirrored on more than one cluster — pick one to clone from",
action: "Clone",
})
}

// selectPlacement resolves which mirror placement a verb should act on. With one
// placement it returns it directly. With an explicit clusterSel it picks the
// matching one (or errors listing the available hosts). With more than one and no
// selector it prompts interactively, failing fast with a p.selector pointer when
// there's no terminal.
func selectPlacement(cmd *cobra.Command, placements []coreapi.ResolvedPlacement, clusterSel string, p placementPicker) (coreapi.ResolvedPlacement, error) {
// Dedupe by cluster host: one placement per cluster is what a caller acts on,
// and the same host appearing twice would only confuse the picker. Key on the
// case-folded host — DNS is case-insensitive, so a --cluster value differing
// case-folded host — DNS is case-insensitive, so a selector value differing
// only in case from the API's ClusterHost must still match (the alternative is
// a misleading "not mirrored on ..." after a successful lookup + dial).
byHost := make(map[string]coreapi.ResolvedPlacement, len(placements))
Expand All @@ -263,20 +290,20 @@ func selectCloneTarget(cmd *cobra.Command, placements []coreapi.ResolvedPlacemen
}
sort.Strings(hosts)

if clusterFlag != "" {
p, ok := byHost[strings.ToLower(strings.TrimSpace(clusterFlag))]
if clusterSel != "" {
match, ok := byHost[strings.ToLower(strings.TrimSpace(clusterSel))]
if !ok {
return coreapi.ResolvedPlacement{}, fmt.Errorf("repo is not mirrored on %q; available: %s", clusterFlag, strings.Join(hosts, ", "))
return coreapi.ResolvedPlacement{}, fmt.Errorf("repo is not mirrored on %q; available: %s", clusterSel, strings.Join(hosts, ", "))
}
return p, nil
return match, nil
}

if len(hosts) == 1 {
return byHost[hosts[0]], nil
}

if !interactive.CanPromptInteractively() {
return coreapi.ResolvedPlacement{}, fmt.Errorf("repo is mirrored on %d clusters; pass --cluster to choose one of: %s", len(hosts), strings.Join(hosts, ", "))
return coreapi.ResolvedPlacement{}, fmt.Errorf("repo is mirrored on %d clusters; pass %s to choose one of: %s", len(hosts), p.selector, strings.Join(hosts, ", "))
}

options := make([]huh.Option[string], len(hosts))
Expand All @@ -287,26 +314,27 @@ func selectCloneTarget(cmd *cobra.Command, placements []coreapi.ResolvedPlacemen
form := NewAccessibleForm(
huh.NewGroup(
huh.NewSelect[string]().
Title("This repo is mirrored on more than one cluster — pick one to clone from").
Title(p.title).
Options(options...).
Value(&selected),
),
)
cancelled := NewSilentError(fmt.Errorf("%s cancelled", strings.ToLower(p.action)))
if err := form.RunWithContext(cmd.Context()); err != nil {
// handleFormCancellation prints "Clone cancelled." and returns nil for a
// handleFormCancellation prints "<action> cancelled." and returns nil for a
// Ctrl+C / cancelled-context abort. Surface that as a SilentError so the
// caller stops instead of falling through to clone a zero-value target
// caller stops instead of falling through to act on a zero-value target
// (the `entire:///gh/...` empty-host bug); a real form error propagates.
if cerr := handleFormCancellation(cmd.ErrOrStderr(), "Clone", err); cerr != nil {
if cerr := handleFormCancellation(cmd.ErrOrStderr(), p.action, err); cerr != nil {
return coreapi.ResolvedPlacement{}, cerr
}
return coreapi.ResolvedPlacement{}, NewSilentError(errors.New("clone cancelled"))
return coreapi.ResolvedPlacement{}, cancelled
}
p, ok := byHost[selected]
match, ok := byHost[selected]
if !ok {
return coreapi.ResolvedPlacement{}, NewSilentError(errors.New("clone cancelled"))
return coreapi.ResolvedPlacement{}, cancelled
}
return p, nil
return match, nil
}

// mirrorCellLabel is the human label for a mirror placement in the clone picker:
Expand Down
7 changes: 4 additions & 3 deletions cmd/entire/cli/repo_mirror.go
Original file line number Diff line number Diff line change
Expand Up @@ -450,9 +450,9 @@ func validateClusterHost(host string) error {
// newRepoMirrorCmd is the `entire repo mirror` subtree: manage EntireDB
// GitHub-mirror placements on a cluster. Mirrors the standalone entiredb
// CLI's `entire repo mirror` surface for the server-side half (create /
// list / get / remove). The local-clone rewrite (`mirror use`) is not
// ported — it's a git-config + git-remote-entire concern outside the
// control-plane API.
// list / get / remove), plus the local-clone rewrite (`use`) — the one verb
// here that touches no control-plane state beyond a placement lookup and
// instead edits the current clone's git config (see repo_mirror_use.go).
func newRepoMirrorCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "mirror",
Expand All @@ -461,6 +461,7 @@ func newRepoMirrorCmd() *cobra.Command {
cmd.AddCommand(newRepoMirrorCreateCmd())
cmd.AddCommand(newRepoMirrorListCmd())
cmd.AddCommand(newRepoMirrorGetCmd())
cmd.AddCommand(newRepoMirrorUseCmd())
cmd.AddCommand(newRepoMirrorRemoveCmd())
cmd.AddCommand(newRepoMirrorCollaboratorsCmd())
return cmd
Expand Down
Loading
Loading