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 {
253 // The registry is the only place flags are written down, so 253 // The registry is the only place flags are written down, so
254 // --help asks the server rather than reprinting the one-line 254 // --help asks the server rather than reprinting the one-line
255 // summary cobra holds. 255 // summary cobra holds.
256 cliPath := cliPathOf(cmd)
256 for _, a := range args { 257 for _, a := range args {
257 if a == "--help" || a == "-h" { 258 if a == "--help" || a == "-h" {
258 os.Exit(runServerHelp(o)) 259 os.Exit(runServerHelp(o, cliPath))
259 } 260 }
260 } 261 }
261 os.Exit(runPass(o, args)) 262 os.Exit(runPass(o, cliPath, args))
262 return nil 263 return nil
263 }, 264 },
264 } 265 }
@@ -276,16 +277,17 @@ func withShort(cmd *cobra.Command, short string) *cobra.Command {
276} 277}
277 278
278// runServerHelp prints the registry's usage for one command. 279// runServerHelp prints the registry's usage for one command.
279func runServerHelp(o passOpts) int { 280func runServerHelp(o passOpts, cliPath string) int {
280 t, err := resolveTarget() 281 t, err := resolveTarget()
281 if err != nil { 282 if err != nil {
282 fmt.Fprintln(os.Stderr, "gitbay:", err) 283 fmt.Fprintln(os.Stderr, "gitbay:", err)
283 return protocol.ExitFailure 284 return protocol.ExitFailure
284 } 285 }
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(""))
286} 288}
287 289
288func runPass(o passOpts, args []string) int { 290func runPass(o passOpts, cliPath string, args []string) int {
289 t, err := resolveTarget() 291 t, err := resolveTarget()
290 if err != nil { 292 if err != nil {
291 fmt.Fprintln(os.Stderr, "gitbay:", err) 293 fmt.Fprintln(os.Stderr, "gitbay:", err)
@@ -343,7 +345,8 @@ func runPass(o passOpts, args []string) int {
343 stdin = r 345 stdin = r
344 } 346 }
345 } 347 }
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))
347} 350}
348 351
349func isEmptyReader(r io.Reader) bool { 352func isEmptyReader(r io.Reader) bool {
@@ -380,7 +383,7 @@ func group(use, short string, subs ...*cobra.Command) *cobra.Command {
380 // know by that name, cobra's own tree still prints. 383 // know by that name, cobra's own tree still prints.
381 local := c.HelpFunc() 384 local := c.HelpFunc()
382 c.SetHelpFunc(func(cmd *cobra.Command, args []string) { 385 c.SetHelpFunc(func(cmd *cobra.Command, args []string) {
383 if !serverHelp(use) { 386 if !serverHelp(use, cliPathOf(c)) {
384 local(cmd, args) 387 local(cmd, args)
385 } 388 }
386 }) 389 })
@@ -388,19 +391,21 @@ func group(use, short string, subs ...*cobra.Command) *cobra.Command {
388} 391}
389 392
390// serverHelp prints the registry's usage for a prefix and reports whether 393// 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 394// it did. cliPath is the group's own path in the CLI. At a terminal it
392// gets the same --term=<cols>[,color] treatment (and layout) as any other 395// goes through the terminal-aware path, so it gets the same
393// command; piped, it stays a quiet capture, so a network or lookup 396// --term=<cols>[,color] treatment (and layout) as any other command;
394// failure falls back to cobra's local help without noise. 397// piped, it stays a quiet capture, so a network or lookup failure falls
395func serverHelp(prefix string) bool { 398// back to cobra's local help without noise.
399func serverHelp(prefix, cliPath string) bool {
396 t, err := resolveTarget() 400 t, err := resolveTarget()
397 if err != nil { 401 if err != nil {
398 return false 402 return false
399 } 403 }
404 argv := withCLIPath(cliPath, prefix, []string{"help", prefix})
400 if term.IsTerminal(int(os.Stdout.Fd())) { 405 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
402 } 407 }
403 out, code := sshCapture(t, []string{"help", prefix}) 408 out, code := sshCapture(t, argv)
404 if code != 0 || out == "" { 409 if code != 0 || out == "" {
405 return false 410 return false
406 } 411 }
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 (
17 "gitbay.org/gitbay/internal/cliconfig" 17 "gitbay.org/gitbay/internal/cliconfig"
18 "gitbay.org/gitbay/internal/protocol" 18 "gitbay.org/gitbay/internal/protocol"
19 "gitbay.org/gitbay/internal/toolpath" 19 "gitbay.org/gitbay/internal/toolpath"
20 "github.com/spf13/cobra"
20) 21)
21 22
22// context is the resolved target for a command: which instance to talk to 23// context is the resolved target for a command: which instance to talk to
@@ -166,6 +167,23 @@ func pages(server, args []string) bool {
166 return false 167 return false
167} 168}
168 169
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
169// runSSH executes the server command over the system ssh binary, wiring 187// runSSH executes the server command over the system ssh binary, wiring
170// stdio through, with no pager. It returns the remote exit code. 188// stdio through, with no pager. It returns the remote exit code.
171func runSSH(t target, serverArgv []string, stdin io.Reader) int { 189func runSSH(t target, serverArgv []string, stdin io.Reader) int {
cmd/gitbay/term_test.go +28
@@ -1,8 +1,11 @@
1package main 1package main
2 2
3import ( 3import (
4 "slices"
4 "strings" 5 "strings"
5 "testing" 6 "testing"
7
8 "github.com/spf13/cobra"
6) 9)
7 10
8func TestTermValue(t *testing.T) { 11func TestTermValue(t *testing.T) {
@@ -77,3 +80,28 @@ func TestPages(t *testing.T) {
77 t.Error("list must not page") 80 t.Error("list must not page")
78 } 81 }
79} 82}
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}