Commit a3fa0f0777
Verified · cmc
Layout: unified · split
internal/control/control.go +27 −12
| @@ -30,6 +30,12 @@ type Ctx struct { | |||
| 30 | // Term is the client's terminal, from GITBAY_TERM. The zero value | 30 | // Term is the client's terminal, from GITBAY_TERM. The zero value |
| 31 | // is plain output. | 31 | // is plain output. |
| 32 | Term Term | 32 | Term Term |
| 33 | // CLIPath is the path the gitbay CLI resolved this call to, from a | ||
| 34 | // leading --path=, when it differs from the registered path being | ||
| 35 | // dispatched (auth keys remove for keys remove). Usage and help | ||
| 36 | // print it in place of the registered path (#267). Empty for stock | ||
| 37 | // ssh, the web and the API. | ||
| 38 | CLIPath string | ||
| 33 | // ViaAPI marks requests arriving over HTTP, from the token API or | 39 | // ViaAPI marks requests arriving over HTTP, from the token API or |
| 34 | // the web. Every command runs there; nothing is held back for SSH | 40 | // the web. Every command runs there; nothing is held back for SSH |
| 35 | // any more (#234). The flag stays because the rate limiter and the | 41 | // any more (#234). The flag stays because the rate limiter and the |
| @@ -66,13 +72,13 @@ type Ctx struct { | |||
| 66 | // usage reports a bad invocation with the command's registered usage, | 72 | // usage reports a bad invocation with the command's registered usage, |
| 67 | // the one source of it. | 73 | // the one source of it. |
| 68 | func (c *Ctx) usage() int { | 74 | func (c *Ctx) usage() int { |
| 69 | return c.fail(protocol.ExitUsage, "usage: %s", c.Cmd.Usage) | 75 | return c.fail(protocol.ExitUsage, "usage: %s", c.cmdUsage()) |
| 70 | } | 76 | } |
| 71 | 77 | ||
| 72 | // usageWith reports a specific problem with the arguments, then the | 78 | // usageWith reports a specific problem with the arguments, then the |
| 73 | // registered usage, so a person always sees the shape that was expected. | 79 | // registered usage, so a person always sees the shape that was expected. |
| 74 | func (c *Ctx) usageWith(msg string) int { | 80 | func (c *Ctx) usageWith(msg string) int { |
| 75 | return c.fail(protocol.ExitUsage, "%s\nusage: %s", msg, c.Cmd.Usage) | 81 | return c.fail(protocol.ExitUsage, "%s\nusage: %s", msg, c.cmdUsage()) |
| 76 | } | 82 | } |
| 77 | 83 | ||
| 78 | // Flag is one flag in a command's help. | 84 | // Flag is one flag in a command's help. |
| @@ -132,18 +138,27 @@ func Dispatch(c *Ctx, argv []string) int { | |||
| 132 | return c.fail(protocol.ExitUsage, "no command given; try: ssh <host> help") | 138 | return c.fail(protocol.ExitUsage, "no command given; try: ssh <host> help") |
| 133 | } | 139 | } |
| 134 | // A leading --term=<v> selects terminal output for this session, the | 140 | // A leading --term=<v> selects terminal output for this session, the |
| 135 | // same as GITBAY_TERM. It must come off before Lookup: Lookup matches | 141 | // same as GITBAY_TERM; a leading --path=<v> is the CLI's own path for |
| 136 | // argv against a command's Path, and a --term= in front would never | 142 | // the command (Ctx.CLIPath). Both come off before Lookup, in either |
| 137 | // match one. Over HTTP it is dropped unread: the web and the API | 143 | // order: Lookup matches argv against a command's Path, and either in |
| 138 | // render no terminal. | 144 | // front would never match one. Over HTTP both are dropped unread: the |
| 139 | if v, ok := strings.CutPrefix(argv[0], "--term="); ok { | 145 | // web and the API render no terminal and have no CLI path. |
| 140 | if !c.ViaAPI { | 146 | for len(argv) > 0 { |
| 141 | c.Term = ParseTerm(v) | 147 | if v, ok := strings.CutPrefix(argv[0], "--term="); ok { |
| 148 | if !c.ViaAPI { | ||
| 149 | c.Term = ParseTerm(v) | ||
| 150 | } | ||
| 151 | } else if v, ok := strings.CutPrefix(argv[0], "--path="); ok { | ||
| 152 | if !c.ViaAPI { | ||
| 153 | c.CLIPath = v | ||
| 154 | } | ||
| 155 | } else { | ||
| 156 | break | ||
| 142 | } | 157 | } |
| 143 | argv = argv[1:] | 158 | argv = argv[1:] |
| 144 | if len(argv) == 0 { | 159 | } |
| 145 | return c.fail(protocol.ExitUsage, "no command given; try: ssh <host> help") | 160 | if len(argv) == 0 { |
| 146 | } | 161 | return c.fail(protocol.ExitUsage, "no command given; try: ssh <host> help") |
| 147 | } | 162 | } |
| 148 | cmd, rest, ok := Lookup(argv) | 163 | cmd, rest, ok := Lookup(argv) |
| 149 | c.Cmd = cmd | 164 | c.Cmd = cmd |
internal/control/control_test.go +32 −2
| @@ -260,6 +260,34 @@ func TestFailErrExitCodes(t *testing.T) { | |||
| 260 | } | 260 | } |
| 261 | } | 261 | } |
| 262 | 262 | ||
| 263 | // TestPathArgument: --path= is read only as a leading argument, in | ||
| 264 | // either order with --term=, and never over HTTP. | ||
| 265 | func TestPathArgument(t *testing.T) { | ||
| 266 | cases := []struct { | ||
| 267 | name string | ||
| 268 | viaAPI bool | ||
| 269 | argv []string | ||
| 270 | want string | ||
| 271 | wantArgv []string | ||
| 272 | }{ | ||
| 273 | {"leading", false, []string{"--path=auth keys remove", "keys", "remove", "abc"}, "auth keys remove", []string{"abc"}}, | ||
| 274 | {"after term", false, []string{"--term=80", "--path=auth keys remove", "keys", "remove", "abc"}, "auth keys remove", []string{"abc"}}, | ||
| 275 | {"before term", false, []string{"--path=auth keys remove", "--term=80", "keys", "remove", "abc"}, "auth keys remove", []string{"abc"}}, | ||
| 276 | {"later", false, []string{"keys", "remove", "abc", "--path=x"}, "", []string{"abc", "--path=x"}}, | ||
| 277 | {"over HTTP", true, []string{"--path=auth keys remove", "keys", "remove", "abc"}, "", []string{"abc"}}, | ||
| 278 | } | ||
| 279 | for _, tc := range cases { | ||
| 280 | c := &Ctx{Scope: "git", ViaAPI: tc.viaAPI, Stdout: io.Discard, Stderr: io.Discard} | ||
| 281 | Dispatch(c, tc.argv) | ||
| 282 | if c.CLIPath != tc.want { | ||
| 283 | t.Errorf("%s: CLIPath %q, want %q", tc.name, c.CLIPath, tc.want) | ||
| 284 | } | ||
| 285 | if !slices.Equal(c.Argv, tc.wantArgv) { | ||
| 286 | t.Errorf("%s: Argv %q, want %q", tc.name, c.Argv, tc.wantArgv) | ||
| 287 | } | ||
| 288 | } | ||
| 289 | } | ||
| 290 | |||
| 263 | // TestArgumentRefusalsNameTheUsage: a missing positional argument is a | 291 | // TestArgumentRefusalsNameTheUsage: a missing positional argument is a |
| 264 | // usage error that prints the registered usage, the shared reference | 292 | // usage error that prints the registered usage, the shared reference |
| 265 | // helpers included (#215). | 293 | // helpers included (#215). |
| @@ -267,12 +295,14 @@ func TestArgumentRefusalsNameTheUsage(t *testing.T) { | |||
| 267 | for _, argv := range [][]string{{"build", "show"}, {"release", "show"}, {"mr", "resolve"}, {"issue", "show"}} { | 295 | for _, argv := range [][]string{{"build", "show"}, {"release", "show"}, {"mr", "resolve"}, {"issue", "show"}} { |
| 268 | var out, errOut bytes.Buffer | 296 | var out, errOut bytes.Buffer |
| 269 | c := &Ctx{Scope: "full", Stdout: &out, Stderr: &errOut} | 297 | c := &Ctx{Scope: "full", Stdout: &out, Stderr: &errOut} |
| 298 | c.Cfg.Server.SiteURL = "https://forge.test" | ||
| 270 | if code := Dispatch(c, argv); code != protocol.ExitUsage { | 299 | if code := Dispatch(c, argv); code != protocol.ExitUsage { |
| 271 | t.Errorf("%v: exit %d, want %d (%s)", argv, code, protocol.ExitUsage, errOut.String()) | 300 | t.Errorf("%v: exit %d, want %d (%s)", argv, code, protocol.ExitUsage, errOut.String()) |
| 272 | continue | 301 | continue |
| 273 | } | 302 | } |
| 274 | if !strings.Contains(errOut.String(), "usage: "+strings.Join(argv, " ")) { | 303 | want := "usage: ssh git@forge.test " + strings.Join(argv, " ") |
| 275 | t.Errorf("%v: no usage line: %q", argv, errOut.String()) | 304 | if !strings.Contains(errOut.String(), want) { |
| 305 | t.Errorf("%v: got %q, want it to contain %q", argv, errOut.String(), want) | ||
| 276 | } | 306 | } |
| 277 | } | 307 | } |
| 278 | } | 308 | } |
internal/control/help.go +74 −8
| @@ -111,16 +111,80 @@ func runHelp(c *Ctx, args []string) int { | |||
| 111 | }) | 111 | }) |
| 112 | } | 112 | } |
| 113 | 113 | ||
| 114 | // program is how help spells the command it documents: the CLI at a | 114 | // viaCLI reports whether the caller is the gitbay CLI, as far as the |
| 115 | // terminal (only the CLI or a caller passing --term sets one), ssh | 115 | // session says: a terminal (only the CLI or a caller passing --term sets |
| 116 | // otherwise. | 116 | // one), or a CLI path, which only the CLI sends. |
| 117 | func (c *Ctx) viaCLI() bool { | ||
| 118 | return c.Term.Cols > 0 || c.CLIPath != "" | ||
| 119 | } | ||
| 120 | |||
| 121 | // program is how help spells the command it documents: gitbay for the | ||
| 122 | // CLI, ssh otherwise. | ||
| 117 | func (c *Ctx) program() string { | 123 | func (c *Ctx) program() string { |
| 118 | if c.Term.Cols > 0 { | 124 | if c.viaCLI() { |
| 119 | return "gitbay" | 125 | return "gitbay" |
| 120 | } | 126 | } |
| 121 | return "ssh git@" + hostOf(c.Cfg.Server.SiteURL) | 127 | return "ssh git@" + hostOf(c.Cfg.Server.SiteURL) |
| 122 | } | 128 | } |
| 123 | 129 | ||
| 130 | // cliUsage marks a leading <owner/name> optional in a usage line for the | ||
| 131 | // CLI, which infers it inside a clone (cmd/gitbay/ssh.go's withRepo). | ||
| 132 | // Stock ssh never does. | ||
| 133 | func cliUsage(usage string) string { | ||
| 134 | return strings.Replace(usage, "<owner/name>", "[<owner/name>]", 1) | ||
| 135 | } | ||
| 136 | |||
| 137 | // shownAs rewrites full, which starts with the registered path, to start | ||
| 138 | // with the CLI's path instead when the CLI sent one that differs (#267). | ||
| 139 | // The CLI path must name this command: either it regroups it (the same | ||
| 140 | // last word, auth keys remove for keys remove) or extends it (repo | ||
| 141 | // topics list for repo topics). Arguments after a CLI command can | ||
| 142 | // dispatch to a longer registered path (gitbay repo topics list add | ||
| 143 | // reaches repo topics add), and that command keeps its own name. | ||
| 144 | func (c *Ctx) shownAs(registered, full string) string { | ||
| 145 | rest, ok := strings.CutPrefix(full, registered) | ||
| 146 | if !ok || c.CLIPath == "" || c.CLIPath == registered { | ||
| 147 | return full | ||
| 148 | } | ||
| 149 | reg, cli := strings.Fields(registered), strings.Fields(c.CLIPath) | ||
| 150 | if len(reg) == 0 || len(cli) == 0 || (cli[len(cli)-1] != reg[len(reg)-1] && !strings.HasPrefix(c.CLIPath, registered+" ")) { | ||
| 151 | return full | ||
| 152 | } | ||
| 153 | return c.CLIPath + rest | ||
| 154 | } | ||
| 155 | |||
| 156 | // shownBelow is how another command listed beside registered prints to | ||
| 157 | // this caller. When the CLI only regrouped the command (auth keys remove | ||
| 158 | // for keys remove, the same last word) the other command takes the CLI's | ||
| 159 | // parent in place of the registered one. When the CLI renamed the last | ||
| 160 | // word (repo topics list for repo topics) nothing follows about the | ||
| 161 | // other command's name, so it keeps its registered path. | ||
| 162 | func (c *Ctx) shownBelow(registered, other string) string { | ||
| 163 | reg, cli, o := strings.Fields(registered), strings.Fields(c.CLIPath), strings.Fields(other) | ||
| 164 | if len(cli) == 0 || len(reg) == 0 || len(o) < len(reg) || cli[len(cli)-1] != reg[len(reg)-1] || | ||
| 165 | !slices.Equal(o[:len(reg)-1], reg[:len(reg)-1]) { | ||
| 166 | return other | ||
| 167 | } | ||
| 168 | return joinPath(append(slices.Clip(cli[:len(cli)-1]), o[len(reg)-1:]...)) | ||
| 169 | } | ||
| 170 | |||
| 171 | // usageShape is cmd's registered usage as this caller should see it: the | ||
| 172 | // CLI's path in place of the registered one where they differ, and a | ||
| 173 | // leading <owner/name> optional for the CLI. | ||
| 174 | func (c *Ctx) usageShape(cmd Command) string { | ||
| 175 | shape := c.shownAs(joinPath(cmd.Path), cmd.Usage) | ||
| 176 | if c.viaCLI() { | ||
| 177 | shape = cliUsage(shape) | ||
| 178 | } | ||
| 179 | return shape | ||
| 180 | } | ||
| 181 | |||
| 182 | // cmdUsage is the running command's usage with the program in front, as | ||
| 183 | // a usage refusal prints it. | ||
| 184 | func (c *Ctx) cmdUsage() string { | ||
| 185 | return c.program() + " " + c.usageShape(c.Cmd) | ||
| 186 | } | ||
| 187 | |||
| 124 | func (c *Ctx) heading(w io.Writer, s string) { | 188 | func (c *Ctx) heading(w io.Writer, s string) { |
| 125 | fmt.Fprintln(w, c.Term.paint(sgrBold, s)) | 189 | fmt.Fprintln(w, c.Term.paint(sgrBold, s)) |
| 126 | } | 190 | } |
| @@ -152,7 +216,8 @@ func (c *Ctx) helpVerb(w io.Writer, cmd Command, below []Command) { | |||
| 152 | // A required flag (repo delete --yes) or an alternative | 216 | // A required flag (repo delete --yes) or an alternative |
| 153 | // (notifications read <id>... | --all) has no " [--" to cut at, so | 217 | // (notifications read <id>... | --all) has no " [--" to cut at, so |
| 154 | // the usage prints whole. | 218 | // the usage prints whole. |
| 155 | shape := cmd.Usage | 219 | registered := joinPath(cmd.Path) |
| 220 | shape := c.usageShape(cmd) | ||
| 156 | if i := strings.Index(shape, " [--"); i >= 0 { | 221 | if i := strings.Index(shape, " [--"); i >= 0 { |
| 157 | shape = shape[:i] + " [flags]" | 222 | shape = shape[:i] + " [flags]" |
| 158 | } | 223 | } |
| @@ -190,7 +255,7 @@ func (c *Ctx) helpVerb(w io.Writer, cmd Command, below []Command) { | |||
| 190 | fmt.Fprintln(w) | 255 | fmt.Fprintln(w) |
| 191 | c.heading(w, "SEE ALSO") | 256 | c.heading(w, "SEE ALSO") |
| 192 | for _, b := range below { | 257 | for _, b := range below { |
| 193 | fmt.Fprintf(w, " %s %s\n", c.program(), joinPath(b.Path)) | 258 | fmt.Fprintf(w, " %s %s\n", c.program(), c.shownBelow(registered, joinPath(b.Path))) |
| 194 | } | 259 | } |
| 195 | } | 260 | } |
| 196 | } | 261 | } |
| @@ -200,7 +265,8 @@ func (c *Ctx) helpNoun(w io.Writer, prefix string, cmds []Command) { | |||
| 200 | fmt.Fprintln(w, head) | 265 | fmt.Fprintln(w, head) |
| 201 | fmt.Fprintln(w) | 266 | fmt.Fprintln(w) |
| 202 | c.heading(w, "USAGE") | 267 | c.heading(w, "USAGE") |
| 203 | fmt.Fprintf(w, " %s %s <verb> ...\n", c.program(), prefix) | 268 | display := c.shownAs(prefix, prefix) |
| 269 | fmt.Fprintf(w, " %s %s <verb> ...\n", c.program(), display) | ||
| 204 | wide := 0 | 270 | wide := 0 |
| 205 | for _, cmd := range cmds { | 271 | for _, cmd := range cmds { |
| 206 | wide = max(wide, cells(strings.TrimPrefix(joinPath(cmd.Path), prefix+" "))) | 272 | wide = max(wide, cells(strings.TrimPrefix(joinPath(cmd.Path), prefix+" "))) |
| @@ -224,5 +290,5 @@ func (c *Ctx) helpNoun(w io.Writer, prefix string, cmds []Command) { | |||
| 224 | } | 290 | } |
| 225 | } | 291 | } |
| 226 | fmt.Fprintln(w) | 292 | fmt.Fprintln(w) |
| 227 | fmt.Fprintf(w, "%s %s <verb> --help for flags.\n", c.program(), prefix) | 293 | fmt.Fprintf(w, "%s %s <verb> --help for flags.\n", c.program(), display) |
| 228 | } | 294 | } |
internal/control/help_test.go +101 −2
| @@ -25,7 +25,7 @@ func TestHelpVerb(t *testing.T) { | |||
| 25 | out := helpOut(t, Term{Cols: 100}, "issue", "list") | 25 | out := helpOut(t, Term{Cols: 100}, "issue", "list") |
| 26 | for _, want := range []string{ | 26 | for _, want := range []string{ |
| 27 | "list issues\n", | 27 | "list issues\n", |
| 28 | "USAGE\n gitbay issue list <owner/name> [flags]\n", | 28 | "USAGE\n gitbay issue list [<owner/name>] [flags]\n", |
| 29 | "FLAGS\n", | 29 | "FLAGS\n", |
| 30 | " --state open|closed|all", | 30 | " --state open|closed|all", |
| 31 | "which issues (default open)\n", | 31 | "which issues (default open)\n", |
| @@ -50,7 +50,7 @@ func TestHelpVerb(t *testing.T) { | |||
| 50 | // as though --yes were optional. | 50 | // as though --yes were optional. |
| 51 | func TestHelpVerbUsageKeepsRequiredFlags(t *testing.T) { | 51 | func TestHelpVerbUsageKeepsRequiredFlags(t *testing.T) { |
| 52 | out := helpOut(t, Term{Cols: 100}, "repo", "delete") | 52 | out := helpOut(t, Term{Cols: 100}, "repo", "delete") |
| 53 | if !strings.Contains(out, "USAGE\n gitbay repo delete <owner/name> --yes\n") { | 53 | if !strings.Contains(out, "USAGE\n gitbay repo delete [<owner/name>] --yes\n") { |
| 54 | t.Errorf("missing required --yes in usage:\n%s", out) | 54 | t.Errorf("missing required --yes in usage:\n%s", out) |
| 55 | } | 55 | } |
| 56 | if strings.Contains(out, "[flags]") { | 56 | if strings.Contains(out, "[flags]") { |
| @@ -142,3 +142,102 @@ func TestHelpIsComplete(t *testing.T) { | |||
| 142 | } | 142 | } |
| 143 | } | 143 | } |
| 144 | } | 144 | } |
| 145 | |||
| 146 | func TestCmdUsagePrefixesTheProgram(t *testing.T) { | ||
| 147 | c := &Ctx{Cmd: Command{Path: []string{"keys", "remove"}, Usage: "keys remove <fingerprint>"}} | ||
| 148 | c.Cfg.Server.SiteURL = "https://forge.test" | ||
| 149 | if got := c.cmdUsage(); got != "ssh git@forge.test keys remove <fingerprint>" { | ||
| 150 | t.Errorf("ssh form: %q", got) | ||
| 151 | } | ||
| 152 | c.Term = Term{Cols: 100} | ||
| 153 | if got := c.cmdUsage(); got != "gitbay keys remove <fingerprint>" { | ||
| 154 | t.Errorf("cli form, no CLIPath sent: %q", got) | ||
| 155 | } | ||
| 156 | c.CLIPath = "auth keys remove" | ||
| 157 | if got := c.cmdUsage(); got != "gitbay auth keys remove <fingerprint>" { | ||
| 158 | t.Errorf("cli form, mismatched registered path: %q", got) | ||
| 159 | } | ||
| 160 | c.Term = Term{} | ||
| 161 | if got := c.cmdUsage(); got != "gitbay auth keys remove <fingerprint>" { | ||
| 162 | t.Errorf("cli form off a terminal, mismatched registered path: %q", got) | ||
| 163 | } | ||
| 164 | |||
| 165 | c2 := &Ctx{Cmd: Command{Path: []string{"repo", "tree"}, Usage: "repo tree <owner/name> [<path>] [--ref <ref>]"}, Term: Term{Cols: 100}} | ||
| 166 | if got := c2.cmdUsage(); got != "gitbay repo tree [<owner/name>] [<path>] [--ref <ref>]" { | ||
| 167 | t.Errorf("optional owner/name: %q", got) | ||
| 168 | } | ||
| 169 | c2.CLIPath = "repo tree" | ||
| 170 | if got := c2.cmdUsage(); got != "gitbay repo tree [<owner/name>] [<path>] [--ref <ref>]" { | ||
| 171 | t.Errorf("matching CLIPath changes nothing: %q", got) | ||
| 172 | } | ||
| 173 | |||
| 174 | c3 := &Ctx{Cmd: Command{Path: []string{"repo", "topics", "add"}, Usage: "repo topics add <owner/name> <topic>..."}, Term: Term{Cols: 100}, CLIPath: "repo topics list"} | ||
| 175 | if got := c3.cmdUsage(); got != "gitbay repo topics add [<owner/name>] <topic>..." { | ||
| 176 | t.Errorf("CLIPath naming another command: %q", got) | ||
| 177 | } | ||
| 178 | c3.Cmd = Command{Path: []string{"repo", "topics"}, Usage: "repo topics <owner/name>"} | ||
| 179 | if got := c3.cmdUsage(); got != "gitbay repo topics list [<owner/name>]" { | ||
| 180 | t.Errorf("CLIPath extending the registered path: %q", got) | ||
| 181 | } | ||
| 182 | c3.CLIPath = " " | ||
| 183 | if got := c3.cmdUsage(); got != "gitbay repo topics [<owner/name>]" { | ||
| 184 | t.Errorf("blank CLIPath: %q", got) | ||
| 185 | } | ||
| 186 | } | ||
| 187 | |||
| 188 | // TestShownBelow: another command listed beside the one the CLI named | ||
| 189 | // takes the CLI's parent when the CLI only regrouped the command (auth | ||
| 190 | // keys remove), and keeps its registered path when the CLI renamed the | ||
| 191 | // leaf (repo topics list for repo topics), since that says nothing about | ||
| 192 | // what the other command is called. | ||
| 193 | func TestShownBelow(t *testing.T) { | ||
| 194 | cases := []struct { | ||
| 195 | cliPath, registered, other, want string | ||
| 196 | }{ | ||
| 197 | {"", "keys remove", "keys list", "keys list"}, | ||
| 198 | {"keys remove", "keys remove", "keys list", "keys list"}, | ||
| 199 | {"auth keys remove", "keys remove", "keys list", "auth keys list"}, | ||
| 200 | {"auth export", "account export", "account export extra", "auth export extra"}, | ||
| 201 | {"repo topics list", "repo topics", "repo topics add", "repo topics add"}, | ||
| 202 | } | ||
| 203 | for _, tc := range cases { | ||
| 204 | c := &Ctx{CLIPath: tc.cliPath} | ||
| 205 | if got := c.shownBelow(tc.registered, tc.other); got != tc.want { | ||
| 206 | t.Errorf("CLIPath %q, %q beside %q: got %q, want %q", tc.cliPath, tc.other, tc.registered, got, tc.want) | ||
| 207 | } | ||
| 208 | } | ||
| 209 | } | ||
| 210 | |||
| 211 | // TestHelpPrintsTheCLIPath: help reached through the CLI with a --path= | ||
| 212 | // prints the CLI's path in USAGE, the noun header and SEE ALSO. | ||
| 213 | func TestHelpPrintsTheCLIPath(t *testing.T) { | ||
| 214 | via := func(t *testing.T, term Term, argv ...string) string { | ||
| 215 | t.Helper() | ||
| 216 | var out, errOut bytes.Buffer | ||
| 217 | c := &Ctx{Stdout: &out, Stderr: &errOut, Term: term, Scope: "full"} | ||
| 218 | c.Cfg.Server.SiteURL = "https://forge.test" | ||
| 219 | if code := Dispatch(c, argv); code != protocol.ExitOK { | ||
| 220 | t.Fatalf("%v: exit %d: %s", argv, code, errOut.String()) | ||
| 221 | } | ||
| 222 | return out.String() | ||
| 223 | } | ||
| 224 | verb := via(t, Term{Cols: 100}, "--path=auth keys remove", "help", "keys", "remove") | ||
| 225 | if !strings.Contains(verb, "USAGE\n gitbay auth keys remove <fingerprint>") { | ||
| 226 | t.Errorf("verb usage:\n%s", verb) | ||
| 227 | } | ||
| 228 | topics := via(t, Term{Cols: 100}, "--path=repo topics list", "help", "repo", "topics") | ||
| 229 | for _, want := range []string{"USAGE\n gitbay repo topics list [<owner/name>]", "SEE ALSO\n gitbay repo topics add\n"} { | ||
| 230 | if !strings.Contains(topics, want) { | ||
| 231 | t.Errorf("missing %q in:\n%s", want, topics) | ||
| 232 | } | ||
| 233 | } | ||
| 234 | if strings.Contains(topics, "topics list add") { | ||
| 235 | t.Errorf("SEE ALSO renamed a child after the leaf:\n%s", topics) | ||
| 236 | } | ||
| 237 | noun := via(t, Term{Cols: 100}, "--path=auth keys", "help", "keys") | ||
| 238 | for _, want := range []string{"USAGE\n gitbay auth keys <verb> ...\n", "gitbay auth keys <verb> --help for flags.\n"} { | ||
| 239 | if !strings.Contains(noun, want) { | ||
| 240 | t.Errorf("missing %q in:\n%s", want, noun) | ||
| 241 | } | ||
| 242 | } | ||
| 243 | } | ||