Commit 4eb0f95ce5

4eb0f95ce50ad8d5e29cccf5d3b47a33b9d761df

parent: 44191b1a52

Verified · cmc

cmc <hello@cleberg.net> · 2026-09-28 07:22 UTC

help: auth (and any future CLI-only grouping) renders with the registry layout, in the CLI's own paths

Ref #267

Layout: unified · split

cmd/gitbay/main.go +1 −1
@@ -484,7 +484,7 @@ func authCmd() *cobra.Command {
484 pass("list", passOpts{server: []string{"token", "list"}}), 484 pass("list", passOpts{server: []string{"token", "list"}}),
485 pass("revoke", passOpts{server: []string{"token", "revoke"}}), 485 pass("revoke", passOpts{server: []string{"token", "revoke"}}),
486 ) 486 )
487 return group("auth", "identity: whoami, SSH and PGP keys", 487 return group("auth", "whoami, SSH and PGP keys, email, API tokens",
488 pass("export", passOpts{server: []string{"account", "export"}}), 488 pass("export", passOpts{server: []string{"account", "export"}}),
489 tokens, 489 tokens,
490 pass("whoami", passOpts{server: []string{"whoami"}}), 490 pass("whoami", passOpts{server: []string{"whoami"}}),
internal/control/help.go +70 −7
@@ -28,6 +28,7 @@ var nounSummaries = map[string]string{
28 "account": "export or import your account, for instance migration", 28 "account": "export or import your account, for instance migration",
29 "admin": "instance administration (admins)", 29 "admin": "instance administration (admins)",
30 "audit": "instance audit log (admins)", 30 "audit": "instance audit log (admins)",
31 "auth": "whoami, SSH and PGP keys, email, API tokens",
31 "build": "CI builds", 32 "build": "CI builds",
32 "dashboard": "pinned repos, open MRs, assigned issues, recent builds", 33 "dashboard": "pinned repos, open MRs, assigned issues, recent builds",
33 "email": "manage email addresses", 34 "email": "manage email addresses",
@@ -61,6 +62,35 @@ var nounSummaries = map[string]string{
61// test (task 4.5). 62// test (task 4.5).
62func NounSummaries() map[string]string { return nounSummaries } 63func NounSummaries() map[string]string { return nounSummaries }
63 64
65// nounAlias is one bucket of registered commands, reachable under a
66// CLI-only noun that is not itself a registry path (auth, gathering
67// several unrelated registry prefixes): Registered is what runHelp
68// matches against the registry, CLI is the path a gitbay caller
69// actually types to reach it — not always Registered with the alias's
70// own name stitched on (account export -> auth export drops a word),
71// so the two are paired explicitly rather than derived.
72type nounAlias struct {
73 Registered string
74 CLI string
75}
76
77// nounAliases groups a CLI-only noun into the real prefixes it gathers,
78// so `help auth` renders with the same layout a real noun gets instead
79// of falling back to whatever a caller does when help fails. A stock
80// ssh caller — the only one who could ever ask for a bare "auth" and
81// get nothing back from the registry — sees the Registered forms
82// unchanged; the CLI, having sent its own path, sees CLI.
83var nounAliases = map[string][]nounAlias{
84 "auth": {
85 {"account export", "auth export"},
86 {"whoami", "auth whoami"},
87 {"keys", "auth keys"},
88 {"email", "auth email"},
89 {"pgp", "auth pgp"},
90 {"token", "auth token"},
91 },
92}
93
64// helpEntry is one row of the registry as help reports it. 94// helpEntry is one row of the registry as help reports it.
65type helpEntry struct { 95type helpEntry struct {
66 Path string `json:"path"` 96 Path string `json:"path"`
@@ -76,11 +106,33 @@ type helpEntry struct {
76// noun with several commands under it renders a READ/WRITE summary. 106// noun with several commands under it renders a READ/WRITE summary.
77func runHelp(c *Ctx, args []string) int { 107func runHelp(c *Ctx, args []string) int {
78 prefix := joinPath(args) 108 prefix := joinPath(args)
109 prefixes := []string{prefix}
110 override := map[string]string{}
111 if aliased, ok := nounAliases[prefix]; ok {
112 prefixes = nil
113 for _, a := range aliased {
114 prefixes = append(prefixes, a.Registered)
115 }
116 if c.CLIPath != "" {
117 for _, cmd := range registry {
118 p := joinPath(cmd.Path)
119 for _, a := range aliased {
120 if p == a.Registered || strings.HasPrefix(p, a.Registered+" ") {
121 override[p] = a.CLI + strings.TrimPrefix(p, a.Registered)
122 break
123 }
124 }
125 }
126 }
127 }
79 var matched []Command 128 var matched []Command
80 for _, cmd := range registry { 129 for _, cmd := range registry {
81 p := joinPath(cmd.Path) 130 p := joinPath(cmd.Path)
82 if prefix == "" || p == prefix || strings.HasPrefix(p, prefix+" ") { 131 for _, pfx := range prefixes {
83 matched = append(matched, cmd) 132 if pfx == "" || p == pfx || strings.HasPrefix(p, pfx+" ") {
133 matched = append(matched, cmd)
134 break
135 }
84 } 136 }
85 } 137 }
86 if len(matched) == 0 { 138 if len(matched) == 0 {
@@ -106,7 +158,7 @@ func runHelp(c *Ctx, args []string) int {
106 case joinPath(matched[0].Path) == prefix: 158 case joinPath(matched[0].Path) == prefix:
107 c.helpVerb(w, matched[0], matched[1:]) 159 c.helpVerb(w, matched[0], matched[1:])
108 default: 160 default:
109 c.helpNoun(w, prefix, matched) 161 c.helpNoun(w, prefix, matched, override)
110 } 162 }
111 }) 163 })
112} 164}
@@ -260,16 +312,28 @@ func (c *Ctx) helpVerb(w io.Writer, cmd Command, below []Command) {
260 } 312 }
261} 313}
262 314
263func (c *Ctx) helpNoun(w io.Writer, prefix string, cmds []Command) { 315// helpNoun renders a noun with several commands under it. override, from
316// an aliased noun (auth), gives the full CLI path for a row that is not
317// itself under prefix (keys list, gathered under auth, becomes "auth
318// keys list"); it is empty for an ordinary noun, so every row there
319// still trims to just its own verb.
320func (c *Ctx) helpNoun(w io.Writer, prefix string, cmds []Command, override map[string]string) {
264 head := nounSummaries[strings.Fields(prefix)[0]] 321 head := nounSummaries[strings.Fields(prefix)[0]]
265 fmt.Fprintln(w, head) 322 fmt.Fprintln(w, head)
266 fmt.Fprintln(w) 323 fmt.Fprintln(w)
267 c.heading(w, "USAGE") 324 c.heading(w, "USAGE")
268 display := c.shownAs(prefix, prefix) 325 display := c.shownAs(prefix, prefix)
269 fmt.Fprintf(w, " %s %s <verb> ...\n", c.program(), display) 326 fmt.Fprintf(w, " %s %s <verb> ...\n", c.program(), display)
327 rowText := func(cmd Command) string {
328 full := joinPath(cmd.Path)
329 if ov, ok := override[full]; ok {
330 return ov
331 }
332 return strings.TrimPrefix(full, prefix+" ")
333 }
270 wide := 0 334 wide := 0
271 for _, cmd := range cmds { 335 for _, cmd := range cmds {
272 wide = max(wide, cells(strings.TrimPrefix(joinPath(cmd.Path), prefix+" "))) 336 wide = max(wide, cells(rowText(cmd)))
273 } 337 }
274 for _, section := range []struct { 338 for _, section := range []struct {
275 title string 339 title string
@@ -285,8 +349,7 @@ func (c *Ctx) helpNoun(w io.Writer, prefix string, cmds []Command) {
285 c.heading(w, section.title) 349 c.heading(w, section.title)
286 first = false 350 first = false
287 } 351 }
288 verb := strings.TrimPrefix(joinPath(cmd.Path), prefix+" ") 352 fmt.Fprintf(w, " %s %s\n", pad(rowText(cmd), wide), cmd.Summary)
289 fmt.Fprintf(w, " %s %s\n", pad(verb, wide), cmd.Summary)
290 } 353 }
291 } 354 }
292 fmt.Fprintln(w) 355 fmt.Fprintln(w)
internal/control/help_test.go +30
@@ -241,3 +241,33 @@ func TestHelpPrintsTheCLIPath(t *testing.T) {
241 } 241 }
242 } 242 }
243} 243}
244
245func TestHelpRendersAnAliasedNounWithTheRegistryLayout(t *testing.T) {
246 var out, errOut bytes.Buffer
247 c := &Ctx{Stdout: &out, Stderr: &errOut, Term: Term{Cols: 100}, Scope: "full", CLIPath: "auth"}
248 c.Cfg.Server.SiteURL = "https://forge.test"
249 if code := Dispatch(c, []string{"help", "auth"}); code != protocol.ExitOK {
250 t.Fatalf("exit %d: %s", code, errOut.String())
251 }
252 got := out.String()
253 for _, want := range []string{"auth whoami", "auth keys list", "auth pgp add", "auth token create", "auth export"} {
254 if !strings.Contains(got, want) {
255 t.Errorf("missing %q in:\n%s", want, got)
256 }
257 }
258 if strings.Contains(got, "no command matches") {
259 t.Errorf("auth did not resolve: %s", got)
260 }
261}
262
263func TestHelpRendersAnAliasedNounInRegisteredFormOverSSH(t *testing.T) {
264 got := helpOut(t, Term{}, "auth")
265 for _, want := range []string{"whoami", "keys list", "pgp add", "token create", "account export"} {
266 if !strings.Contains(got, want) {
267 t.Errorf("missing %q in:\n%s", want, got)
268 }
269 }
270 if strings.Contains(got, "auth keys list") {
271 t.Errorf("stock ssh should not see the CLI-only auth prefix: %s", got)
272 }
273}