Skip to content

Commit ba533b8

Browse files
gtrrz-victorclaude
andcommitted
refactor(repo): mirror get owner/repo renders a record, not a list
The owner/repo drill-down now reads as a detail view: bold repo name, dim-labeled identity fields (private, access when known), then a CLUSTER / CLONE URL / STATUS table — cluster cyan, clone URL the default foreground (it is this view's payload), status by lifecycle. Repeating the repo name on every row is gone. Data source moves from a ListMirrors owner sweep to one exact-match /repos?filter= lookup plus the cluster catalog. That also makes --json emit the same repoDirRow shape as list --json (placements nested), and readies a candidate rendering (access + availability, no table) for when the control plane's filter starts matching onboardable repos — today it matches onboarded only, so the not-found error points at `mirror list --available`. Placements sort by cluster slug for a deterministic table. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Entire-Checkpoint: 01KY78J19CM3TP2ZV976P6VXRP
1 parent 501a87c commit ba533b8

2 files changed

Lines changed: 176 additions & 111 deletions

File tree

cmd/entire/cli/repo_mirror.go

Lines changed: 106 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1017,8 +1017,9 @@ func newRepoMirrorGetCmd() *cobra.Command {
10171017
Use: "get <mirror>",
10181018
Short: "Show a repo's mirrors by owner/repo, or one mirror by ULID or clone URL",
10191019
Long: "Show a mirror, or every mirror of a repo. <mirror> is one of:\n\n" +
1020-
" - <owner>/<repo>, as shown in the `mirror list` NAME column — lists that\n" +
1021-
" repo's mirror on every cluster, with per-cluster clone URL and status\n" +
1020+
" - <owner>/<repo>, as shown in the `mirror list` NAME column — shows the\n" +
1021+
" repo (visibility, access) and its mirror on every cluster, with\n" +
1022+
" per-cluster clone URL and status\n" +
10221023
" - a mirror ULID\n" +
10231024
" - an entire:// clone URL (entire://<cluster>/gh/<owner>/<repo>) — the form\n" +
10241025
" `git clone` accepts; a trailing .git, as pasted from `git remote -v`, is\n" +
@@ -1051,19 +1052,13 @@ func newRepoMirrorGetCmd() *cobra.Command {
10511052
return runCoreObject(cmd, columnHeaders(mirrorColumns), mirrorRow, show)
10521053
}
10531054
// The owner/repo form is the drill-down from the grouped `mirror
1054-
// list` NAME column: one row per cluster mirror of that repo, so
1055-
// the per-placement detail the list aggregates away (clone URL,
1056-
// per-cluster status) is one copy-paste away. Like a ULID it
1057-
// carries no cluster coordinate, so it resolves on the active
1058-
// context's core.
1059-
if owner, repo, ok := parseOwnerRepoRef(ref); ok {
1060-
// Not paged, so the writer runCoreList renders to is the
1061-
// final one — but cells are still pre-colored for consistency
1062-
// with the list view (same status palette, yellow headers).
1063-
st := newStatusStyles(cmd.OutOrStdout())
1064-
return runCoreList(cmd, "", styledHeaders(st, columnHeaders(mirrorGetColumns)), mirrorGetRowStyled(st), func(ctx context.Context, c *coreapi.Client) ([]coreapi.Mirror, error) {
1065-
return listRepoMirrors(ctx, c, owner, repo)
1066-
})
1055+
// list` NAME column: a record view of that repo — visibility,
1056+
// access, then its mirror on every cluster with the per-placement
1057+
// detail the list aggregates away (clone URL, per-cluster
1058+
// status). Like a ULID it carries no cluster coordinate, so it
1059+
// resolves on the active context's core.
1060+
if _, _, ok := parseOwnerRepoRef(ref); ok {
1061+
return runRepoMirrorGetByName(cmd, ref)
10671062
}
10681063
clusterHost, _, _, _, err := parseMirrorCloneURL(ref)
10691064
if err != nil {
@@ -1077,35 +1072,108 @@ func newRepoMirrorGetCmd() *cobra.Command {
10771072
return cmd
10781073
}
10791074

1080-
// mirrorGetColumns is the per-cluster view `get <owner/repo>` renders: one row
1081-
// per mirror of the repo, the clone URL you'd copy, and each mirror's clone
1082-
// lifecycle status — exactly the placement detail the grouped `mirror list`
1083-
// row aggregates away.
1084-
var mirrorGetColumns = []column{colName, colCloneURL, colPrivate, colStatus}
1075+
// runRepoMirrorGetByName renders the record view behind `get <owner/repo>`:
1076+
// the repo's identity fields (visibility, access), then its mirror placements
1077+
// as a cluster/clone-URL/status table. One exact-match /repos?filter= lookup
1078+
// (the endpoint returns that repo's zero-or-one entries; no directory walk)
1079+
// plus the cluster catalog for clone-URL synthesis. A candidate entry renders
1080+
// its access and availability instead of a placements table — though today's
1081+
// control plane only matches onboarded repos in the filter, so that path
1082+
// waits on the server (the not-found error points at `list --available`).
1083+
// --json emits the same repoDirRow shape `list --json` uses, placements
1084+
// nested.
1085+
func runRepoMirrorGetByName(cmd *cobra.Command, ref string) error {
1086+
return runCore(cmd, func(ctx context.Context, c *coreapi.Client) error {
1087+
out, err := c.ListRepos(ctx, coreapi.ListReposParams{Filter: coreapi.NewOptString(ref)})
1088+
if err != nil {
1089+
return err
1090+
}
1091+
if len(out.Repos) == 0 {
1092+
// The filter only matches onboarded repos on today's control
1093+
// plane, so a not-yet-mirrored GitHub repo lands here too —
1094+
// point at the list mode that shows those.
1095+
return fmt.Errorf("no repo matching %q visible from your login (GitHub repos you could onboard: `entire repo mirror list --available`)", ref)
1096+
}
1097+
clusters, err := c.ListClusters(ctx)
1098+
if err != nil {
1099+
return err
1100+
}
1101+
row := mirrorRepoDetailRow(out.Repos[0], clusterHostBySlug(clusters.Clusters))
1102+
if jsonRequested(cmd) {
1103+
return printJSON(cmd.OutOrStdout(), row)
1104+
}
1105+
renderRepoDetail(cmd.OutOrStdout(), row)
1106+
return nil
1107+
})
1108+
}
10851109

1086-
func mirrorGetRow(m coreapi.Mirror) []string {
1087-
return append(mirrorRow(m), orDash(string(m.Status.Or(""))))
1110+
// mirrorRepoDetailRow shapes one directory entry for the record view, reusing the
1111+
// list's row builder so both views agree on placement/candidate semantics.
1112+
// buildRepoDir drops a repo with no GitHub-mirror placements (a native
1113+
// `entire repo create` repo); the detail view was asked about that repo by
1114+
// name, so it falls back to a bare identity row instead of vanishing.
1115+
// Placements are ordered by cluster slug for a deterministic table.
1116+
func mirrorRepoDetailRow(e coreapi.RepoIndexEntry, hostBySlug map[string]string) repoDirRow {
1117+
rows := buildRepoDir([]coreapi.RepoIndexEntry{e}, hostBySlug)
1118+
if len(rows) == 0 {
1119+
name := e.FullName
1120+
if name == "" {
1121+
name = e.Name
1122+
}
1123+
return repoDirRow{Repo: name, Private: strings.EqualFold(e.Visibility, "private")}
1124+
}
1125+
row := rows[0]
1126+
slices.SortFunc(row.Placements, func(a, b repoDirPlacement) int {
1127+
return cmp.Compare(a.Cluster, b.Cluster)
1128+
})
1129+
return row
10881130
}
10891131

1090-
// mirrorGetRowStyled colors the per-cluster rows the way the list view does:
1091-
// private gray, status by lifecycle, and the CLONE URL cyan — it is the
1092-
// payload of this view (the one thing `get` shows that the grouped list
1093-
// doesn't), and a cell left unstyled here would instead pick up printTable's
1094-
// muted secondary-column gray, reading as de-emphasized. Cyan also matches
1095-
// the list's cluster accent, and the URL is the cluster-bearing value.
1096-
func mirrorGetRowStyled(st statusStyles) func(coreapi.Mirror) []string {
1097-
return func(m coreapi.Mirror) []string {
1098-
cells := mirrorGetRow(m)
1099-
if !st.colorEnabled {
1100-
return cells
1132+
// renderRepoDetail prints the `get <owner/repo>` record: bold repo name,
1133+
// dim-labeled identity fields, then the placements table — cluster cyan,
1134+
// clone URL the default foreground (it is the payload of this view), status
1135+
// by lifecycle. A candidate (no placements, availability in Status) states
1136+
// its availability instead of an empty table; a native-only repo states it
1137+
// has no GitHub mirrors.
1138+
func renderRepoDetail(w io.Writer, row repoDirRow) {
1139+
st := newStatusStyles(w)
1140+
fmt.Fprintln(w, st.render(st.bold, row.Repo))
1141+
fields := []explainRow{{Label: "private", Value: yesNo(row.Private)}}
1142+
if row.Access != "" {
1143+
fields = append(fields, explainRow{Label: "access", Value: row.Access})
1144+
}
1145+
fmt.Fprint(w, st.metadataRows(fields))
1146+
fmt.Fprintln(w)
1147+
1148+
if len(row.Placements) == 0 {
1149+
if row.Status != "" {
1150+
fmt.Fprintf(w, "Not mirrored on any cluster (%s).\n", row.Status)
1151+
return
11011152
}
1102-
cells[1] = st.render(st.cyan, cells[1])
1103-
cells[2] = st.render(st.gray, cells[2])
1104-
if style, ok := repoStatusColor(st, cells[3]); ok {
1105-
cells[3] = st.render(style, cells[3])
1153+
fmt.Fprintln(w, "No GitHub mirror placements.")
1154+
return
1155+
}
1156+
1157+
headers := styledHeaders(st, []string{"CLUSTER", "CLONE URL", "STATUS"})
1158+
rows := make([][]string, len(row.Placements))
1159+
for i, p := range row.Placements {
1160+
cluster, status := p.Cluster, p.Status
1161+
if st.colorEnabled {
1162+
cluster = st.render(st.cyan, cluster)
1163+
if style, ok := repoStatusColor(st, status); ok {
1164+
status = st.render(style, status)
1165+
}
11061166
}
1107-
return cells
1167+
rows[i] = []string{cluster, orDash(p.CloneURL), status}
1168+
}
1169+
widths := columnWidths(headers, rows)
1170+
var b strings.Builder
1171+
plain := func(int) lipgloss.Style { return lipgloss.Style{} }
1172+
writeTableRow(&b, headers, widths, plain, tableStyles{})
1173+
for _, r := range rows {
1174+
writeTableRow(&b, r, widths, plain, tableStyles{})
11081175
}
1176+
fmt.Fprint(w, b.String())
11091177
}
11101178

11111179
// parseOwnerRepoRef reads a bare <owner>/<repo> mirror reference — the NAME
@@ -1123,43 +1191,6 @@ func parseOwnerRepoRef(ref string) (owner, repo string, ok bool) {
11231191
return owner, repo, true
11241192
}
11251193

1126-
// listRepoMirrors fetches every caller-visible mirror of owner/repo on the
1127-
// active context's core, ordered by cluster host for a deterministic table.
1128-
// No mirrors is an error, not an empty table: `get` names one repo, so
1129-
// nothing-found means the reference didn't resolve.
1130-
func listRepoMirrors(ctx context.Context, c *coreapi.Client, owner, repo string) ([]coreapi.Mirror, error) {
1131-
mirrors, err := fetchAllPages(ctx, func(ctx context.Context, cursor string) ([]coreapi.Mirror, string, error) {
1132-
params := coreapi.ListMirrorsParams{
1133-
Provider: coreapi.NewOptString("github"),
1134-
Owner: coreapi.NewOptString(owner),
1135-
}
1136-
if cursor != "" {
1137-
params.PageToken = coreapi.NewOptString(cursor)
1138-
}
1139-
out, lerr := c.ListMirrors(ctx, params)
1140-
if lerr != nil {
1141-
return nil, "", lerr
1142-
}
1143-
return out.Mirrors, out.NextPageToken.Or(""), nil
1144-
})
1145-
if err != nil {
1146-
return nil, err
1147-
}
1148-
// ListMirrors has no repo filter, so the owner-scoped result is matched
1149-
// on repo client-side; owner/repo are stored lowercase, EqualFold guards
1150-
// a differently-cased argument.
1151-
mirrors = slices.DeleteFunc(mirrors, func(m coreapi.Mirror) bool {
1152-
return !strings.EqualFold(m.Repo, repo)
1153-
})
1154-
if len(mirrors) == 0 {
1155-
return nil, noMirrorErr(owner + "/" + repo)
1156-
}
1157-
slices.SortFunc(mirrors, func(a, b coreapi.Mirror) int {
1158-
return cmp.Compare(a.ClusterHost, b.ClusterHost)
1159-
})
1160-
return mirrors, nil
1161-
}
1162-
11631194
// resolveMirrorRef turns a mirror reference into its ULID. A ULID passes
11641195
// through unchanged. Otherwise the ref is parsed as an entire:// clone URL and
11651196
// resolved by listing the caller-visible mirrors for that (cluster, provider,

cmd/entire/cli/repo_mirror_test.go

Lines changed: 70 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1311,13 +1311,13 @@ func TestRepoMirrorGet_Routing(t *testing.T) {
13111311
clusterCoreClient = fn
13121312
t.Cleanup(func() { clusterCoreClient = prev })
13131313
}
1314-
runGet := func(t *testing.T, ref string) (string, error) {
1314+
runGet := func(t *testing.T, args ...string) (string, error) {
13151315
t.Helper()
13161316
cmd := newRepoCmd()
13171317
var out, errW bytes.Buffer
13181318
cmd.SetOut(&out)
13191319
cmd.SetErr(&errW)
1320-
cmd.SetArgs([]string{"mirror", "get", ref})
1320+
cmd.SetArgs(append([]string{"mirror", "get"}, args...))
13211321
err := cmd.ExecuteContext(t.Context())
13221322
return out.String(), err
13231323
}
@@ -1368,24 +1368,23 @@ func TestRepoMirrorGet_Routing(t *testing.T) {
13681368
require.ErrorContains(t, err, "pass a mirror ULID or a clone URL")
13691369
})
13701370

1371-
t.Run("owner/repo lists every cluster mirror with clone URL and status", func(t *testing.T) {
1372-
// The drill-down from the grouped `mirror list` NAME cell: one row per
1373-
// cluster mirror, per-cluster clone URL + status, deterministic
1374-
// (cluster-host) order. The owner narrowing is server-side; the repo
1375-
// match is client-side (ListMirrors has no repo filter), so the
1376-
// sibling repo must not leak in.
1377-
var gotOwner, gotProvider string
1371+
// serveRepoDetail answers the two endpoints the owner/repo form uses: the
1372+
// exact-match /repos?filter= lookup and the cluster catalog.
1373+
serveRepoDetail := func(t *testing.T, repos []coreapi.RepoIndexEntry, clusters []coreapi.Cluster) *string {
1374+
t.Helper()
1375+
var gotFilter string
13781376
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
13791377
w.Header().Set("Content-Type", "application/json")
1380-
assert.Equal(t, mirrorsAPIPath, r.URL.Path)
1381-
gotOwner, gotProvider = r.URL.Query().Get("owner"), r.URL.Query().Get("provider")
1382-
assert.NoError(t, printJSON(w, &coreapi.ListMirrorsOutputBody{Mirrors: []coreapi.Mirror{
1383-
{MirrorId: "M2", Owner: "entirehq", Repo: "entiredb", ClusterHost: "eu-west-1.entire.io",
1384-
IsPrivate: coreapi.NewOptBool(true), Status: coreapi.NewOptMirrorStatus(coreapi.MirrorStatusFailed)},
1385-
{MirrorId: "M3", Owner: "entirehq", Repo: "other", ClusterHost: "aws-us-east-2.entire.io"},
1386-
{MirrorId: "M1", Owner: "entirehq", Repo: "entiredb", ClusterHost: "aws-us-east-2.entire.io",
1387-
IsPrivate: coreapi.NewOptBool(true), Status: coreapi.NewOptMirrorStatus(coreapi.MirrorStatusReady)},
1388-
}}))
1378+
switch r.URL.Path {
1379+
case testClustersPath:
1380+
assert.NoError(t, printJSON(w, &coreapi.ListClustersOutputBody{Clusters: clusters}))
1381+
case testReposPath:
1382+
gotFilter = r.URL.Query().Get("filter")
1383+
assert.NoError(t, printJSON(w, &coreapi.ListReposOutputBody{Repos: repos}))
1384+
default:
1385+
t.Errorf("unexpected path %q", r.URL.Path)
1386+
w.WriteHeader(http.StatusNotFound)
1387+
}
13891388
}))
13901389
t.Cleanup(srv.Close)
13911390
seamActive(t, func(context.Context) (*coreapi.Client, error) {
@@ -1395,30 +1394,68 @@ func TestRepoMirrorGet_Routing(t *testing.T) {
13951394
t.Errorf("owner/repo get dialed cluster core %q; it has no cluster coordinate", host)
13961395
return nil, errors.New("wrong core")
13971396
})
1397+
return &gotFilter
1398+
}
1399+
detailClusters := []coreapi.Cluster{
1400+
{Slug: "us", PublicUrl: "https://aws-us-east-2.entire.io"},
1401+
{Slug: "eu", PublicUrl: "https://eu-west-1.entire.io"},
1402+
}
1403+
1404+
t.Run("owner/repo renders the record view with a per-cluster table", func(t *testing.T) {
1405+
// The drill-down from the grouped `mirror list` NAME cell: identity
1406+
// fields, then one row per cluster mirror with clone URL + status,
1407+
// deterministic (cluster-slug) order — the entry delivers eu-first.
1408+
gotFilter := serveRepoDetail(t, []coreapi.RepoIndexEntry{
1409+
{FullName: "entirehq/entiredb", Visibility: "private", Placements: []coreapi.RepoPlacement{
1410+
{ClusterSlug: "eu", Status: coreapi.RepoPlacementStatusFailed, Mirror: true},
1411+
{ClusterSlug: "us", Status: coreapi.RepoPlacementStatusReady, Mirror: true},
1412+
}},
1413+
}, detailClusters)
13981414

13991415
out, err := runGet(t, "entirehq/entiredb")
14001416
require.NoError(t, err)
1401-
require.Equal(t, "entirehq", gotOwner, "owner narrowing must be server-side")
1402-
require.Equal(t, "github", gotProvider)
1417+
require.Equal(t, "entirehq/entiredb", *gotFilter, "the lookup must be the server-side exact-match filter")
14031418
requireOrder(t, out,
1404-
"entire://aws-us-east-2.entire.io/gh/entirehq/entiredb", "ready",
1405-
"entire://eu-west-1.entire.io/gh/entirehq/entiredb", "failed",
1419+
"entirehq/entiredb",
1420+
"private", "yes",
1421+
"CLUSTER", "CLONE URL", "STATUS",
1422+
"eu", "entire://eu-west-1.entire.io/gh/entirehq/entiredb", "failed",
1423+
"us", "entire://aws-us-east-2.entire.io/gh/entirehq/entiredb", "ready",
14061424
)
1407-
require.NotContains(t, out, "entirehq/other", "the sibling repo must not leak into the match")
14081425
})
14091426

1410-
t.Run("owner/repo with no mirrors is a friendly error", func(t *testing.T) {
1411-
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
1412-
w.Header().Set("Content-Type", "application/json")
1413-
assert.NoError(t, printJSON(w, &coreapi.ListMirrorsOutputBody{}))
1414-
}))
1415-
t.Cleanup(srv.Close)
1416-
seamActive(t, func(context.Context) (*coreapi.Client, error) {
1417-
return coreapi.NewWithBearer(srv.URL, "tok")
1418-
})
1427+
t.Run("owner/repo on a candidate shows access and availability, no table", func(t *testing.T) {
1428+
serveRepoDetail(t, []coreapi.RepoIndexEntry{
1429+
candidateEntry("entirehq/notyet", "private", coreapi.RepoCandidateAccessWrite, true),
1430+
}, detailClusters)
1431+
1432+
out, err := runGet(t, "entirehq/notyet")
1433+
require.NoError(t, err)
1434+
requireOrder(t, out, "entirehq/notyet", "private", "yes", "access", "write", "Not mirrored on any cluster (available).")
1435+
require.NotContains(t, out, "CLONE URL", "a candidate has no placements table")
1436+
})
1437+
1438+
t.Run("owner/repo --json emits the list's row shape, placements nested", func(t *testing.T) {
1439+
serveRepoDetail(t, []coreapi.RepoIndexEntry{
1440+
{FullName: "entirehq/entiredb", Visibility: "private", Placements: []coreapi.RepoPlacement{
1441+
{ClusterSlug: "us", Status: coreapi.RepoPlacementStatusReady, Mirror: true},
1442+
}},
1443+
}, detailClusters)
1444+
1445+
out, err := runGet(t, "entirehq/entiredb", "--json")
1446+
require.NoError(t, err)
1447+
var row repoDirRow
1448+
require.NoError(t, json.Unmarshal([]byte(out), &row))
1449+
require.Equal(t, repoDirRow{Repo: "entirehq/entiredb", Private: true, Status: "ready", Placements: []repoDirPlacement{
1450+
{Cluster: "us", Status: "ready", CloneURL: "entire://aws-us-east-2.entire.io/gh/entirehq/entiredb"},
1451+
}}, row)
1452+
})
1453+
1454+
t.Run("owner/repo with no matching repo is a friendly error", func(t *testing.T) {
1455+
serveRepoDetail(t, nil, detailClusters)
14191456
_, err := runGet(t, "entirehq/ghost")
14201457
require.Error(t, err)
1421-
require.ErrorContains(t, err, "no mirror matching")
1458+
require.ErrorContains(t, err, "no repo matching")
14221459
})
14231460
}
14241461

@@ -1551,9 +1588,6 @@ func TestStyledCellsDisabledGate(t *testing.T) {
15511588

15521589
headers := columnHeaders(repoDirColumns)
15531590
require.Equal(t, headers, styledHeaders(st, headers))
1554-
1555-
m := coreapi.Mirror{Owner: "acme", Repo: "web", ClusterHost: "h", Status: coreapi.NewOptMirrorStatus(coreapi.MirrorStatusReady)}
1556-
require.Equal(t, mirrorGetRow(m), mirrorGetRowStyled(st)(m))
15571591
}
15581592

15591593
func TestClusterHostBySlug(t *testing.T) {

0 commit comments

Comments
 (0)