Commit f050e964f5

f050e964f52bd6b5e6416d9811029e7abc0027f6

parent: f2c5e7302d

Verified · cmc ci/build: success ci/test: success

cmc <hello@cleberg.net> · 2026-09-29 04:41 UTC

help: examples take the CLI's path for regrouped commands

Closes #304

Layout: unified · split

CHANGELOG.org +3
@@ -8,6 +8,9 @@ anything beyond "replace the binary and restart" is needed.
8 8
9- Marking notifications read from the web dispatches =notifications 9- Marking notifications read from the web dispatches =notifications
10 read=, so it counts against the write budget and is audited (#261). 10 read=, so it counts against the write budget and is audited (#261).
11- =gitbay <cli path> --help= for commands the CLI regroups (=auth keys
12 add=, =auth email list=, =repo topics list=) prints EXAMPLES under the
13 CLI's path instead of the registered one (#304).
11- =gitbayd admin backup --verify= also checks every release asset the 14- =gitbayd admin backup --verify= also checks every release asset the
12 database names against its recorded size and sha256, and every 15 database names against its recorded size and sha256, and every
13 archived LFS object against its name (#259). 16 archived LFS object against its name (#259).
cmd/gitbay/serverpath_test.go +52
@@ -1,10 +1,15 @@
1package main 1package main
2 2
3import ( 3import (
4 "bytes"
4 "slices" 5 "slices"
6 "strings"
5 "testing" 7 "testing"
6 8
7 "github.com/spf13/cobra" 9 "github.com/spf13/cobra"
10
11 "gitbay.org/gitbay/internal/control"
12 "gitbay.org/gitbay/internal/protocol"
8) 13)
9 14
10// TestServerPathMismatches pins the commands whose CLI path differs from 15// TestServerPathMismatches pins the commands whose CLI path differs from
@@ -79,3 +84,50 @@ func TestHelpArgvSendsThePathForAMismatchedGroup(t *testing.T) {
79 t.Errorf("helpArgv(keys, auth keys) = %v, want %v", got, want) 84 t.Errorf("helpArgv(keys, auth keys) = %v, want %v", got, want)
80 } 85 }
81} 86}
87
88// TestRenamedCommandHelpNamesTheCLIPath runs the server's help for every
89// command whose CLI path differs from its registered path, sent as the
90// CLI sends it, and fails if the text names the registered path where the
91// CLI's belongs (#304).
92func TestRenamedCommandHelpNamesTheCLIPath(t *testing.T) {
93 type renamed struct{ cli, server string }
94 var cmds []renamed
95 var walk func(*cobra.Command)
96 walk = func(c *cobra.Command) {
97 if p := c.Annotations[serverPath]; p != "" && p != "help" {
98 if cli := cliPathOf(c); cli != p {
99 cmds = append(cmds, renamed{cli, p})
100 }
101 }
102 for _, sub := range c.Commands() {
103 walk(sub)
104 }
105 }
106 root := newRoot()
107 root.InitDefaultHelpCmd()
108 walk(root)
109 if len(cmds) == 0 {
110 t.Fatal("no renamed commands found")
111 }
112 for _, rc := range cmds {
113 argv := withCLIPath(rc.cli, rc.server, append([]string{"help"}, strings.Fields(rc.server)...))
114 var out, errOut bytes.Buffer
115 c := &control.Ctx{Stdout: &out, Stderr: &errOut, Scope: "full"}
116 c.Cfg.Server.SiteURL = "https://forge.test"
117 if code := control.Dispatch(c, argv); code != protocol.ExitOK {
118 t.Errorf("%v: exit %d: %s", argv, code, errOut.String())
119 continue
120 }
121 // What the CLI path itself contains (repo topics list holds the
122 // registered repo topics) is not a mismatch.
123 // SEE ALSO names sibling commands, which keep their own paths.
124 text, _, _ := strings.Cut(out.String(), "SEE ALSO")
125 text = strings.ReplaceAll(text, "gitbay "+rc.cli, "")
126 for _, line := range strings.Split(text, "\n") {
127 if strings.Contains(line, "gitbay "+rc.server+" ") || strings.HasSuffix(line, "gitbay "+rc.server) {
128 t.Errorf("%s: help names the registered path %q:\n%s", rc.cli, rc.server, out.String())
129 break
130 }
131 }
132 }
133}
internal/control/help.go +1 −1
@@ -310,7 +310,7 @@ func (c *Ctx) helpVerb(w io.Writer, cmd Command, below []Command) {
310 fmt.Fprintln(w) 310 fmt.Fprintln(w)
311 c.heading(w, "EXAMPLES") 311 c.heading(w, "EXAMPLES")
312 for _, ex := range cmd.Examples { 312 for _, ex := range cmd.Examples {
313 c.wrapLine(w, " "+c.program()+" ", ex) 313 c.wrapLine(w, " "+c.program()+" ", c.shownAs(registered, ex))
314 } 314 }
315 } 315 }
316 if len(below) > 0 { 316 if len(below) > 0 {