Commit a8523b10b1

a8523b10b109926f56de83ec5cb3d06e87b6005e

parent: a3fa0f0777

Verified · cmc

cmc <hello@cleberg.net> · 2026-09-28 06:56 UTC

cli: send --path= where the CLI path differs from the server path

Ref #267

Layout: unified · split

cmd/gitbay/main.go +19 −14
@@ -253,12 +253,13 @@ func pass(use string, o passOpts) *cobra.Command {
253253 // The registry is the only place flags are written down, so
254254 // --help asks the server rather than reprinting the one-line
255255 // summary cobra holds.
256 cliPath := cliPathOf(cmd)
256257 for _, a := range args {
257258 if a == "--help" || a == "-h" {
258 os.Exit(runServerHelp(o))
259 os.Exit(runServerHelp(o, cliPath))
259260 }
260261 }
261 os.Exit(runPass(o, args))
262 os.Exit(runPass(o, cliPath, args))
262263 return nil
263264 },
264265 }
@@ -276,16 +277,17 @@ func withShort(cmd *cobra.Command, short string) *cobra.Command {
276277}
277278
278279// runServerHelp prints the registry's usage for one command.
279func runServerHelp(o passOpts) int {
280func runServerHelp(o passOpts, cliPath string) int {
280281 t, err := resolveTarget()
281282 if err != nil {
282283 fmt.Fprintln(os.Stderr, "gitbay:", err)
283284 return protocol.ExitFailure
284285 }
285 return runSSH(t, append([]string{"help"}, o.server...), strings.NewReader(""))
286 argv := withCLIPath(cliPath, strings.Join(o.server, " "), append([]string{"help"}, o.server...))
287 return runSSH(t, argv, strings.NewReader(""))
286288}
287289
288func runPass(o passOpts, args []string) int {
290func runPass(o passOpts, cliPath string, args []string) int {
289291 t, err := resolveTarget()
290292 if err != nil {
291293 fmt.Fprintln(os.Stderr, "gitbay:", err)
@@ -343,7 +345,8 @@ func runPass(o passOpts, args []string) int {
343345 stdin = r
344346 }
345347 }
346 return runSSHPaged(t, append(o.server, args...), stdin, pages(o.server, args))
348 argv := withCLIPath(cliPath, strings.Join(o.server, " "), append(o.server, args...))
349 return runSSHPaged(t, argv, stdin, pages(o.server, args))
347350}
348351
349352func isEmptyReader(r io.Reader) bool {
@@ -380,7 +383,7 @@ func group(use, short string, subs ...*cobra.Command) *cobra.Command {
380383 // know by that name, cobra's own tree still prints.
381384 local := c.HelpFunc()
382385 c.SetHelpFunc(func(cmd *cobra.Command, args []string) {
383 if !serverHelp(use) {
386 if !serverHelp(use, cliPathOf(c)) {
384387 local(cmd, args)
385388 }
386389 })
@@ -388,19 +391,21 @@ func group(use, short string, subs ...*cobra.Command) *cobra.Command {
388391}
389392
390393// serverHelp prints the registry's usage for a prefix and reports whether
391// it did. At a terminal it goes through the terminal-aware path, so it
392// gets the same --term=<cols>[,color] treatment (and layout) as any other
393// command; piped, it stays a quiet capture, so a network or lookup
394// failure falls back to cobra's local help without noise.
395func serverHelp(prefix string) bool {
394// it did. cliPath is the group's own path in the CLI. At a terminal it
395// goes through the terminal-aware path, so it gets the same
396// --term=<cols>[,color] treatment (and layout) as any other command;
397// piped, it stays a quiet capture, so a network or lookup failure falls
398// back to cobra's local help without noise.
399func serverHelp(prefix, cliPath string) bool {
396400 t, err := resolveTarget()
397401 if err != nil {
398402 return false
399403 }
404 argv := withCLIPath(cliPath, prefix, []string{"help", prefix})
400405 if term.IsTerminal(int(os.Stdout.Fd())) {
401 return runSSH(t, []string{"help", prefix}, strings.NewReader("")) == 0
406 return runSSH(t, argv, strings.NewReader("")) == 0
402407 }
403 out, code := sshCapture(t, []string{"help", prefix})
408 out, code := sshCapture(t, argv)
404409 if code != 0 || out == "" {
405410 return false
406411 }
cmd/gitbay/serverpath_test.go added +45
@@ -0,0 +1,45 @@
1package main
2
3import (
4 "slices"
5 "testing"
6
7 "github.com/spf13/cobra"
8)
9
10// TestServerPathMismatches pins the commands whose CLI path differs from
11// the server path they dispatch, the ones that send --path= so usage and
12// help print the CLI's path (#267). A change to this list is deliberate.
13func TestServerPathMismatches(t *testing.T) {
14 want := []string{
15 "auth email add", "auth email list", "auth email primary",
16 "auth email remove", "auth email verify",
17 "auth export",
18 "auth keys add", "auth keys label", "auth keys list", "auth keys remove",
19 "auth pgp add", "auth pgp list", "auth pgp remove",
20 "auth token create", "auth token list", "auth token revoke",
21 "auth whoami",
22 "repo topics list",
23 }
24
25 var got []string
26 var walk func(*cobra.Command)
27 walk = func(c *cobra.Command) {
28 if p := c.Annotations[serverPath]; p != "" {
29 if cli := cliPathOf(c); cli != p {
30 got = append(got, cli)
31 }
32 }
33 for _, sub := range c.Commands() {
34 walk(sub)
35 }
36 }
37 root := newRoot()
38 root.InitDefaultHelpCmd()
39 walk(root)
40 slices.Sort(got)
41
42 if !slices.Equal(got, want) {
43 t.Errorf("mismatched CLI paths = %v\nwant %v", got, want)
44 }
45}
cmd/gitbay/ssh.go +18
@@ -17,6 +17,7 @@ import (
1717 "gitbay.org/gitbay/internal/cliconfig"
1818 "gitbay.org/gitbay/internal/protocol"
1919 "gitbay.org/gitbay/internal/toolpath"
20 "github.com/spf13/cobra"
2021)
2122
2223// context is the resolved target for a command: which instance to talk to
@@ -166,6 +167,23 @@ func pages(server, args []string) bool {
166167 return false
167168}
168169
170// cliPathOf is the path a cobra command was reached by, without the
171// root's name: "auth keys remove".
172func cliPathOf(cmd *cobra.Command) string {
173 return strings.TrimPrefix(cmd.CommandPath(), "gitbay ")
174}
175
176// withCLIPath prepends --path=<cliPath> to a server command line when the
177// CLI path differs from the server path it dispatches, so usage and help
178// print a command the caller can run (#267). Dispatch strips it before
179// Lookup, as it does --term=.
180func withCLIPath(cliPath, server string, argv []string) []string {
181 if cliPath == server {
182 return argv
183 }
184 return append([]string{"--path=" + cliPath}, argv...)
185}
186
169187// runSSH executes the server command over the system ssh binary, wiring
170188// stdio through, with no pager. It returns the remote exit code.
171189func runSSH(t target, serverArgv []string, stdin io.Reader) int {
cmd/gitbay/term_test.go +28
@@ -1,8 +1,11 @@
11package main
22
33import (
4 "slices"
45 "strings"
56 "testing"
7
8 "github.com/spf13/cobra"
69)
710
811func TestTermValue(t *testing.T) {
@@ -77,3 +80,28 @@ func TestPages(t *testing.T) {
7780 t.Error("list must not page")
7881 }
7982}
83
84func TestCLIPathOf(t *testing.T) {
85 root := &cobra.Command{Use: "gitbay"}
86 auth := &cobra.Command{Use: "auth"}
87 keys := &cobra.Command{Use: "keys"}
88 remove := &cobra.Command{Use: "remove"}
89 keys.AddCommand(remove)
90 auth.AddCommand(keys)
91 root.AddCommand(auth)
92 if got := cliPathOf(remove); got != "auth keys remove" {
93 t.Errorf("cliPathOf = %q", got)
94 }
95}
96
97func TestWithCLIPath(t *testing.T) {
98 argv := []string{"keys", "remove", "abc"}
99 if got := withCLIPath("keys remove", "keys remove", argv); !slices.Equal(got, argv) {
100 t.Errorf("matching path: %v", got)
101 }
102 got := withCLIPath("auth keys remove", "keys remove", argv)
103 want := []string{"--path=auth keys remove", "keys", "remove", "abc"}
104 if !slices.Equal(got, want) {
105 t.Errorf("got %v, want %v", got, want)
106 }
107}