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

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 ...
`