detached 3a3918d

refactor(cli): drop repo create --private and share output and paging helpers

commit
3a3918d7434b25cde72b4e199b11d3f91fdf8a67
author
brice · · 2026-10-10 10:21 UTC
committer
brice · · 2026-10-10 11:17 UTC
parent
66b7417
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
4 files · +80 -103
M cmd/ermite/cli.go +46 -11
@@ -104,18 +104,53 @@ func writeJSON(w io.Writer, v any) error {
104104 return enc.Encode(v)
105105 }
106106
107-// clean makes a server-provided string safe for a terminal: control
108-// characters (escape sequences, line breaks) are replaced.
109-func clean(s string) string {
110- return strings.Map(func(r rune) rune {
111- switch {
112- case r == '\t' || r == '\n' || r == '\r':
113- return ' '
114- case r < 0x20 || r == 0x7f || (r >= 0x80 && r < 0xa0):
115- return '?'
107+// clean makes a server-provided string safe for a terminal.
108+func clean(s string) string { return client.Clean(s) }
109+
110+// collect reads every page: fetch gets the page token and returns the items
111+// and the next page token ("" at the end).
112+func collect[T any](ctx context.Context, fetch func(token string) ([]T, string, error)) ([]T, error) {
113+ var all []T
114+ token := ""
115+ for {
116+ if err := ctx.Err(); err != nil {
117+ return nil, err
116118 }
117- return r
118- }, s)
119+ items, next, err := fetch(token)
120+ if err != nil {
121+ return nil, err
122+ }
123+ all = append(all, items...)
124+ if next == "" {
125+ return all, nil
126+ }
127+ token = next
128+ }
129+}
130+
131+// complete builds a shell completion: it opens a client (silently giving up
132+// on any failure), lists with a 3 s timeout and keeps the entries whose
133+// first element starts with the prefix.
134+func complete[T any](env *cliEnv, cmd *cobra.Command, prefix string,
135+ list func(context.Context, *client.Client) ([]T, error), entry func(T) (key, text string),
136+) []string {
137+ c, err := env.client(cmd, false)
138+ if err != nil {
139+ return nil
140+ }
141+ ctx, cancel := context.WithTimeout(env.ctx(cmd), 3*time.Second)
142+ defer cancel()
143+ items, err := list(ctx, c)
144+ if err != nil {
145+ return nil
146+ }
147+ var out []string
148+ for _, it := range items {
149+ if k, text := entry(it); strings.HasPrefix(k, prefix) {
150+ out = append(out, k+"\t"+text)
151+ }
152+ }
153+ return out
119154 }
120155
121156 // table prints aligned columns under upper-case headers.
@@ -104,18 +104,53 @@ func writeJSON(w io.Writer, v any) error {
104 return enc.Encode(v)104 return enc.Encode(v)
105 }105 }
106 106
107-// clean makes a server-provided string safe for a terminal: control107+// clean makes a server-provided string safe for a terminal.
108-// characters (escape sequences, line breaks) are replaced.108+func clean(s string) string { return client.Clean(s) }
109-func clean(s string) string {109+
110- return strings.Map(func(r rune) rune {110+// collect reads every page: fetch gets the page token and returns the items
111- switch {111+// and the next page token ("" at the end).
112- case r == '\t' || r == '\n' || r == '\r':112+func collect[T any](ctx context.Context, fetch func(token string) ([]T, string, error)) ([]T, error) {
113- return ' '113+ var all []T
114- case r < 0x20 || r == 0x7f || (r >= 0x80 && r < 0xa0):114+ token := ""
115- return '?'115+ for {
116+ if err := ctx.Err(); err != nil {
117+ return nil, err
116 }118 }
117- return r119+ items, next, err := fetch(token)
118- }, s)120+ if err != nil {
121+ return nil, err
122+ }
123+ all = append(all, items...)
124+ if next == "" {
125+ return all, nil
126+ }
127+ token = next
128+ }
129+}
130+
131+// complete builds a shell completion: it opens a client (silently giving up
132+// on any failure), lists with a 3 s timeout and keeps the entries whose
133+// first element starts with the prefix.
134+func complete[T any](env *cliEnv, cmd *cobra.Command, prefix string,
135+ list func(context.Context, *client.Client) ([]T, error), entry func(T) (key, text string),
136+) []string {
137+ c, err := env.client(cmd, false)
138+ if err != nil {
139+ return nil
140+ }
141+ ctx, cancel := context.WithTimeout(env.ctx(cmd), 3*time.Second)
142+ defer cancel()
143+ items, err := list(ctx, c)
144+ if err != nil {
145+ return nil
146+ }
147+ var out []string
148+ for _, it := range items {
149+ if k, text := entry(it); strings.HasPrefix(k, prefix) {
150+ out = append(out, k+"\t"+text)
151+ }
152+ }
153+ return out
119 }154 }
120 155
121 // table prints aligned columns under upper-case headers.156 // table prints aligned columns under upper-case headers.
M cmd/ermite/cli_repo.go +26 -58
@@ -9,7 +9,6 @@ import (
99 "fmt"
1010 "io"
1111 "strings"
12- "time"
1312
1413 "connectrpc.com/connect"
1514 "github.com/spf13/cobra"
@@ -87,19 +86,13 @@ func newRepoCmd(env *cliEnv) *cobra.Command {
8786
8887 // listRepos reads every page of repositories.
8988 func listRepos(ctx context.Context, c *client.Client) ([]*ermite.Repo, error) {
90- var repos []*ermite.Repo
91- token := ""
92- for {
89+ return collect(ctx, func(token string) ([]*ermite.Repo, string, error) {
9390 res, err := c.Repos().ListRepos(ctx, connect.NewRequest(&ermite.ListReposRequest{PageToken: token}))
9491 if err != nil {
95- return nil, c.Describe(err)
92+ return nil, "", c.Describe(err)
9693 }
97- repos = append(repos, res.Msg.GetRepos()...)
98- token = res.Msg.GetNextPageToken()
99- if token == "" {
100- return repos, nil
101- }
102- }
94+ return res.Msg.GetRepos(), res.Msg.GetNextPageToken(), nil
95+ })
10396 }
10497
10598 // completeRepoNames completes the first argument with repository names. Any
@@ -109,36 +102,28 @@ func completeRepoNames(env *cliEnv) func(*cobra.Command, []string, string) ([]st
109102 if len(args) > 0 {
110103 return nil, cobra.ShellCompDirectiveNoFileComp
111104 }
112- names, err := repoNames(env.ctx(cmd), env, cmd, toComplete)
113- if err != nil {
114- return nil, cobra.ShellCompDirectiveNoFileComp
115- }
116- return names, cobra.ShellCompDirectiveNoFileComp
105+ return repoNames(env, cmd, toComplete), cobra.ShellCompDirectiveNoFileComp
117106 }
118107 }
119108
120-func repoNames(ctx context.Context, env *cliEnv, cmd *cobra.Command, prefix string) ([]string, error) {
121- c, err := env.client(cmd, false)
122- if err != nil {
123- return nil, err
124- }
125- ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
126- defer cancel()
127- repos, err := listRepos(ctx, c)
128- if err != nil {
129- return nil, err
130- }
131- var names []string
132- for _, r := range repos {
133- if strings.HasPrefix(r.GetName(), prefix) {
134- desc := visibilityName(r.GetVisibility())
135- if d := clean(r.GetDescription()); d != "" {
136- desc += ", " + d
137- }
138- names = append(names, r.GetName()+"\t"+desc)
109+// repoNames lists the repository names starting with prefix, with their
110+// visibility and description as completion hints.
111+func repoNames(env *cliEnv, cmd *cobra.Command, prefix string) []string {
112+ return complete(env, cmd, prefix, listRepos, func(r *ermite.Repo) (string, string) {
113+ desc := visibilityName(r.GetVisibility())
114+ if d := clean(r.GetDescription()); d != "" {
115+ desc += ", " + d
139116 }
117+ return r.GetName(), desc
118+ })
119+}
120+
121+// outputRepo prints a repository as JSON or as fields.
122+func outputRepo(cmd *cobra.Command, r *ermite.Repo, asJSON bool) error {
123+ if asJSON {
124+ return writeJSON(cmd.OutOrStdout(), toJSONRepo(r))
140125 }
141- return names, nil
126+ return printRepo(cmd.OutOrStdout(), r)
142127 }
143128
144129 func newRepoListCmd(env *cliEnv) *cobra.Command {
@@ -214,10 +199,7 @@ func newRepoShowCmd(env *cliEnv) *cobra.Command {
214199 if err != nil {
215200 return c.Describe(err)
216201 }
217- if *asJSON {
218- return writeJSON(cmd.OutOrStdout(), toJSONRepo(res.Msg.GetRepo()))
219- }
220- return printRepo(cmd.OutOrStdout(), res.Msg.GetRepo())
202+ return outputRepo(cmd, res.Msg.GetRepo(), *asJSON)
221203 }
222204 return cmd
223205 }
@@ -226,7 +208,6 @@ func newRepoCreateCmd(env *cliEnv) *cobra.Command {
226208 var (
227209 description string
228210 visibility string
229- private bool
230211 )
231212 cmd := &cobra.Command{
232213 Use: "create <name>",
@@ -237,11 +218,10 @@ func newRepoCreateCmd(env *cliEnv) *cobra.Command {
237218 }
238219 cmd.Flags().StringVar(&description, "description", "", "one-line description")
239220 cmd.Flags().StringVar(&visibility, "visibility", visibilityPrivate, "visibility: public or private")
240- cmd.Flags().BoolVar(&private, "private", false, "create a private repository (the default; same as --visibility private)")
241221 _ = cmd.RegisterFlagCompletionFunc("visibility", visibilityCompletion)
242222 asJSON := addJSONFlag(cmd)
243223 cmd.RunE = func(cmd *cobra.Command, args []string) error {
244- req, err := buildCreateRepo(cmd, args[0], description, visibility, private)
224+ req, err := buildCreateRepo(args[0], description, visibility)
245225 if err != nil {
246226 return err
247227 }
@@ -253,10 +233,7 @@ func newRepoCreateCmd(env *cliEnv) *cobra.Command {
253233 if err != nil {
254234 return c.Describe(err)
255235 }
256- if *asJSON {
257- return writeJSON(cmd.OutOrStdout(), toJSONRepo(res.Msg.GetRepo()))
258- }
259- return printRepo(cmd.OutOrStdout(), res.Msg.GetRepo())
236+ return outputRepo(cmd, res.Msg.GetRepo(), *asJSON)
260237 }
261238 return cmd
262239 }
@@ -266,17 +243,11 @@ func visibilityCompletion(*cobra.Command, []string, string) ([]string, cobra.She
266243 }
267244
268245 // buildCreateRepo maps the flags of `repo create` to the request.
269-func buildCreateRepo(cmd *cobra.Command, name, description, visibility string, private bool) (*ermite.CreateRepoRequest, error) {
246+func buildCreateRepo(name, description, visibility string) (*ermite.CreateRepoRequest, error) {
270247 vis, err := parseVisibility(visibility)
271248 if err != nil {
272249 return nil, err
273250 }
274- if private {
275- if cmd.Flags().Changed("visibility") && vis != ermite.Visibility_VISIBILITY_PRIVATE {
276- return nil, errors.New("--private conflicts with --visibility " + visibilityPublic)
277- }
278- vis = ermite.Visibility_VISIBILITY_PRIVATE
279- }
280251 return &ermite.CreateRepoRequest{Name: name, Description: description, Visibility: vis}, nil
281252 }
282253
@@ -308,10 +279,7 @@ func newRepoEditCmd(env *cliEnv) *cobra.Command {
308279 if err != nil {
309280 return c.Describe(err)
310281 }
311- if *asJSON {
312- return writeJSON(cmd.OutOrStdout(), toJSONRepo(res.Msg.GetRepo()))
313- }
314- return printRepo(cmd.OutOrStdout(), res.Msg.GetRepo())
282+ return outputRepo(cmd, res.Msg.GetRepo(), *asJSON)
315283 }
316284 return cmd
317285 }
@@ -9,7 +9,6 @@ import (
9 "fmt"9 "fmt"
10 "io"10 "io"
11 "strings"11 "strings"
12- "time"
13 12
14 "connectrpc.com/connect"13 "connectrpc.com/connect"
15 "github.com/spf13/cobra"14 "github.com/spf13/cobra"
@@ -87,19 +86,13 @@ func newRepoCmd(env *cliEnv) *cobra.Command {
87 86
88 // listRepos reads every page of repositories.87 // listRepos reads every page of repositories.
89 func listRepos(ctx context.Context, c *client.Client) ([]*ermite.Repo, error) {88 func listRepos(ctx context.Context, c *client.Client) ([]*ermite.Repo, error) {
90- var repos []*ermite.Repo89+ return collect(ctx, func(token string) ([]*ermite.Repo, string, error) {
91- token := ""
92- for {
93 res, err := c.Repos().ListRepos(ctx, connect.NewRequest(&ermite.ListReposRequest{PageToken: token}))90 res, err := c.Repos().ListRepos(ctx, connect.NewRequest(&ermite.ListReposRequest{PageToken: token}))
94 if err != nil {91 if err != nil {
95- return nil, c.Describe(err)92+ return nil, "", c.Describe(err)
96 }93 }
97- repos = append(repos, res.Msg.GetRepos()...)94+ return res.Msg.GetRepos(), res.Msg.GetNextPageToken(), nil
98- token = res.Msg.GetNextPageToken()95+ })
99- if token == "" {
100- return repos, nil
101- }
102- }
103 }96 }
104 97
105 // completeRepoNames completes the first argument with repository names. Any98 // completeRepoNames completes the first argument with repository names. Any
@@ -109,36 +102,28 @@ func completeRepoNames(env *cliEnv) func(*cobra.Command, []string, string) ([]st
109 if len(args) > 0 {102 if len(args) > 0 {
110 return nil, cobra.ShellCompDirectiveNoFileComp103 return nil, cobra.ShellCompDirectiveNoFileComp
111 }104 }
112- names, err := repoNames(env.ctx(cmd), env, cmd, toComplete)105+ return repoNames(env, cmd, toComplete), cobra.ShellCompDirectiveNoFileComp
113- if err != nil {
114- return nil, cobra.ShellCompDirectiveNoFileComp
115- }
116- return names, cobra.ShellCompDirectiveNoFileComp
117 }106 }
118 }107 }
119 108
120-func repoNames(ctx context.Context, env *cliEnv, cmd *cobra.Command, prefix string) ([]string, error) {109+// repoNames lists the repository names starting with prefix, with their
121- c, err := env.client(cmd, false)110+// visibility and description as completion hints.
122- if err != nil {111+func repoNames(env *cliEnv, cmd *cobra.Command, prefix string) []string {
123- return nil, err112+ return complete(env, cmd, prefix, listRepos, func(r *ermite.Repo) (string, string) {
124- }113+ desc := visibilityName(r.GetVisibility())
125- ctx, cancel := context.WithTimeout(ctx, 3*time.Second)114+ if d := clean(r.GetDescription()); d != "" {
126- defer cancel()115+ desc += ", " + d
127- repos, err := listRepos(ctx, c)
128- if err != nil {
129- return nil, err
130- }
131- var names []string
132- for _, r := range repos {
133- if strings.HasPrefix(r.GetName(), prefix) {
134- desc := visibilityName(r.GetVisibility())
135- if d := clean(r.GetDescription()); d != "" {
136- desc += ", " + d
137- }
138- names = append(names, r.GetName()+"\t"+desc)
139 }116 }
117+ return r.GetName(), desc
118+ })
119+}
120+
121+// outputRepo prints a repository as JSON or as fields.
122+func outputRepo(cmd *cobra.Command, r *ermite.Repo, asJSON bool) error {
123+ if asJSON {
124+ return writeJSON(cmd.OutOrStdout(), toJSONRepo(r))
140 }125 }
141- return names, nil126+ return printRepo(cmd.OutOrStdout(), r)
142 }127 }
143 128
144 func newRepoListCmd(env *cliEnv) *cobra.Command {129 func newRepoListCmd(env *cliEnv) *cobra.Command {
@@ -214,10 +199,7 @@ func newRepoShowCmd(env *cliEnv) *cobra.Command {
214 if err != nil {199 if err != nil {
215 return c.Describe(err)200 return c.Describe(err)
216 }201 }
217- if *asJSON {202+ return outputRepo(cmd, res.Msg.GetRepo(), *asJSON)
218- return writeJSON(cmd.OutOrStdout(), toJSONRepo(res.Msg.GetRepo()))
219- }
220- return printRepo(cmd.OutOrStdout(), res.Msg.GetRepo())
221 }203 }
222 return cmd204 return cmd
223 }205 }
@@ -226,7 +208,6 @@ func newRepoCreateCmd(env *cliEnv) *cobra.Command {
226 var (208 var (
227 description string209 description string
228 visibility string210 visibility string
229- private bool
230 )211 )
231 cmd := &cobra.Command{212 cmd := &cobra.Command{
232 Use: "create <name>",213 Use: "create <name>",
@@ -237,11 +218,10 @@ func newRepoCreateCmd(env *cliEnv) *cobra.Command {
237 }218 }
238 cmd.Flags().StringVar(&description, "description", "", "one-line description")219 cmd.Flags().StringVar(&description, "description", "", "one-line description")
239 cmd.Flags().StringVar(&visibility, "visibility", visibilityPrivate, "visibility: public or private")220 cmd.Flags().StringVar(&visibility, "visibility", visibilityPrivate, "visibility: public or private")
240- cmd.Flags().BoolVar(&private, "private", false, "create a private repository (the default; same as --visibility private)")
241 _ = cmd.RegisterFlagCompletionFunc("visibility", visibilityCompletion)221 _ = cmd.RegisterFlagCompletionFunc("visibility", visibilityCompletion)
242 asJSON := addJSONFlag(cmd)222 asJSON := addJSONFlag(cmd)
243 cmd.RunE = func(cmd *cobra.Command, args []string) error {223 cmd.RunE = func(cmd *cobra.Command, args []string) error {
244- req, err := buildCreateRepo(cmd, args[0], description, visibility, private)224+ req, err := buildCreateRepo(args[0], description, visibility)
245 if err != nil {225 if err != nil {
246 return err226 return err
247 }227 }
@@ -253,10 +233,7 @@ func newRepoCreateCmd(env *cliEnv) *cobra.Command {
253 if err != nil {233 if err != nil {
254 return c.Describe(err)234 return c.Describe(err)
255 }235 }
256- if *asJSON {236+ return outputRepo(cmd, res.Msg.GetRepo(), *asJSON)
257- return writeJSON(cmd.OutOrStdout(), toJSONRepo(res.Msg.GetRepo()))
258- }
259- return printRepo(cmd.OutOrStdout(), res.Msg.GetRepo())
260 }237 }
261 return cmd238 return cmd
262 }239 }
@@ -266,17 +243,11 @@ func visibilityCompletion(*cobra.Command, []string, string) ([]string, cobra.She
266 }243 }
267 244
268 // buildCreateRepo maps the flags of `repo create` to the request.245 // buildCreateRepo maps the flags of `repo create` to the request.
269-func buildCreateRepo(cmd *cobra.Command, name, description, visibility string, private bool) (*ermite.CreateRepoRequest, error) {246+func buildCreateRepo(name, description, visibility string) (*ermite.CreateRepoRequest, error) {
270 vis, err := parseVisibility(visibility)247 vis, err := parseVisibility(visibility)
271 if err != nil {248 if err != nil {
272 return nil, err249 return nil, err
273 }250 }
274- if private {
275- if cmd.Flags().Changed("visibility") && vis != ermite.Visibility_VISIBILITY_PRIVATE {
276- return nil, errors.New("--private conflicts with --visibility " + visibilityPublic)
277- }
278- vis = ermite.Visibility_VISIBILITY_PRIVATE
279- }
280 return &ermite.CreateRepoRequest{Name: name, Description: description, Visibility: vis}, nil251 return &ermite.CreateRepoRequest{Name: name, Description: description, Visibility: vis}, nil
281 }252 }
282 253
@@ -308,10 +279,7 @@ func newRepoEditCmd(env *cliEnv) *cobra.Command {
308 if err != nil {279 if err != nil {
309 return c.Describe(err)280 return c.Describe(err)
310 }281 }
311- if *asJSON {282+ return outputRepo(cmd, res.Msg.GetRepo(), *asJSON)
312- return writeJSON(cmd.OutOrStdout(), toJSONRepo(res.Msg.GetRepo()))
313- }
314- return printRepo(cmd.OutOrStdout(), res.Msg.GetRepo())
315 }283 }
316 return cmd284 return cmd
317 }285 }
M cmd/ermite/cli_repo_test.go +0 -3
@@ -114,9 +114,7 @@ func TestRepoCreateMapsFlags(t *testing.T) {
114114 desc string
115115 }{
116116 {"default is private", []string{"x"}, ermite.Visibility_VISIBILITY_PRIVATE, ""},
117- {"--private", []string{"x", "--private"}, ermite.Visibility_VISIBILITY_PRIVATE, ""},
118117 {"--visibility public", []string{"x", "--visibility", "public", "--description", "d d"}, ermite.Visibility_VISIBILITY_PUBLIC, "d d"},
119- {"--visibility private --private", []string{"x", "--visibility", "private", "--private"}, ermite.Visibility_VISIBILITY_PRIVATE, ""},
120118 {"case insensitive", []string{"x", "--visibility", "Public"}, ermite.Visibility_VISIBILITY_PUBLIC, ""},
121119 }
122120 for _, tt := range tests {
@@ -140,7 +138,6 @@ func TestRepoCreateRejectsBadFlagsWithoutCalling(t *testing.T) {
140138 url := f.serve(t)
141139 for _, args := range [][]string{
142140 {"repo", "create", "x", "--visibility", "secret"},
143- {"repo", "create", "x", "--visibility", "public", "--private"},
144141 } {
145142 res := runCLI(t, envFor(url), "", args...)
146143 require.Error(t, res.err, "%v", args)
@@ -114,9 +114,7 @@ func TestRepoCreateMapsFlags(t *testing.T) {
114 desc string114 desc string
115 }{115 }{
116 {"default is private", []string{"x"}, ermite.Visibility_VISIBILITY_PRIVATE, ""},116 {"default is private", []string{"x"}, ermite.Visibility_VISIBILITY_PRIVATE, ""},
117- {"--private", []string{"x", "--private"}, ermite.Visibility_VISIBILITY_PRIVATE, ""},
118 {"--visibility public", []string{"x", "--visibility", "public", "--description", "d d"}, ermite.Visibility_VISIBILITY_PUBLIC, "d d"},117 {"--visibility public", []string{"x", "--visibility", "public", "--description", "d d"}, ermite.Visibility_VISIBILITY_PUBLIC, "d d"},
119- {"--visibility private --private", []string{"x", "--visibility", "private", "--private"}, ermite.Visibility_VISIBILITY_PRIVATE, ""},
120 {"case insensitive", []string{"x", "--visibility", "Public"}, ermite.Visibility_VISIBILITY_PUBLIC, ""},118 {"case insensitive", []string{"x", "--visibility", "Public"}, ermite.Visibility_VISIBILITY_PUBLIC, ""},
121 }119 }
122 for _, tt := range tests {120 for _, tt := range tests {
@@ -140,7 +138,6 @@ func TestRepoCreateRejectsBadFlagsWithoutCalling(t *testing.T) {
140 url := f.serve(t)138 url := f.serve(t)
141 for _, args := range [][]string{139 for _, args := range [][]string{
142 {"repo", "create", "x", "--visibility", "secret"},140 {"repo", "create", "x", "--visibility", "secret"},
143- {"repo", "create", "x", "--visibility", "public", "--private"},
144 } {141 } {
145 res := runCLI(t, envFor(url), "", args...)142 res := runCLI(t, envFor(url), "", args...)
146 require.Error(t, res.err, "%v", args)143 require.Error(t, res.err, "%v", args)
M cmd/ermite/cli_token.go +8 -31
@@ -97,11 +97,7 @@ func newTokenCreateCmd(env *cliEnv) *cobra.Command {
9797 return client.ScopeNames(client.AllScopes()), cobra.ShellCompDirectiveNoFileComp
9898 })
9999 _ = cmd.RegisterFlagCompletionFunc("repo", func(c *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) {
100- names, err := repoNames(env.ctx(c), env, c, toComplete)
101- if err != nil {
102- return nil, cobra.ShellCompDirectiveNoFileComp
103- }
104- return names, cobra.ShellCompDirectiveNoFileComp
100+ return repoNames(env, c, toComplete), cobra.ShellCompDirectiveNoFileComp
105101 })
106102 asJSON := addJSONFlag(cmd)
107103 cmd.RunE = func(cmd *cobra.Command, args []string) error {
@@ -169,19 +165,13 @@ func printCreatedToken(cmd *cobra.Command, res *ermite.CreateTokenResponse, asJS
169165 }
170166
171167 func listTokens(ctx context.Context, c *client.Client) ([]*ermite.Token, error) {
172- var tokens []*ermite.Token
173- page := ""
174- for {
168+ return collect(ctx, func(page string) ([]*ermite.Token, string, error) {
175169 res, err := c.Tokens().ListTokens(ctx, connect.NewRequest(&ermite.ListTokensRequest{PageToken: page}))
176170 if err != nil {
177- return nil, c.Describe(err)
171+ return nil, "", c.Describe(err)
178172 }
179- tokens = append(tokens, res.Msg.GetTokens()...)
180- page = res.Msg.GetNextPageToken()
181- if page == "" {
182- return tokens, nil
183- }
184- }
173+ return res.Msg.GetTokens(), res.Msg.GetNextPageToken(), nil
174+ })
185175 }
186176
187177 func newTokenListCmd(env *cliEnv) *cobra.Command {
@@ -238,22 +228,9 @@ func newTokenRevokeCmd(env *cliEnv) *cobra.Command {
238228 if len(args) > 0 {
239229 return nil, cobra.ShellCompDirectiveNoFileComp
240230 }
241- c, err := env.client(cmd, false)
242- if err != nil {
243- return nil, cobra.ShellCompDirectiveNoFileComp
244- }
245- ctx, cancel := context.WithTimeout(env.ctx(cmd), 3*time.Second)
246- defer cancel()
247- tokens, err := listTokens(ctx, c)
248- if err != nil {
249- return nil, cobra.ShellCompDirectiveNoFileComp
250- }
251- var ids []string
252- for _, t := range tokens {
253- if strings.HasPrefix(t.GetId(), toComplete) {
254- ids = append(ids, t.GetId()+"\t"+clean(t.GetLabel()))
255- }
256- }
231+ ids := complete(env, cmd, toComplete, listTokens, func(t *ermite.Token) (string, string) {
232+ return t.GetId(), clean(t.GetLabel())
233+ })
257234 return ids, cobra.ShellCompDirectiveNoFileComp
258235 },
259236 RunE: func(cmd *cobra.Command, args []string) error {
@@ -97,11 +97,7 @@ func newTokenCreateCmd(env *cliEnv) *cobra.Command {
97 return client.ScopeNames(client.AllScopes()), cobra.ShellCompDirectiveNoFileComp97 return client.ScopeNames(client.AllScopes()), cobra.ShellCompDirectiveNoFileComp
98 })98 })
99 _ = cmd.RegisterFlagCompletionFunc("repo", func(c *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) {99 _ = cmd.RegisterFlagCompletionFunc("repo", func(c *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) {
100- names, err := repoNames(env.ctx(c), env, c, toComplete)100+ return repoNames(env, c, toComplete), cobra.ShellCompDirectiveNoFileComp
101- if err != nil {
102- return nil, cobra.ShellCompDirectiveNoFileComp
103- }
104- return names, cobra.ShellCompDirectiveNoFileComp
105 })101 })
106 asJSON := addJSONFlag(cmd)102 asJSON := addJSONFlag(cmd)
107 cmd.RunE = func(cmd *cobra.Command, args []string) error {103 cmd.RunE = func(cmd *cobra.Command, args []string) error {
@@ -169,19 +165,13 @@ func printCreatedToken(cmd *cobra.Command, res *ermite.CreateTokenResponse, asJS
169 }165 }
170 166
171 func listTokens(ctx context.Context, c *client.Client) ([]*ermite.Token, error) {167 func listTokens(ctx context.Context, c *client.Client) ([]*ermite.Token, error) {
172- var tokens []*ermite.Token168+ return collect(ctx, func(page string) ([]*ermite.Token, string, error) {
173- page := ""
174- for {
175 res, err := c.Tokens().ListTokens(ctx, connect.NewRequest(&ermite.ListTokensRequest{PageToken: page}))169 res, err := c.Tokens().ListTokens(ctx, connect.NewRequest(&ermite.ListTokensRequest{PageToken: page}))
176 if err != nil {170 if err != nil {
177- return nil, c.Describe(err)171+ return nil, "", c.Describe(err)
178 }172 }
179- tokens = append(tokens, res.Msg.GetTokens()...)173+ return res.Msg.GetTokens(), res.Msg.GetNextPageToken(), nil
180- page = res.Msg.GetNextPageToken()174+ })
181- if page == "" {
182- return tokens, nil
183- }
184- }
185 }175 }
186 176
187 func newTokenListCmd(env *cliEnv) *cobra.Command {177 func newTokenListCmd(env *cliEnv) *cobra.Command {
@@ -238,22 +228,9 @@ func newTokenRevokeCmd(env *cliEnv) *cobra.Command {
238 if len(args) > 0 {228 if len(args) > 0 {
239 return nil, cobra.ShellCompDirectiveNoFileComp229 return nil, cobra.ShellCompDirectiveNoFileComp
240 }230 }
241- c, err := env.client(cmd, false)231+ ids := complete(env, cmd, toComplete, listTokens, func(t *ermite.Token) (string, string) {
242- if err != nil {232+ return t.GetId(), clean(t.GetLabel())
243- return nil, cobra.ShellCompDirectiveNoFileComp233+ })
244- }
245- ctx, cancel := context.WithTimeout(env.ctx(cmd), 3*time.Second)
246- defer cancel()
247- tokens, err := listTokens(ctx, c)
248- if err != nil {
249- return nil, cobra.ShellCompDirectiveNoFileComp
250- }
251- var ids []string
252- for _, t := range tokens {
253- if strings.HasPrefix(t.GetId(), toComplete) {
254- ids = append(ids, t.GetId()+"\t"+clean(t.GetLabel()))
255- }
256- }
257 return ids, cobra.ShellCompDirectiveNoFileComp234 return ids, cobra.ShellCompDirectiveNoFileComp
258 },235 },
259 RunE: func(cmd *cobra.Command, args []string) error {236 RunE: func(cmd *cobra.Command, args []string) error {

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 2

Tags 45

↑↓ moveenter openesc close