Skip to content

Commit 51fe0a4

Browse files
help: generate the 'src help' command list from the registered commands
The "The commands are:" block in 'src help' was a hand-maintained string in main.go. It had drifted from the registered commands: 'debug', 'snapshot', and 'lsp' were never added, and the codeowners description differed from the command's own Usage. Build the list at runtime from both registries (legacy 'commands' and urfave/cli 'migratedCommands'). Legacy commands get a 'description' field and a 'hidden' flag; 'src doc' uses the same flag instead of hard-coding the names to skip. 'version' gets a Usage so it has a description. Tests keep the three lists apples-to-apples: - 'src help' == registered commands (TestHelpListsAllRegisteredCommands) - 'src doc' root index == 'src help' == registered (TestDocRootIndexMatchesHelp) - every visible command has a description (TestRootCommandsAreWellFormed) Amp-Thread-ID: https://ampcode.com/threads/T-01a08410-86ca-72be-9928-2810e837fae1 Co-authored-by: Amp <amp@ampcode.com>
1 parent 89e7de3 commit 51fe0a4

19 files changed

Lines changed: 305 additions & 96 deletions

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ All notable changes to `src-cli` are documented in this file.
1717

1818
- HTTP requests now fail instead of hanging forever if the server does not start responding within 1 minute. Set the `SRC_RESPONSE_HEADER_TIMEOUT` environment variable to change this timeout, or to `0` to disable it. Responses that stream data for a long time (for example, large search job results) are not affected.
1919
- `src search-jobs logs` and `src search-jobs results` now use the standard API client, gaining proxy support, `-insecure-skip-verify`, and cross-host redirect protection, and now report an error on non-200 responses instead of writing the error page into the output.
20+
- The command list in `src help` is now generated from the registered commands instead of being maintained by hand. `src debug`, `src snapshot`, and `src lsp` now appear in it; aliases are shown after each description.
2021

2122
### Fixed
2223

‎cmd/src/batch.go‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ Use "src batch [command] -h" for more information about a command.
3737

3838
// Register the command.
3939
commands = append(commands, &command{
40-
flagSet: flagSet,
40+
flagSet: flagSet,
41+
description: "manages batch changes",
4142
aliases: []string{
4243
"batchchange",
4344
"batch-change",

‎cmd/src/cmd.go‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,14 @@ type command struct {
2222
// flagSet.Usage function to invoke on e.g. -h flag. If nil, a default one is
2323
// used.
2424
usageFunc func()
25+
26+
// description is the one-line summary shown next to the command in
27+
// 'src help'. Required for top-level commands unless hidden is set.
28+
description string
29+
30+
// hidden excludes the command from 'src help' and from the reference
31+
// documentation generated by 'src doc'. It can still be run.
32+
hidden bool
2533
}
2634

2735
// matches tells if the given name matches this command or one of its aliases.

‎cmd/src/code_intel.go‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,10 @@ Use "src code-intel [command] -h" for more information about a command.
2828

2929
// Register the command.
3030
commands = append(commands, &command{
31-
flagSet: flagSet,
32-
aliases: []string{"code-intel"},
33-
handler: handler,
31+
flagSet: flagSet,
32+
description: "manages code intelligence data",
33+
aliases: []string{"code-intel"},
34+
handler: handler,
3435
usageFunc: func() {
3536
fmt.Println(usage)
3637
},

‎cmd/src/config.go‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,9 @@ Use "src config [command] -h" for more information about a command.
4545

4646
// Register the command.
4747
commands = append(commands, &command{
48-
flagSet: flagSet,
49-
handler: handler,
48+
flagSet: flagSet,
49+
description: "manages global, org, and user settings",
50+
handler: handler,
5051
usageFunc: func() {
5152
fmt.Println(usage)
5253
},

‎cmd/src/debug.go‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,9 @@ src debug has access to flags on src -- Ex: src -v kube -o foo.zip
3434

3535
// Register the command.
3636
commands = append(commands, &command{
37-
flagSet: flagSet,
38-
aliases: []string{},
39-
handler: handler,
40-
usageFunc: func() { fmt.Println(usage) },
37+
flagSet: flagSet,
38+
description: "gathers and bundles debug data from a Sourcegraph deployment for troubleshooting",
39+
handler: handler,
40+
usageFunc: func() { fmt.Println(usage) },
4141
})
4242
}

‎cmd/src/doc.go‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ Examples:
8282
name,
8383
}, " "))
8484

85-
if fqcn == "doc" || fqcn == "publish" {
85+
if cmd.hidden {
8686
continue
8787
}
8888

@@ -176,6 +176,7 @@ Examples:
176176

177177
commands = append(commands, &command{
178178
flagSet: flagSet,
179+
hidden: true,
179180
handler: handler,
180181
usageFunc: func() {
181182
fmt.Fprintln(flag.CommandLine.Output(), usage)

‎cmd/src/doc_test.go‎

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -187,28 +187,34 @@ func TestDocLegacyGroupsHaveSubcommandPages(t *testing.T) {
187187
}
188188
}
189189

190-
// The root index must link every top-level command, both legacy (commander)
191-
// and migrated (urfave/cli) ones.
192-
func TestDocRootIndexListsAllCommands(t *testing.T) {
190+
// The root index.md written by 'src doc' must link exactly the commands that
191+
// 'src help' lists, which in turn must be exactly the registered commands.
192+
// A command that is registered but missing from either is a bug.
193+
func TestDocRootIndexMatchesHelp(t *testing.T) {
193194
dir, _ := runDocCommand(t)
194195

195196
index, err := os.ReadFile(filepath.Join(dir, "index.md"))
196197
if err != nil {
197198
t.Fatal(err)
198199
}
199200

200-
var missing []string
201-
for _, cmd := range commands {
202-
name := cmd.flagSet.Name()
203-
if name == "doc" || name == "publish" {
201+
var indexed []string
202+
for _, line := range strings.Split(string(index), "\n") {
203+
// Lines look like: * [`name`](name.md) or * [`name`](name/index.md)
204+
rest, ok := strings.CutPrefix(strings.TrimSpace(line), "* [`")
205+
if !ok {
204206
continue
205207
}
206-
if !strings.Contains(string(index), "[`"+name+"`](") {
207-
missing = append(missing, name)
208-
}
208+
name, _, _ := strings.Cut(rest, "`")
209+
indexed = append(indexed, name)
210+
}
211+
sort.Strings(indexed)
212+
213+
registered := registeredRootCommandNames()
214+
if diff := cmp.Diff(registered, indexed); diff != "" {
215+
t.Errorf("'src doc' root index does not match the registered commands (-registered +index):\n%s", diff)
209216
}
210-
if len(missing) > 0 {
211-
sort.Strings(missing)
212-
t.Errorf("root index.md is missing legacy commands: %v", missing)
217+
if diff := cmp.Diff(helpCommandNames(t, usageText()), indexed); diff != "" {
218+
t.Errorf("'src doc' root index does not match 'src help' (-help +index):\n%s", diff)
213219
}
214220
}

‎cmd/src/extsvc.go‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,10 @@ Use "src extsvc [command] -h" for more information about a command.
3636

3737
// Register the command.
3838
commands = append(commands, &command{
39-
flagSet: flagSet,
40-
aliases: []string{"extsvc", "external-service"},
41-
handler: handler,
39+
flagSet: flagSet,
40+
description: "manages external services",
41+
aliases: []string{"extsvc", "external-service"},
42+
handler: handler,
4243
usageFunc: func() {
4344
fmt.Println(usage)
4445
},

‎cmd/src/help.go‎

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
package main
2+
3+
import (
4+
"cmp"
5+
"fmt"
6+
"slices"
7+
"strings"
8+
9+
"github.com/sourcegraph/sourcegraph/lib/docgen"
10+
)
11+
12+
// rootCommand is a top-level 'src' command as shown in 'src help'. It is the
13+
// single source for the command list in the help text and for the tests that
14+
// keep 'src help' and the 'src doc' root index in sync.
15+
type rootCommand struct {
16+
name string
17+
aliases []string
18+
description string
19+
}
20+
21+
// rootCommands returns every visible top-level command, whether it is
22+
// registered with the legacy commander (commands) or with urfave/cli
23+
// (migratedCommands), sorted by name.
24+
func rootCommands() []rootCommand {
25+
var root []rootCommand
26+
27+
for _, cmd := range commands {
28+
if cmd.hidden {
29+
continue
30+
}
31+
name := cmd.flagSet.Name()
32+
var aliases []string
33+
for _, alias := range cmd.aliases {
34+
// Some legacy commands register their own name as an alias.
35+
if alias != name {
36+
aliases = append(aliases, alias)
37+
}
38+
}
39+
root = append(root, rootCommand{
40+
name: name,
41+
aliases: aliases,
42+
description: cmd.description,
43+
})
44+
}
45+
46+
for _, cmd := range docgen.VisibleCommands(migratedRootCommand().Commands) {
47+
root = append(root, rootCommand{
48+
name: cmd.Name,
49+
aliases: slices.Clone(cmd.Aliases),
50+
description: cmd.Usage,
51+
})
52+
}
53+
54+
slices.SortFunc(root, func(a, b rootCommand) int {
55+
return cmp.Compare(a.name, b.name)
56+
})
57+
return root
58+
}
59+
60+
// formatCommandList renders the "The commands are:" block of 'src help':
61+
// one tab-indented line per command with the name padded to a common width,
62+
// the description, and any aliases in parentheses.
63+
func formatCommandList(cmds []rootCommand) string {
64+
width := 0
65+
for _, cmd := range cmds {
66+
width = max(width, len(cmd.name))
67+
}
68+
69+
var b strings.Builder
70+
for _, cmd := range cmds {
71+
fmt.Fprintf(&b, "\t%-*s %s", width, cmd.name, cmd.description)
72+
if len(cmd.aliases) > 0 {
73+
fmt.Fprintf(&b, " (alias: %s)", strings.Join(cmd.aliases, ", "))
74+
}
75+
b.WriteString("\n")
76+
}
77+
return b.String()
78+
}
79+
80+
// usageText renders the top-level 'src help' output.
81+
func usageText() string {
82+
return usageHeader + formatCommandList(rootCommands()) + usageFooter
83+
}
84+
85+
const usageHeader = `src is a tool that provides access to Sourcegraph instances.
86+
For more information, see https://github.com/sourcegraph/src-cli
87+
88+
Usage:
89+
90+
src [options] command [command options]
91+
92+
Environment variables
93+
SRC_ACCESS_TOKEN Sourcegraph access token
94+
SRC_ENDPOINT endpoint to use, if unset will default to "https://sourcegraph.com"
95+
SRC_PROXY A proxy to use for proxying requests to the Sourcegraph endpoint.
96+
Supports HTTP(S), SOCKS5/5h, and UNIX Domain Socket proxies.
97+
If a UNIX Domain Socket, the path can be either an absolute path,
98+
or can start with ~/ or %USERPROFILE%\ for a path in the user's home directory.
99+
Examples:
100+
- https://localhost:3080
101+
- https://<user>:<password>localhost:8080
102+
- socks5h://localhost:1080
103+
- socks5://<username>:<password>@localhost:1080
104+
- unix://~/src-proxy.sock
105+
- unix://%USERPROFILE%\src-proxy.sock
106+
- ~/src-proxy.sock
107+
- %USERPROFILE%\src-proxy.sock
108+
- C:\some\path\src-proxy.sock
109+
110+
The options are:
111+
112+
-v print verbose output
113+
114+
The commands are:
115+
116+
`
117+
118+
const usageFooter = `
119+
Use "src [command] -h" for more information about a command.
120+
121+
`

0 commit comments

Comments
 (0)