@@ -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,
0 commit comments