From 38e469055107e7dc3afdfaaf275bfeeb7253d9d3 Mon Sep 17 00:00:00 2001 From: Marko Bevc Date: Wed, 8 Jul 2026 17:54:00 +0100 Subject: [PATCH 1/7] docs(repos): add usage examples to list repos Other `kosli list *` reference pages render an "Examples Use Cases" section from the command's Example field; list repos had none. Add examples covering the default listing, `--name` and `--provider` filters, and pagination, matching the style of `list flows` / `list environments`. --- cmd/kosli/listRepos.go | 36 ++++++++++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/cmd/kosli/listRepos.go b/cmd/kosli/listRepos.go index b92226f83..03915140f 100644 --- a/cmd/kosli/listRepos.go +++ b/cmd/kosli/listRepos.go @@ -14,6 +14,33 @@ import ( const listReposDesc = `List repos for an org.` +const listReposExample = ` +# list all repos for an org: +kosli list repos \ + --api-token yourAPIToken \ + --org yourOrgName + +# list repos filtered by 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 (15 per page): +kosli list repos \ + --page-limit 15 \ + --page 2 \ + --api-token yourAPIToken \ + --org yourOrgName +` + type listReposOptions struct { listOptions name string @@ -24,10 +51,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: listReposDesc, + Long: listReposDesc, + Example: listReposExample, + Args: cobra.NoArgs, PreRunE: func(cmd *cobra.Command, args []string) error { err := RequireGlobalFlags(global, []string{"Org", "ApiToken"}) if err != nil { From ad5a8a60192f50d235b795b3743df8256316fdf5 Mon Sep 17 00:00:00 2001 From: Marko Bevc Date: Wed, 8 Jul 2026 18:05:20 +0100 Subject: [PATCH 2/7] docs(repo): add long description --- cmd/kosli/listRepos.go | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/cmd/kosli/listRepos.go b/cmd/kosli/listRepos.go index 03915140f..5863faf0e 100644 --- a/cmd/kosli/listRepos.go +++ b/cmd/kosli/listRepos.go @@ -12,7 +12,12 @@ import ( "github.com/spf13/cobra" ) -const listReposDesc = `List repos for an org.` +const listReposShortDesc = `List repos for an org. ` + +const listReposLongDesc = listReposShortDesc + `By default, all repos for the org are returned. +Pass --page-limit and/or --page to paginate the results. +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 all repos for an org: @@ -20,7 +25,7 @@ kosli list repos \ --api-token yourAPIToken \ --org yourOrgName -# list repos filtered by name: +# list repos filtered by name (exact match on the full repo name): kosli list repos \ --name my-org/my-repo \ --api-token yourAPIToken \ @@ -52,8 +57,8 @@ func newListReposCmd(out io.Writer) *cobra.Command { o := new(listReposOptions) cmd := &cobra.Command{ Use: "repos", - Short: listReposDesc, - Long: listReposDesc, + Short: listReposShortDesc, + Long: listReposLongDesc, Example: listReposExample, Args: cobra.NoArgs, PreRunE: func(cmd *cobra.Command, args []string) error { @@ -69,7 +74,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.") From 8186c43cff9548803efb46e6047c2c6dfdf3ce7c Mon Sep 17 00:00:00 2001 From: Marko Bevc Date: Wed, 8 Jul 2026 18:16:01 +0100 Subject: [PATCH 3/7] docs(repo): update example pagination --- cmd/kosli/listRepos.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/kosli/listRepos.go b/cmd/kosli/listRepos.go index 5863faf0e..c1e277705 100644 --- a/cmd/kosli/listRepos.go +++ b/cmd/kosli/listRepos.go @@ -38,9 +38,9 @@ kosli list repos \ --org yourOrgName \ --output json -# show the second page of repos (15 per page): +# show the second page of repos (25 per page): kosli list repos \ - --page-limit 15 \ + --page-limit 25 \ --page 2 \ --api-token yourAPIToken \ --org yourOrgName From 78959c9f8376b671f3307cb44c6563e2bd146f7b Mon Sep 17 00:00:00 2001 From: Marko Bevc Date: Wed, 8 Jul 2026 22:13:40 +0100 Subject: [PATCH 4/7] feat(repos): accurate pagination docs and footer for list repos The list repos endpoint always paginates (page 1, 15 per page by default, max 50) with no unpaginated "return all" mode, so correct the long description and example accordingly. Surface the page context in table output with a "Showing page X of Y, total Z repos" footer from the response envelope, mirroring list controls/environments/flows, and make the no-repos message page-aware. Covered by a server-free unit test plus an integration assertion. --- cmd/kosli/listRepos.go | 41 ++++++++++++++++++++++--------------- cmd/kosli/listRepos_test.go | 8 +++++++- 2 files changed, 32 insertions(+), 17 deletions(-) diff --git a/cmd/kosli/listRepos.go b/cmd/kosli/listRepos.go index c1e277705..9f74ebfc1 100644 --- a/cmd/kosli/listRepos.go +++ b/cmd/kosli/listRepos.go @@ -14,13 +14,14 @@ import ( const listReposShortDesc = `List repos for an org. ` -const listReposLongDesc = listReposShortDesc + `By default, all repos for the org are returned. -Pass --page-limit and/or --page to paginate the results. +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 all repos for an org: +# list repos for an org (first page, 15 per page): kosli list repos \ --api-token yourAPIToken \ --org yourOrgName @@ -38,9 +39,9 @@ kosli list repos \ --org yourOrgName \ --output json -# show the second page of repos (25 per page): +# show the second page of repos (15 per page): kosli list repos \ - --page-limit 25 \ + --page-limit 15 \ --page 2 \ --api-token yourAPIToken \ --org yourOrgName @@ -117,29 +118,37 @@ 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.") + if len(response.Repos) == 0 { + msg := "No repos were found" + if page != 1 { + msg = fmt.Sprintf("%s at page number %d", msg, 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..25c29fe7c 100644 --- a/cmd/kosli/listRepos_test.go +++ b/cmd/kosli/listRepos_test.go @@ -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) } From e126a06ecf7a9cda0572c70a91462dd46e986d1d Mon Sep 17 00:00:00 2001 From: Marko Bevc Date: Wed, 8 Jul 2026 22:21:06 +0100 Subject: [PATCH 5/7] docs(repos): use a non-default page size in the pagination example `--page-limit 15` equalled the default, making the flag look like a no-op; use 25 (matching the sibling commands' examples) so the flag's effect is clear, and update the comment to match. --- cmd/kosli/listRepos.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/kosli/listRepos.go b/cmd/kosli/listRepos.go index 9f74ebfc1..f17eb7d77 100644 --- a/cmd/kosli/listRepos.go +++ b/cmd/kosli/listRepos.go @@ -39,9 +39,9 @@ kosli list repos \ --org yourOrgName \ --output json -# show the second page of repos (15 per page): +# show the second page of repos (25 per page): kosli list repos \ - --page-limit 15 \ + --page-limit 25 \ --page 2 \ --api-token yourAPIToken \ --org yourOrgName From a0a50c033479e279b55e2ecc75e6fc5fdd802ac8 Mon Sep 17 00:00:00 2001 From: Marko Bevc Date: Wed, 8 Jul 2026 22:26:22 +0100 Subject: [PATCH 6/7] test(repos): use a non-default page size in the pagination test case `--page-limit 15` equalled the default, so the value demonstrated nothing distinct from an unparameterised call; use 25 to match the example. --- cmd/kosli/listRepos_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/kosli/listRepos_test.go b/cmd/kosli/listRepos_test.go index 25c29fe7c..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: "", }, { From 615c8e5ab75249fc621ab126806d571ef98067dd Mon Sep 17 00:00:00 2001 From: Marko Bevc Date: Wed, 8 Jul 2026 22:41:50 +0100 Subject: [PATCH 7/7] refactor(repos): read list page number from one source The empty-list message read the requested page argument while the footer read response.Page. They agree in practice, but sourcing both from the response envelope removes the divergence and keeps the page number consistent with the total_pages/total_count shown beside it. --- cmd/kosli/listRepos.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/cmd/kosli/listRepos.go b/cmd/kosli/listRepos.go index f17eb7d77..c7727e160 100644 --- a/cmd/kosli/listRepos.go +++ b/cmd/kosli/listRepos.go @@ -131,10 +131,13 @@ func printReposListAsTable(raw string, out io.Writer, page int) error { return err } + // 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 page != 1 { - msg = fmt.Sprintf("%s at page number %d", msg, page) + if response.Page != 1 { + msg = fmt.Sprintf("%s at page number %d", msg, response.Page) } logger.Info(msg + ".") return nil