Merge pull request #1679 from entireio/repo-mirror-list-name-column · Entire
Merge pull request #1679 from entireio/repo-mirror-list-name-column
931bfb6→main·
Soph·1w ago·2 files·+112 added/-78 removed
repo mirror list: rename REPO column and --repo flag to NAME
Changes
2
cmd/entire/cli
Mrepo_mirror.go+72/-48
Mrepo_mirror_test.go+40/-30
17 unmodified lines
// Column header names, the single source of truth for both the table headers
// (mirrorColumns/availableMirrorColumns) and the --sort key switches. parseSort-
// Column returns the canonical header it matched, so the sort switches compare
// against these constants directly.
const (
colRepo = "REPO"
colCloneURL = "CLONE URL"
colPrivate = "PRIVATE"
colAccess = "ACCESS"
colStatus = "STATUS"
// column is a table column with two separable identities: key is the canonical
// name a caller types for --sort (and the value parseSortColumn returns, so the
// sort switches compare against these constants directly); header is the text
// shown in the table. They differ only where the header carries a display hint
// the sort key shouldn't — e.g. NAME's inline "(owner/repo)" — which keeps
// --sort matching a simple equality on key with no header parsing.
type column struct {
key string
header string
}
// Keys are lower-case, single shell tokens (kebab-case for multi-word columns)
// so `--sort clone-url` needs no quoting; headers stay upper-case display text.
var (
colName = column{key: "name", header: "NAME (owner/repo)"}
colCloneURL = column{key: "clone-url", header: "CLONE URL"}
colPrivate = column{key: "private", header: "PRIVATE"}
colAccess = column{key: "access", header: "ACCESS"}
colStatus = column{key: "status", header: "STATUS"}
)
// columnHeaders is the display-header view of a column set, for the table/field
// renderers (runCoreList/runCoreObject) which take plain header strings.
func columnHeaders(cols []column) []string {
h := make([]string, len(cols))
for i, c := range cols {
h[i] = c.header
}
return h
}
// mirrorColumns is the human table/field view of a mirror: the scannable
// owner/repo name, the clone URL you'd copy, and whether the upstream is
// private. Owner, provider, and cluster aren't columns of their own — they're
// inferable from the owner/repo pair and the clone URL
// (entire://<cluster>/gh/<owner>/<repo>). `--repo` filters on the repo name
// only; owner/provider/cluster stay server-side filters, and the wire model's
// internal ids are dropped. The clone URL is synthesised from the mirror's
// coords (the form `git clone` accepts), since the list API doesn't return it.
var mirrorColumns = []string{colRepo, colCloneURL, colPrivate}
// (entire://<cluster>/gh/<owner>/<repo>). `--name` filters on the owner/repo
// name only; owner/provider/cluster stay server-side filters, and the wire
// model's internal ids are dropped. The clone URL is synthesised from the
// mirror's coords (the form `git clone` accepts), since the list API doesn't
// return it.
var mirrorColumns = []column{colName, colCloneURL, colPrivate}
// mirrorPrivate renders the PRIVATE column ("yes"/"no"), shared by the table
// row and the --sort private key so both agree on the cell value.
return []string{repo, cloneURL, mirrorPrivate(m)}
}
// parseSortColumn resolves a --sort spec to the canonical column header it
// names (one of the columns entries) and a direction. It trims first, then
// reads the '-' prefix, so leading/trailing whitespace is handled identically
// on every path (the direction and the column name never disagree). An empty
// spec selects the first column. An unknown name errors naming the valid
// columns. Returning the matched header lets callers switch on the col*
// constants directly.
func parseSortColumn(spec string, columns []string) (col string, desc bool, err error) {
// parseSortColumn resolves a --sort spec to the column it names and a
direction. It trims first, then reads the '-' prefix, so leading/trailing
whitespace is handled identically on every path (the direction and the column
name never disagree). An empty spec selects the first column. A spec matches a
column by its key (case-insensitive) — a plain equality, since key holds no
display hint. An unknown name errors naming the valid keys. Returning the
matched column lets callers switch on the col* constants directly.
func parseSortColumn(spec string, columns []column) (col column, desc bool, err error) {
spec = strings.TrimSpace(spec)
desc = strings.HasPrefix(spec, "-")
name := strings.TrimSpace(strings.TrimPrefix(spec, "-"))
if name == "" {
return columns[0], desc, nil
}
for _, h := range columns {
if strings.EqualFold(h, name) {
return h, desc, nil
}
for _, c := range columns {
if strings.EqualFold(c.key, name) {
return c, desc, nil
}
}
return "", false, fmt.Errorf("unknown sort column %q; valid columns: %s", name, strings.ToLower(strings.Join(columns, ", ")))
valid := make([]string, len(columns))
for i, c := range columns {
valid[i] = c.key
}
return column{}, false, fmt.Errorf("unknown sort column %q; valid columns: %s", name, strings.Join(valid, ", "))
}
// sortMirrors orders mirrors in place by the --sort spec: by the named column's
// value ascending (case-insensitive), always breaking ties by owner/repo then
// cluster host so a repo mirrored across clusters (or rows equal on any other
// column) has a stable, deterministic order rather than arbitrary server order.
// A '-' prefix reverses the whole ordering. `repo`/default sorts by the
// A '-' prefix reverses the whole ordering. `name`/default sorts by the
tiebreak alone.
func sortMirrors(mirrors []coreapi.Mirror, spec string) error {
col, desc, err := parseSortColumn(spec, mirrorColumns)
}
// filterByRepo keeps items whose repo identifier contains substr (case-
// filterByName keeps items whose owner/repo name contains substr (case-
// insensitive). The control plane already filters by owner/provider/cluster
// server-side but not by repo name, so `repo mirror list --repo` narrows that
// last dimension client-side. repoOf returns the item's displayed identifier —
// the callers pass the owner/repo form shown in the REPO column, so a value
// copied from the table (e.g. acme/web) matches the row it came from. An empty
// substr returns items unchanged.
func filterByRepo[T any](items []T, repoOf func(T) string, substr string) []T {
// server-side but not by name, so `repo mirror list --name` narrows that last
// dimension client-side. nameOf returns the item's displayed identifier — the
// callers pass the owner/repo form shown in the NAME column, so a value
// copied from the table (e.g. acme/web) matches the row it came from. An empty substr returns items unchanged.
func filterByName[T any](items []T, nameOf func(T) string, substr string) []T {
substr = strings.TrimSpace(substr)
if substr == "" {
return items
}
substr = strings.ToLower(substr)
out := make([]T, 0, len(items))
for _, it := range items {
if strings.Contains(strings.ToLower(repoOf(it)), substr) {
if strings.Contains(strings.ToLower(nameOf(it)), substr) {
out = append(out, it)
}
}
}
// clone URL), or "owner-only" (a personal repo of another user; only its
// owner may mirror it). No clone URL column: an un-onboarded repo doesn't
// have one yet.
var availableMirrorColumns = []string{colRepo, colAccess, colStatus}
var availableMirrorColumns = []column{colName, colAccess, colStatus}
func availableMirrorRow(m coreapi.AvailableMirror) []string {
return []string{m.Owner + "/" + m.Repo, string(m.Access), string(m.Status)}
}
}
func newRepoMirrorListCmd() *cobra.Command {
var cluster, provider, owner, repo string
var cluster, provider, owner, name string
var sortSpec string
var showAvailable bool
cmd := &cobra.Command{
RunE: func(cmd *cobra.Command, _ []string) error {
if showAvailable {
return runCoreList(cmd, "No repos available to mirror.", availableMirrorColumns, availableMirrorRow, func(ctx context.Context, c *coreapi.Client) ([]coreapi.AvailableMirror, error) {
return runCoreList(cmd, "No repos available to mirror.", columnHeaders(availableMirrorColumns), availableMirrorRow, func(ctx context.Context, c *coreapi.Client) ([]coreapi.AvailableMirror, error) {
// Computed live from GitHub using your own login, so name the
// core being dialled (same rationale as the existing-mirror
// banner). --cluster/--provider don't apply here: the
if err != nil {
return nil, err
}
avail := filterByRepo(out.Available, func(m coreapi.AvailableMirror) string { return m.Owner + "/" + m.Repo }, repo)
avail := filterByName(out.Available, func(m coreapi.AvailableMirror) string { return m.Owner + "/" + m.Repo }, name)
if err := sortAvailable(avail, sortSpec); err != nil {
return nil, err
}
return avail, nil
})
}
}
return runCoreList(cmd, "No mirrors found.", mirrorColumns, mirrorRow, func(ctx context.Context, c *coreapi.Client) ([]coreapi.Mirror, error) {
return runCoreList(cmd, "No mirrors found.", columnHeaders(mirrorColumns), mirrorRow, func(ctx context.Context, c *coreapi.Client) ([]coreapi.Mirror, error) {
// mirror list is identity-scoped: it shows the mirrors visible
// from the active login's federation, so naming that login server
// makes a surprising empty result legible — e.g. mirrors in a
return nil, nil
})
// execMirrorList runs `list` under a parent that carries the control-plane
// persistent flags (--json lives there, not on the list command itself), so
// tests can exercise --json and the client-side --repo/--sort together.
// tests can exercise --json and the client-side --name/--sort together.
func execMirrorList(t *testing.T, args ...string) (stdout, stderr string, err error) {
t.Helper()
parent := &cobra.Command{Use: "mirror"}
}
// TestRepoMirrorList_FilterSort pins the client-side --repo filter and --sort
// TestRepoMirrorList_FilterSort pins the client-side --name filter and --sort
// applied to `repo mirror list` before rendering (server handles
// owner/provider/cluster), so they shape both the table and --json output and
// work under --show-available.
// TODO: Implement functionality
// ...
// Filter repo by substring name matching.
// Filter name by Substring matching.
// ... implementation continues ...
`