Skip to content

Commit 056be8b

Browse files
authored
feat/cli: migrate version subcommand to use urface/cli (#1292)
* add runLegacy * use go 1.26 * add util methods to work urfave/cli flags in api * tweak help template to match legacy help * split command runner into legacy and migrated * migrate version to use urface cli * add onUsageError func and rename funcs to be 'Wrap*' * use Wrap from clicompat instead * remove init * update comments and method names of clicompat api flags * move runMigrated to be in main and work with os.Args * fix lint
1 parent cdb00ca commit 056be8b

File tree

10 files changed

+304
-64
lines changed

10 files changed

+304
-64
lines changed

cmd/src/cmd.go

Lines changed: 6 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@ import (
66
"log"
77
"os"
88
"slices"
9-
10-
"github.com/sourcegraph/src-cli/internal/cmderrors"
119
)
1210

1311
// command is a subcommand handler and its flag set.
@@ -68,43 +66,25 @@ func (c commander) run(flagSet *flag.FlagSet, cmdName, usageText string, args []
6866

6967
// Find the subcommand to execute.
7068
name := flagSet.Arg(0)
69+
70+
// Command is legacy, so lets execute the old way
7171
for _, cmd := range c {
7272
if !cmd.matches(name) {
7373
continue
7474
}
75-
7675
// Read global configuration now.
7776
var err error
7877
cfg, err = readConfig()
7978
if err != nil {
8079
log.Fatal("reading config: ", err)
8180
}
8281

83-
// Parse subcommand flags.
84-
args := flagSet.Args()[1:]
85-
if err := cmd.flagSet.Parse(args); err != nil {
86-
fmt.Printf("Error parsing subcommand flags: %s\n", err)
87-
panic(fmt.Sprintf("all registered commands should use flag.ExitOnError: error: %s", err))
88-
}
89-
90-
// Execute the subcommand.
91-
if err := cmd.handler(flagSet.Args()[1:]); err != nil {
92-
if _, ok := err.(*cmderrors.UsageError); ok {
93-
log.Printf("error: %s\n\n", err)
94-
cmd.flagSet.SetOutput(os.Stderr)
95-
flag.CommandLine.SetOutput(os.Stderr)
96-
cmd.flagSet.Usage()
97-
os.Exit(2)
98-
}
99-
if e, ok := err.(*cmderrors.ExitCodeError); ok {
100-
if e.HasError() {
101-
log.Println(e)
102-
}
103-
os.Exit(e.Code())
104-
}
82+
exitCode, err := runLegacy(cmd, flagSet)
83+
if err != nil {
10584
log.Fatal(err)
10685
}
107-
os.Exit(0)
86+
os.Exit(exitCode)
87+
10888
}
10989
log.Printf("%s: unknown subcommand %q", cmdName, name)
11090
log.Fatalf("Run '%s help' for usage.", cmdName)

cmd/src/main.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,18 @@ func main() {
9393
log.SetFlags(0)
9494
log.SetPrefix("")
9595

96-
commands.run(flag.CommandLine, "src", usageText, normalizeDashHelp(os.Args[1:]))
96+
ranMigratedCmd, exitCode, err := maybeRunMigratedCommand()
97+
if ranMigratedCmd {
98+
if err != nil {
99+
log.Println(err)
100+
}
101+
os.Exit(exitCode)
102+
}
103+
104+
// if we didn't run a migrated command, then lets try running the legacy version
105+
if !ranMigratedCmd {
106+
commands.run(flag.CommandLine, "src", usageText, normalizeDashHelp(os.Args[1:]))
107+
}
97108
}
98109

99110
// normalizeDashHelp converts --help to -help since Go's flag parser only supports single dash.

cmd/src/run_migration_compat.go

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"flag"
6+
"fmt"
7+
"log"
8+
"os"
9+
"sort"
10+
11+
"github.com/sourcegraph/src-cli/internal/clicompat"
12+
"github.com/sourcegraph/src-cli/internal/cmderrors"
13+
"github.com/urfave/cli/v3"
14+
15+
"github.com/sourcegraph/sourcegraph/lib/errors"
16+
)
17+
18+
var migratedCommands = map[string]*cli.Command{
19+
"version": versionCommand,
20+
}
21+
22+
func maybeRunMigratedCommand() (isMigrated bool, exitCode int, err error) {
23+
// need to figure out if a migrated command has been requested
24+
flag.Parse()
25+
subCommand := flag.CommandLine.Arg(0)
26+
_, isMigrated = migratedCommands[subCommand]
27+
if !isMigrated {
28+
return
29+
}
30+
cfg, err = readConfig()
31+
if err != nil {
32+
log.Fatal("reading config: ", err)
33+
}
34+
35+
exitCode, err = runMigrated()
36+
return
37+
}
38+
39+
// migratedRootCommand constructs a root 'src' command and adds
40+
// MigratedCommands as subcommands to it
41+
func migratedRootCommand() *cli.Command {
42+
names := make([]string, 0, len(migratedCommands))
43+
for name := range migratedCommands {
44+
names = append(names, name)
45+
}
46+
sort.Strings(names)
47+
48+
commands := make([]*cli.Command, 0, len(names))
49+
for _, name := range names {
50+
commands = append(commands, migratedCommands[name])
51+
}
52+
53+
return clicompat.WrapRoot(&cli.Command{
54+
Name: "src",
55+
HideVersion: true,
56+
Commands: commands,
57+
})
58+
}
59+
60+
// runMigrated runs the command within urfave/cli framework
61+
func runMigrated() (int, error) {
62+
ctx := context.Background()
63+
64+
err := migratedRootCommand().Run(ctx, os.Args)
65+
if errors.HasType[*cmderrors.UsageError](err) {
66+
return 2, nil
67+
}
68+
var exitErr cli.ExitCoder
69+
if errors.AsInterface(err, &exitErr) {
70+
return exitErr.ExitCode(), err
71+
}
72+
return 0, err
73+
}
74+
75+
// runLegacy runs the command using the original commander framework
76+
func runLegacy(cmd *command, flagSet *flag.FlagSet) (int, error) {
77+
// Parse subcommand flags.
78+
args := flagSet.Args()[1:]
79+
if err := cmd.flagSet.Parse(args); err != nil {
80+
fmt.Printf("Error parsing subcommand flags: %s\n", err)
81+
panic(fmt.Sprintf("all registered commands should use flag.ExitOnError: error: %s", err))
82+
}
83+
84+
// Execute the subcommand.
85+
if err := cmd.handler(flagSet.Args()[1:]); err != nil {
86+
if _, ok := err.(*cmderrors.UsageError); ok {
87+
log.Printf("error: %s\n\n", err)
88+
cmd.flagSet.SetOutput(os.Stderr)
89+
flag.CommandLine.SetOutput(os.Stderr)
90+
cmd.flagSet.Usage()
91+
return 2, nil
92+
}
93+
if e, ok := err.(*cmderrors.ExitCodeError); ok {
94+
if e.HasError() {
95+
log.Println(e)
96+
}
97+
return e.Code(), nil
98+
}
99+
return 1, err
100+
}
101+
return 0, nil
102+
}

cmd/src/version.go

Lines changed: 46 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -3,63 +3,73 @@ package main
33
import (
44
"context"
55
"encoding/json"
6-
"flag"
76
"fmt"
87
"io"
98
"net/http"
9+
"os"
1010

1111
"github.com/sourcegraph/sourcegraph/lib/errors"
1212

1313
"github.com/sourcegraph/src-cli/internal/api"
14+
"github.com/sourcegraph/src-cli/internal/clicompat"
1415
"github.com/sourcegraph/src-cli/internal/version"
16+
17+
"github.com/urfave/cli/v3"
1518
)
1619

17-
func init() {
18-
usage := `
19-
Examples:
20+
const versionExamples = `Examples:
2021
2122
Get the src-cli version and the Sourcegraph instance's recommended version:
2223
2324
$ src version
2425
`
2526

26-
flagSet := flag.NewFlagSet("version", flag.ExitOnError)
27-
28-
var (
29-
clientOnly = flagSet.Bool("client-only", false, "If true, only the client version will be printed.")
30-
apiFlags = api.NewFlags(flagSet)
31-
)
32-
33-
handler := func(args []string) error {
34-
fmt.Printf("Current version: %s\n", version.BuildTag)
35-
if clientOnly != nil && *clientOnly {
36-
return nil
27+
var versionCommand = clicompat.Wrap(&cli.Command{
28+
Name: "version",
29+
Usage: "display and compare the src-cli version against the recommended version for your instance",
30+
UsageText: "src version [options]",
31+
OnUsageError: clicompat.OnUsageError,
32+
Description: `
33+
` + versionExamples,
34+
Flags: clicompat.WithAPIFlags(
35+
&cli.BoolFlag{
36+
Name: "client-only",
37+
Usage: "If true, only the client version will be printed.",
38+
},
39+
),
40+
HideVersion: true,
41+
Action: func(ctx context.Context, c *cli.Command) error {
42+
args := VersionArgs{
43+
Client: cfg.apiClient(clicompat.APIFlagsFromCmd(c), os.Stdout),
44+
ClientOnly: c.Bool("client-only"),
3745
}
46+
return versionHandler(args)
47+
},
48+
})
49+
50+
type VersionArgs struct {
51+
ClientOnly bool
52+
Client api.Client
53+
Output io.Writer
54+
}
3855

39-
client := cfg.apiClient(apiFlags, flagSet.Output())
40-
recommendedVersion, err := getRecommendedVersion(context.Background(), client)
41-
if err != nil {
42-
return errors.Wrap(err, "failed to get recommended version for Sourcegraph deployment")
43-
}
44-
if recommendedVersion == "" {
45-
fmt.Println("Recommended version: <unknown>")
46-
fmt.Println("This Sourcegraph instance does not support this feature.")
47-
return nil
48-
}
49-
fmt.Printf("Recommended version: %s or later\n", recommendedVersion)
56+
func versionHandler(args VersionArgs) error {
57+
fmt.Printf("Current version: %s\n", version.BuildTag)
58+
if args.ClientOnly {
5059
return nil
5160
}
5261

53-
// Register the command.
54-
commands = append(commands, &command{
55-
flagSet: flagSet,
56-
handler: handler,
57-
usageFunc: func() {
58-
fmt.Fprintf(flag.CommandLine.Output(), "Usage of 'src %s':\n", flagSet.Name())
59-
flagSet.PrintDefaults()
60-
fmt.Println(usage)
61-
},
62-
})
62+
recommendedVersion, err := getRecommendedVersion(context.Background(), args.Client)
63+
if err != nil {
64+
return errors.Wrap(err, "failed to get recommended version for Sourcegraph deployment")
65+
}
66+
if recommendedVersion == "" {
67+
fmt.Println("Recommended version: <unknown>")
68+
fmt.Println("This Sourcegraph instance does not support this feature.")
69+
return nil
70+
}
71+
fmt.Printf("Recommended version: %s or later\n", recommendedVersion)
72+
return nil
6373
}
6474

6575
func getRecommendedVersion(ctx context.Context, client api.Client) (string, error) {

go.mod

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module github.com/sourcegraph/src-cli
22

3-
go 1.25.8
3+
go 1.26
44

55
require (
66
cloud.google.com/go/storage v1.50.0
@@ -83,6 +83,7 @@ require (
8383
github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect
8484
github.com/tliron/commonlog v0.2.19 // indirect
8585
github.com/tliron/kutil v0.3.27 // indirect
86+
github.com/urfave/cli/v3 v3.8.0 // indirect
8687
github.com/x448/float16 v0.8.4 // indirect
8788
github.com/xlab/treeprint v1.2.0 // indirect
8889
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,8 @@ github.com/tliron/glsp v0.2.2 h1:IKPfwpE8Lu8yB6Dayta+IyRMAbTVunudeauEgjXBt+c=
370370
github.com/tliron/glsp v0.2.2/go.mod h1:GMVWDNeODxHzmDPvYbYTCs7yHVaEATfYtXiYJ9w1nBg=
371371
github.com/tliron/kutil v0.3.27 h1:Wb0V5jdbTci6Let1tiGY741J/9FIynmV/pCsPDPsjcM=
372372
github.com/tliron/kutil v0.3.27/go.mod h1:AHeLNIFBSKBU39ELVHZdkw2f/ez2eKGAAGoxwBlhMi8=
373+
github.com/urfave/cli/v3 v3.8.0 h1:XqKPrm0q4P0q5JpoclYoCAv0/MIvH/jZ2umzuf8pNTI=
374+
github.com/urfave/cli/v3 v3.8.0/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
373375
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
374376
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
375377
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=

internal/api/flags.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,19 @@ func NewFlags(flagSet *flag.FlagSet) *Flags {
4848
}
4949
}
5050

51+
// NewFlagsFromValues instantiates a new Flags structure from explicit values.
52+
// This is used by cli/v3 compatibility adapters that do not operate on a
53+
// standard flag.FlagSet.
54+
func NewFlagsFromValues(dump, getCurl, trace, insecureSkipVerify, userAgentTelemetry bool) *Flags {
55+
return &Flags{
56+
dump: new(dump),
57+
getCurl: new(getCurl),
58+
trace: new(trace),
59+
insecureSkipVerify: new(insecureSkipVerify),
60+
userAgentTelemetry: new(userAgentTelemetry),
61+
}
62+
}
63+
5164
func defaultFlags() *Flags {
5265
telemetry := defaultUserAgentTelemetry()
5366
d := false

internal/clicompat/api_flags.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package clicompat
2+
3+
import (
4+
"os"
5+
6+
"github.com/sourcegraph/src-cli/internal/api"
7+
"github.com/urfave/cli/v3"
8+
)
9+
10+
// WithAPIFlags appends the standard API-related flags used by legacy src
11+
func WithAPIFlags(baseFlags ...cli.Flag) []cli.Flag {
12+
var flagTable = []struct {
13+
name string
14+
value bool
15+
text string
16+
}{
17+
{"dump-requests", false, "Log GraphQL requests and responses to stdout"},
18+
{"get-curl", false, "Print the curl command for executing this query and exit (WARNING: includes printing your access token!)"},
19+
{"trace", false, "Log the trace ID for requests. See https://docs.sourcegraph.com/admin/observability/tracing"},
20+
{"insecure-skip-verify", false, "Skip validation of TLS certificates against trusted chains"},
21+
{"user-agent-telemetry", defaultAPIUserAgentTelemetry(), "Include the operating system and architecture in the User-Agent sent with requests to Sourcegraph"},
22+
}
23+
24+
flags := append([]cli.Flag{}, baseFlags...)
25+
for _, item := range flagTable {
26+
flags = append(flags, &cli.BoolFlag{
27+
Name: item.name,
28+
Value: item.value,
29+
Usage: item.text,
30+
})
31+
}
32+
33+
return flags
34+
}
35+
36+
// APIFlagsFromCmd reads the shared API-related flags from a command into api.Flags
37+
func APIFlagsFromCmd(cmd *cli.Command) *api.Flags {
38+
return api.NewFlagsFromValues(
39+
cmd.Bool("dump-requests"),
40+
cmd.Bool("get-curl"),
41+
cmd.Bool("trace"),
42+
cmd.Bool("insecure-skip-verify"),
43+
cmd.Bool("user-agent-telemetry"),
44+
)
45+
}
46+
47+
func defaultAPIUserAgentTelemetry() bool {
48+
return os.Getenv("SRC_DISABLE_USER_AGENT_TELEMETRY") == ""
49+
}

0 commit comments

Comments
 (0)