Skip to content
Open
32 changes: 6 additions & 26 deletions cmd/src/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ import (
"log"
"os"
"slices"

"github.com/sourcegraph/src-cli/internal/cmderrors"
)

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

// Find the subcommand to execute.
name := flagSet.Arg(0)

// Command is legacy, so lets execute the old way
for _, cmd := range c {
if !cmd.matches(name) {
continue
}

// Read global configuration now.
var err error
cfg, err = readConfig()
if err != nil {
log.Fatal("reading config: ", err)
}

// Parse subcommand flags.
args := flagSet.Args()[1:]
if err := cmd.flagSet.Parse(args); err != nil {
Comment on lines -84 to -85
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm kinda confused how this all worked before. .Args is the non flag arguments, not the raw argv.

fmt.Printf("Error parsing subcommand flags: %s\n", err)
panic(fmt.Sprintf("all registered commands should use flag.ExitOnError: error: %s", err))
}

// Execute the subcommand.
if err := cmd.handler(flagSet.Args()[1:]); err != nil {
if _, ok := err.(*cmderrors.UsageError); ok {
log.Printf("error: %s\n\n", err)
cmd.flagSet.SetOutput(os.Stderr)
flag.CommandLine.SetOutput(os.Stderr)
cmd.flagSet.Usage()
os.Exit(2)
}
if e, ok := err.(*cmderrors.ExitCodeError); ok {
if e.HasError() {
log.Println(e)
}
os.Exit(e.Code())
}
exitCode, err := runLegacy(cmd, flagSet)
if err != nil {
log.Fatal(err)
}
os.Exit(0)
os.Exit(exitCode)

}
log.Printf("%s: unknown subcommand %q", cmdName, name)
log.Fatalf("Run '%s help' for usage.", cmdName)
Expand Down
13 changes: 12 additions & 1 deletion cmd/src/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,18 @@ func main() {
log.SetFlags(0)
log.SetPrefix("")

commands.run(flag.CommandLine, "src", usageText, normalizeDashHelp(os.Args[1:]))
ranMigratedCmd, exitCode, err := maybeRunMigratedCommand()
if ranMigratedCmd {
if err != nil {
log.Println(err)
}
os.Exit(exitCode)
}

// if we didn't run a migrated command, then lets try running the legacy version
if !ranMigratedCmd {
commands.run(flag.CommandLine, "src", usageText, normalizeDashHelp(os.Args[1:]))
}
}

// normalizeDashHelp converts --help to -help since Go's flag parser only supports single dash.
Expand Down
102 changes: 102 additions & 0 deletions cmd/src/run_migration_compat.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package main

import (
"context"
"flag"
"fmt"
"log"
"os"
"sort"

"github.com/sourcegraph/src-cli/internal/clicompat"
"github.com/sourcegraph/src-cli/internal/cmderrors"
"github.com/urfave/cli/v3"

"github.com/sourcegraph/sourcegraph/lib/errors"
)

var migratedCommands = map[string]*cli.Command{
"version": versionCommand,
}

func maybeRunMigratedCommand() (isMigrated bool, exitCode int, err error) {
// need to figure out if a migrated command has been requested
flag.Parse()
subCommand := flag.CommandLine.Arg(0)
_, isMigrated = migratedCommands[subCommand]
if !isMigrated {
return
}
cfg, err = readConfig()
if err != nil {
log.Fatal("reading config: ", err)
}

exitCode, err = runMigrated()
return
}

// migratedRootCommand constructs a root 'src' command and adds
// MigratedCommands as subcommands to it
func migratedRootCommand() *cli.Command {
names := make([]string, 0, len(migratedCommands))
for name := range migratedCommands {
names = append(names, name)
}
sort.Strings(names)

commands := make([]*cli.Command, 0, len(names))
for _, name := range names {
commands = append(commands, migratedCommands[name])
}

return clicompat.WrapRoot(&cli.Command{
Name: "src",
HideVersion: true,
Commands: commands,
})
}

// runMigrated runs the command within urfave/cli framework
func runMigrated() (int, error) {
ctx := context.Background()

err := migratedRootCommand().Run(ctx, os.Args)
if errors.HasType[*cmderrors.UsageError](err) {
return 2, nil
}
var exitErr cli.ExitCoder
if errors.AsInterface(err, &exitErr) {
return exitErr.ExitCode(), err
}
return 0, err
}

// runLegacy runs the command using the original commander framework
func runLegacy(cmd *command, flagSet *flag.FlagSet) (int, error) {
// Parse subcommand flags.
args := flagSet.Args()[1:]
if err := cmd.flagSet.Parse(args); err != nil {
fmt.Printf("Error parsing subcommand flags: %s\n", err)
panic(fmt.Sprintf("all registered commands should use flag.ExitOnError: error: %s", err))
}

// Execute the subcommand.
if err := cmd.handler(flagSet.Args()[1:]); err != nil {
if _, ok := err.(*cmderrors.UsageError); ok {
log.Printf("error: %s\n\n", err)
cmd.flagSet.SetOutput(os.Stderr)
flag.CommandLine.SetOutput(os.Stderr)
cmd.flagSet.Usage()
return 2, nil
}
if e, ok := err.(*cmderrors.ExitCodeError); ok {
if e.HasError() {
log.Println(e)
}
return e.Code(), nil
}
return 1, err
}
return 0, nil
}
82 changes: 46 additions & 36 deletions cmd/src/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,63 +3,73 @@ package main
import (
"context"
"encoding/json"
"flag"
"fmt"
"io"
"net/http"
"os"

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

"github.com/sourcegraph/src-cli/internal/api"
"github.com/sourcegraph/src-cli/internal/clicompat"
"github.com/sourcegraph/src-cli/internal/version"

"github.com/urfave/cli/v3"
)

func init() {
usage := `
Examples:
const versionExamples = `Examples:

Get the src-cli version and the Sourcegraph instance's recommended version:

$ src version
`

flagSet := flag.NewFlagSet("version", flag.ExitOnError)

var (
clientOnly = flagSet.Bool("client-only", false, "If true, only the client version will be printed.")
apiFlags = api.NewFlags(flagSet)
)

handler := func(args []string) error {
fmt.Printf("Current version: %s\n", version.BuildTag)
if clientOnly != nil && *clientOnly {
return nil
var versionCommand = clicompat.Wrap(&cli.Command{
Name: "version",
Usage: "display and compare the src-cli version against the recommended version for your instance",
UsageText: "src version [options]",
OnUsageError: clicompat.OnUsageError,
Description: `
` + versionExamples,
Flags: clicompat.WithAPIFlags(
&cli.BoolFlag{
Name: "client-only",
Usage: "If true, only the client version will be printed.",
},
),
HideVersion: true,
Action: func(ctx context.Context, c *cli.Command) error {
args := VersionArgs{
Client: cfg.apiClient(clicompat.APIFlagsFromCmd(c), os.Stdout),
ClientOnly: c.Bool("client-only"),
}
return versionHandler(args)
},
})

type VersionArgs struct {
ClientOnly bool
Client api.Client
Output io.Writer
}

client := cfg.apiClient(apiFlags, flagSet.Output())
recommendedVersion, err := getRecommendedVersion(context.Background(), client)
if err != nil {
return errors.Wrap(err, "failed to get recommended version for Sourcegraph deployment")
}
if recommendedVersion == "" {
fmt.Println("Recommended version: <unknown>")
fmt.Println("This Sourcegraph instance does not support this feature.")
return nil
}
fmt.Printf("Recommended version: %s or later\n", recommendedVersion)
func versionHandler(args VersionArgs) error {
fmt.Printf("Current version: %s\n", version.BuildTag)
if args.ClientOnly {
return nil
}

// Register the command.
commands = append(commands, &command{
flagSet: flagSet,
handler: handler,
usageFunc: func() {
fmt.Fprintf(flag.CommandLine.Output(), "Usage of 'src %s':\n", flagSet.Name())
flagSet.PrintDefaults()
fmt.Println(usage)
},
})
recommendedVersion, err := getRecommendedVersion(context.Background(), args.Client)
if err != nil {
return errors.Wrap(err, "failed to get recommended version for Sourcegraph deployment")
}
if recommendedVersion == "" {
fmt.Println("Recommended version: <unknown>")
fmt.Println("This Sourcegraph instance does not support this feature.")
return nil
}
fmt.Printf("Recommended version: %s or later\n", recommendedVersion)
return nil
}

func getRecommendedVersion(ctx context.Context, client api.Client) (string, error) {
Expand Down
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/sourcegraph/src-cli

go 1.25.8
go 1.26

require (
cloud.google.com/go/storage v1.50.0
Expand Down Expand Up @@ -83,6 +83,7 @@ require (
github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect
github.com/tliron/commonlog v0.2.19 // indirect
github.com/tliron/kutil v0.3.27 // indirect
github.com/urfave/cli/v3 v3.8.0 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/xlab/treeprint v1.2.0 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,8 @@ github.com/tliron/glsp v0.2.2 h1:IKPfwpE8Lu8yB6Dayta+IyRMAbTVunudeauEgjXBt+c=
github.com/tliron/glsp v0.2.2/go.mod h1:GMVWDNeODxHzmDPvYbYTCs7yHVaEATfYtXiYJ9w1nBg=
github.com/tliron/kutil v0.3.27 h1:Wb0V5jdbTci6Let1tiGY741J/9FIynmV/pCsPDPsjcM=
github.com/tliron/kutil v0.3.27/go.mod h1:AHeLNIFBSKBU39ELVHZdkw2f/ez2eKGAAGoxwBlhMi8=
github.com/urfave/cli/v3 v3.8.0 h1:XqKPrm0q4P0q5JpoclYoCAv0/MIvH/jZ2umzuf8pNTI=
github.com/urfave/cli/v3 v3.8.0/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
Expand Down
13 changes: 13 additions & 0 deletions internal/api/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,19 @@ func NewFlags(flagSet *flag.FlagSet) *Flags {
}
}

// NewFlagsFromValues instantiates a new Flags structure from explicit values.
// This is used by cli/v3 compatibility adapters that do not operate on a
// standard flag.FlagSet.
func NewFlagsFromValues(dump, getCurl, trace, insecureSkipVerify, userAgentTelemetry bool) *Flags {
return &Flags{
dump: new(dump),
getCurl: new(getCurl),
trace: new(trace),
insecureSkipVerify: new(insecureSkipVerify),
userAgentTelemetry: new(userAgentTelemetry),
}
}

func defaultFlags() *Flags {
telemetry := defaultUserAgentTelemetry()
d := false
Expand Down
49 changes: 49 additions & 0 deletions internal/clicompat/api_flags.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package clicompat

import (
"os"

"github.com/sourcegraph/src-cli/internal/api"
"github.com/urfave/cli/v3"
)

// WithAPIFlags appends the standard API-related flags used by legacy src
func WithAPIFlags(baseFlags ...cli.Flag) []cli.Flag {
var flagTable = []struct {
name string
value bool
text string
}{
{"dump-requests", false, "Log GraphQL requests and responses to stdout"},
{"get-curl", false, "Print the curl command for executing this query and exit (WARNING: includes printing your access token!)"},
{"trace", false, "Log the trace ID for requests. See https://docs.sourcegraph.com/admin/observability/tracing"},
{"insecure-skip-verify", false, "Skip validation of TLS certificates against trusted chains"},
{"user-agent-telemetry", defaultAPIUserAgentTelemetry(), "Include the operating system and architecture in the User-Agent sent with requests to Sourcegraph"},
}

flags := append([]cli.Flag{}, baseFlags...)
for _, item := range flagTable {
flags = append(flags, &cli.BoolFlag{
Name: item.name,
Value: item.value,
Usage: item.text,
})
}

return flags
}

// APIFlagsFromCmd reads the shared API-related flags from a command into api.Flags
func APIFlagsFromCmd(cmd *cli.Command) *api.Flags {
return api.NewFlagsFromValues(
cmd.Bool("dump-requests"),
cmd.Bool("get-curl"),
cmd.Bool("trace"),
cmd.Bool("insecure-skip-verify"),
cmd.Bool("user-agent-telemetry"),
)
}

func defaultAPIUserAgentTelemetry() bool {
return os.Getenv("SRC_DISABLE_USER_AGENT_TELEMETRY") == ""
}
Loading
Loading