diff --git a/cmd/kosli/listRepos.go b/cmd/kosli/listRepos.go index b92226f83..c7727e160 100644 --- a/cmd/kosli/listRepos.go +++ b/cmd/kosli/listRepos.go @@ -12,7 +12,40 @@ import ( "github.com/spf13/cobra" ) -const listReposDesc = `List repos for an org.` +const listReposShortDesc = `List repos for an org. ` + +const listReposLongDesc = listReposShortDesc + `The results are always paginated: +by default the first page is returned with 15 repos per page. Use --page to select +a page and --page-limit to change the page size (maximum 50). +The list can be filtered by name with --name (exact match), by VCS provider with +--provider, and by external repo ID with --repo-id.` + +const listReposExample = ` +# list repos for an org (first page, 15 per page): +kosli list repos \ + --api-token yourAPIToken \ + --org yourOrgName + +# list repos filtered by name (exact match on the full repo name): +kosli list repos \ + --name my-org/my-repo \ + --api-token yourAPIToken \ + --org yourOrgName + +# list repos filtered by VCS provider (in JSON): +kosli list repos \ + --provider github \ + --api-token yourAPIToken \ + --org yourOrgName \ + --output json + +# show the second page of repos (25 per page): +kosli list repos \ + --page-limit 25 \ + --page 2 \ + --api-token yourAPIToken \ + --org yourOrgName +` type listReposOptions struct { listOptions @@ -24,10 +57,11 @@ type listReposOptions struct { func newListReposCmd(out io.Writer) *cobra.Command { o := new(listReposOptions) cmd := &cobra.Command{ - Use: "repos", - Short: listReposDesc, - Long: listReposDesc, - Args: cobra.NoArgs, + Use: "repos", + Short: listReposShortDesc, + Long: listReposLongDesc, + Example: listReposExample, + Args: cobra.NoArgs, PreRunE: func(cmd *cobra.Command, args []string) error { err := RequireGlobalFlags(global, []string{"Org", "ApiToken"}) if err != nil { @@ -41,7 +75,7 @@ func newListReposCmd(out io.Writer) *cobra.Command { } addListFlags(cmd, &o.listOptions) - cmd.Flags().StringVar(&o.name, "name", "", "[optional] The repo name to filter by.") + cmd.Flags().StringVar(&o.name, "name", "", "[optional] The repo name to filter by (exact match).") cmd.Flags().StringVar(&o.provider, "provider", "", "[optional] The VCS provider to filter repos by (e.g. github, gitlab).") cmd.Flags().StringVar(&o.repoID, "repo-id", "", "[optional] The external repo ID to filter repos by.") @@ -84,29 +118,40 @@ func (o *listReposOptions) run(out io.Writer) error { }) } -func printReposListAsTable(raw string, out io.Writer, page int) error { - var repos []map[string]any - var response struct { - Repos []map[string]any `json:"repos"` - } +type listReposResponse struct { + Repos []map[string]any `json:"repos"` + Page int `json:"page"` + TotalPages int `json:"total_pages"` + TotalCount int `json:"total_count"` +} - err := json.Unmarshal([]byte(raw), &response) - if err != nil { +func printReposListAsTable(raw string, out io.Writer, page int) error { + response := &listReposResponse{} + if err := json.Unmarshal([]byte(raw), response); err != nil { return err } - repos = response.Repos - if len(repos) == 0 { - logger.Info("No repos were found.") + // both the empty-list message and the footer read the page from the + // response envelope (the server echoes the requested page), so the two + // paths never disagree on which page is being reported + if len(response.Repos) == 0 { + msg := "No repos were found" + if response.Page != 1 { + msg = fmt.Sprintf("%s at page number %d", msg, response.Page) + } + logger.Info(msg + ".") return nil } header := []string{"NAME", "URL", "PROVIDER", "TAGS"} rows := []string{} - for _, repo := range repos { + for _, repo := range response.Repos { row := fmt.Sprintf("%v\t%v\t%v\t%s", repo["name"], repo["url"], repo["provider"], formatRepoTags(repo["tags"])) rows = append(rows, row) } + + rows = append(rows, fmt.Sprintf("\nShowing page %d of %d, total %d repos", response.Page, response.TotalPages, response.TotalCount)) + tabFormattedPrint(out, header, rows) return nil diff --git a/cmd/kosli/listRepos_test.go b/cmd/kosli/listRepos_test.go index 41ecb7af4..0a29e410b 100644 --- a/cmd/kosli/listRepos_test.go +++ b/cmd/kosli/listRepos_test.go @@ -103,7 +103,7 @@ func (suite *ListReposCommandTestSuite) TestListReposCmd() { }, { name: "08-can list repos with pagination", - cmd: fmt.Sprintf(`list repos --page-limit 15 --page 2 %s`, suite.defaultKosliArguments), + cmd: fmt.Sprintf(`list repos --page-limit 25 --page 2 %s`, suite.defaultKosliArguments), golden: "", }, { @@ -136,6 +136,11 @@ func (suite *ListReposCommandTestSuite) TestListReposCmd() { cmd: fmt.Sprintf(`list repos %s`, suite.acmeOrgKosliArguments), goldenRegex: `(?m)^list-repos-suite-org/untagged-repo\s+https://github\.com/list-repos-suite-org/untagged-repo\s+github\s*$`, }, + { + name: "15-table output shows the pagination footer", + cmd: fmt.Sprintf(`list repos %s`, suite.acmeOrgKosliArguments), + goldenRegex: `(?m)^Showing page 1 of \d+, total \d+ repos$`, + }, } runTestCmd(suite.T(), tests) @@ -151,11 +156,12 @@ func TestPrintReposListAsTableRendersBlankTagsCell(t *testing.T) { raw := `{"repos":[ {"name":"o/tagged","url":"https://github.com/o/tagged","provider":"github","tags":{"team":"platform"}}, {"name":"o/untagged","url":"https://github.com/o/untagged","provider":"github","tags":{}} - ]}` + ],"page":1,"total_pages":3,"total_count":45}` var buf bytes.Buffer require.NoError(t, printReposListAsTable(raw, &buf, 1)) out := buf.String() require.Regexp(t, `(?m)^NAME\s+URL\s+PROVIDER\s+TAGS\s*$`, out) require.Regexp(t, `(?m)^o/tagged\s+https://github\.com/o/tagged\s+github\s+team=platform\s*$`, out) require.Regexp(t, `(?m)^o/untagged\s+https://github\.com/o/untagged\s+github\s*$`, out) + require.Regexp(t, `(?m)^Showing page 1 of 3, total 45 repos$`, out) }