From 5263d728c0cef0ae0c739bb9921500ecbdcdfbd3 Mon Sep 17 00:00:00 2001 From: William Bezuidenhout Date: Tue, 5 May 2026 14:35:29 +0200 Subject: [PATCH] feat: migrate codeowners to urfave/cli --- cmd/src/codeowners.go | 72 +++++++++++----------- cmd/src/codeowners_create.go | 104 ++++++++++++-------------------- cmd/src/codeowners_delete.go | 70 +++++++++------------ cmd/src/codeowners_get.go | 78 ++++++++++-------------- cmd/src/codeowners_update.go | 103 ++++++++++++------------------- cmd/src/run_migration_compat.go | 13 ++-- 6 files changed, 187 insertions(+), 253 deletions(-) diff --git a/cmd/src/codeowners.go b/cmd/src/codeowners.go index 47b9cee19b..bf0074327a 100644 --- a/cmd/src/codeowners.go +++ b/cmd/src/codeowners.go @@ -1,48 +1,28 @@ package main import ( - "flag" - "fmt" "io" "os" -) -var codeownersCommands commander + "github.com/sourcegraph/sourcegraph/lib/errors" + "github.com/sourcegraph/src-cli/internal/clicompat" + "github.com/urfave/cli/v3" +) -func init() { - usage := `'src codeowners' is a tool that manages ingested code ownership data in a Sourcegraph instance. +const codeownersExamples = `'src codeowners' manages ingested code ownership data in a Sourcegraph instance. Usage: - src codeowners command [command options] + src codeowners [command options] -The commands are: +Examples: - get returns the codeowners file for a repository, if exists - create create a codeowners file - update update a codeowners file - delete delete a codeowners file - -Use "src codeowners [command] -h" for more information about a command. + $ src codeowners get -repo='github.com/sourcegraph/sourcegraph' + $ src codeowners create -repo='github.com/sourcegraph/sourcegraph' -f CODEOWNERS + $ src codeowners update -repo='github.com/sourcegraph/sourcegraph' -f CODEOWNERS + $ src codeowners delete -repo='github.com/sourcegraph/sourcegraph' ` - flagSet := flag.NewFlagSet("codeowners", flag.ExitOnError) - handler := func(args []string) error { - codeownersCommands.run(flagSet, "src codeowners", usage, args) - return nil - } - - // Register the command. - commands = append(commands, &command{ - flagSet: flagSet, - aliases: []string{"codeowner"}, - handler: handler, - usageFunc: func() { - fmt.Println(usage) - }, - }) -} - const codeownersFragment = ` fragment CodeownersFileFields on CodeownersIngestedFile { contents @@ -59,9 +39,33 @@ type CodeownersIngestedFile struct { } `json:"repository"` } -func readFile(f string) (io.Reader, error) { +var codeownersCommand = clicompat.Wrap(&cli.Command{ + Name: "codeowners", + Aliases: []string{"codeowner"}, + Usage: "manages ingested code ownership data", + UsageText: "src codeowners [command options]", + Description: codeownersExamples, + HideVersion: true, + Commands: []*cli.Command{ + codeownersGetCommand, + codeownersCreateCommand, + codeownersUpdateCommand, + codeownersDeleteCommand, + }, +}) + +func readFile(f string) ([]byte, error) { if f == "-" { - return os.Stdin, nil + return io.ReadAll(os.Stdin) + } + return os.ReadFile(f) +} + +func requiresNotEmpty(errMsg string) func(string) error { + return func(value string) error { + if value == "" { + return errors.New(errMsg) + } + return nil } - return os.Open(f) } diff --git a/cmd/src/codeowners_create.go b/cmd/src/codeowners_create.go index cb489b0695..b446743896 100644 --- a/cmd/src/codeowners_create.go +++ b/cmd/src/codeowners_create.go @@ -2,74 +2,57 @@ package main import ( "context" - "flag" - "fmt" - "io" "strings" "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/cmderrors" + "github.com/urfave/cli/v3" ) -func init() { - usage := ` -Examples: - - Create a codeowners file for the repository "github.com/sourcegraph/sourcegraph": - - $ src codeowners create -repo='github.com/sourcegraph/sourcegraph' -f CODEOWNERS +const codeownersCreateExamples = ` +Create a codeowners file for a repository. - Create a codeowners file for the repository "github.com/sourcegraph/sourcegraph" from stdin: +Examples: - $ src codeowners create -repo='github.com/sourcegraph/sourcegraph' -f - + $ src codeowners create -repo='github.com/sourcegraph/sourcegraph' -f CODEOWNERS + $ src codeowners create -repo='github.com/sourcegraph/sourcegraph' -f - ` - flagSet := flag.NewFlagSet("create", flag.ExitOnError) - usageFunc := func() { - fmt.Fprintf(flag.CommandLine.Output(), "Usage of 'src codeowners %s':\n", flagSet.Name()) - flagSet.PrintDefaults() - fmt.Println(usage) - } - var ( - repoFlag = flagSet.String("repo", "", "The repository to attach the data to") - fileFlag = flagSet.String("file", "", "File path to read ownership information from (- for stdin)") - fileShortFlag = flagSet.String("f", "", "File path to read ownership information from (- for stdin). Alias for -file") - - apiFlags = api.NewFlags(flagSet) - ) - - handler := func(args []string) error { - if err := flagSet.Parse(args); err != nil { - return err - } - - if *repoFlag == "" { - return errors.New("provide a repo name using -repo") - } - - if *fileFlag == "" && *fileShortFlag == "" { - return errors.New("provide a file using -file") - } - if *fileFlag != "" && *fileShortFlag != "" { - return errors.New("have to provide either -file or -f") - } - if *fileShortFlag != "" { - *fileFlag = *fileShortFlag - } - - file, err := readFile(*fileFlag) +var codeownersCreateCommand = clicompat.Wrap(&cli.Command{ + Name: "create", + Usage: "create a codeowners file", + UsageText: "src codeowners create [options]", + Description: codeownersCreateExamples, + HideVersion: true, + Flags: clicompat.WithAPIFlags( + &cli.StringFlag{ + Name: "repo", + Usage: "The repository to attach the data to", + Required: true, + Validator: requiresNotEmpty("provide a repo name using -repo"), + }, + &cli.StringFlag{ + Name: "file", + Aliases: []string{"f"}, + Usage: "File path to read ownership information from (- for stdin)", + TakesFile: true, + Required: true, + Validator: requiresNotEmpty("provide a file using -file"), + }, + ), + Action: func(ctx context.Context, cmd *cli.Command) error { + repoName := cmd.String("repo") + fileName := cmd.String("file") + + content, err := readFile(fileName) if err != nil { return err } - content, err := io.ReadAll(file) - if err != nil { - return err - } - - client := cfg.apiClient(apiFlags, flagSet.Output()) + client := cfg.apiClient(clicompat.APIFlagsFromCmd(cmd), cmd.Writer) query := `mutation CreateCodeownersFile( $repoName: String!, @@ -89,14 +72,14 @@ Examples: AddCodeownersFile CodeownersIngestedFile } if ok, err := client.NewRequest(query, map[string]any{ - "repoName": *repoFlag, + "repoName": repoName, "content": string(content), - }).Do(context.Background(), &result); err != nil || !ok { + }).Do(ctx, &result); err != nil || !ok { var gqlErr api.GraphQlErrors if errors.As(err, &gqlErr) { for _, e := range gqlErr { if strings.Contains(e.Error(), "repo not found:") { - return cmderrors.ExitCode(2, errors.Newf("repository %q not found", *repoFlag)) + return cmderrors.ExitCode(2, errors.Newf("repository %q not found", repoName)) } if strings.Contains(e.Error(), "codeowners file has already been ingested for this repository") { return cmderrors.ExitCode(2, errors.New("codeowners file has already been ingested for this repository")) @@ -107,12 +90,5 @@ Examples: } return nil - } - - // Register the command. - codeownersCommands = append(codeownersCommands, &command{ - flagSet: flagSet, - handler: handler, - usageFunc: usageFunc, - }) -} + }, +}) diff --git a/cmd/src/codeowners_delete.go b/cmd/src/codeowners_delete.go index f6655c30e7..11e1e901e7 100644 --- a/cmd/src/codeowners_delete.go +++ b/cmd/src/codeowners_delete.go @@ -2,46 +2,41 @@ package main import ( "context" - "flag" - "fmt" "strings" "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/cmderrors" + "github.com/urfave/cli/v3" ) -func init() { - usage := ` -Examples: +const codeownersDeleteExamples = ` +Delete a codeowners file for a repository. - Delete a codeowners file for the repository "github.com/sourcegraph/sourcegraph": +Examples: - $ src codeowners delete -repo='github.com/sourcegraph/sourcegraph' + $ src codeowners delete -repo='github.com/sourcegraph/sourcegraph' ` - flagSet := flag.NewFlagSet("delete", flag.ExitOnError) - usageFunc := func() { - fmt.Fprintf(flag.CommandLine.Output(), "Usage of 'src codeowners %s':\n", flagSet.Name()) - flagSet.PrintDefaults() - fmt.Println(usage) - } - var ( - repoFlag = flagSet.String("repo", "", "The repository to delete the data for") - apiFlags = api.NewFlags(flagSet) - ) - - handler := func(args []string) error { - if err := flagSet.Parse(args); err != nil { - return err - } - - if *repoFlag == "" { - return errors.New("provide a repo name using -repo") - } - - client := cfg.apiClient(apiFlags, flagSet.Output()) +var codeownersDeleteCommand = clicompat.Wrap(&cli.Command{ + Name: "delete", + Usage: "delete a codeowners file", + UsageText: "src codeowners delete [options]", + Description: codeownersDeleteExamples, + HideVersion: true, + Flags: clicompat.WithAPIFlags( + &cli.StringFlag{ + Name: "repo", + Usage: "The repository to delete the data for", + Required: true, + Validator: requiresNotEmpty("provide a repo name using -repo"), + }, + ), + Action: func(ctx context.Context, cmd *cli.Command) error { + repoName := cmd.String("repo") + client := cfg.apiClient(clicompat.APIFlagsFromCmd(cmd), cmd.Writer) query := `mutation DeleteCodeownersFile( $repoName: String!, @@ -58,16 +53,16 @@ Examples: DeleteCodeownersFile CodeownersIngestedFile } if ok, err := client.NewRequest(query, map[string]any{ - "repoName": *repoFlag, - }).Do(context.Background(), &result); err != nil || !ok { + "repoName": repoName, + }).Do(ctx, &result); err != nil || !ok { var gqlErr api.GraphQlErrors if errors.As(err, &gqlErr) { for _, e := range gqlErr { if strings.Contains(e.Error(), "repo not found:") { - return cmderrors.ExitCode(2, errors.Newf("repository %q not found", *repoFlag)) + return cmderrors.ExitCode(2, errors.Newf("repository %q not found", repoName)) } if strings.Contains(e.Error(), "codeowners file not found:") { - return cmderrors.ExitCode(2, errors.Newf("no data found for repository %q", *repoFlag)) + return cmderrors.ExitCode(2, errors.Newf("no data found for repository %q", repoName)) } } } @@ -75,12 +70,5 @@ Examples: } return nil - } - - // Register the command. - codeownersCommands = append(codeownersCommands, &command{ - flagSet: flagSet, - handler: handler, - usageFunc: usageFunc, - }) -} + }, +}) diff --git a/cmd/src/codeowners_get.go b/cmd/src/codeowners_get.go index e1e1e2fa03..c781f427c6 100644 --- a/cmd/src/codeowners_get.go +++ b/cmd/src/codeowners_get.go @@ -2,53 +2,47 @@ package main import ( "context" - "flag" "fmt" - "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/cmderrors" + "github.com/urfave/cli/v3" ) -func init() { - usage := ` -Examples: +const codeownersGetExamples = ` +Read the current codeowners file for a repository. - Read the current codeowners file for the repository "github.com/sourcegraph/sourcegraph": +Examples: - $ src codeowners get -repo='github.com/sourcegraph/sourcegraph' + $ src codeowners get -repo='github.com/sourcegraph/sourcegraph' ` - flagSet := flag.NewFlagSet("get", flag.ExitOnError) - usageFunc := func() { - fmt.Fprintf(flag.CommandLine.Output(), "Usage of 'src codeowners %s':\n", flagSet.Name()) - flagSet.PrintDefaults() - fmt.Println(usage) - } - var ( - repoFlag = flagSet.String("repo", "", "The repository to attach the data to") - apiFlags = api.NewFlags(flagSet) - ) - - handler := func(args []string) error { - if err := flagSet.Parse(args); err != nil { - return err - } - - if *repoFlag == "" { - return errors.New("provide a repo name using -repo") - } - - client := cfg.apiClient(apiFlags, flagSet.Output()) +var codeownersGetCommand = clicompat.Wrap(&cli.Command{ + Name: "get", + Usage: "returns the codeowners file for a repository, if it exists", + UsageText: "src codeowners get [options]", + Description: codeownersGetExamples, + HideVersion: true, + Flags: clicompat.WithAPIFlags( + &cli.StringFlag{ + Name: "repo", + Usage: "The repository to attach the data to", + Required: true, + Validator: requiresNotEmpty("provide a repo name using -repo"), + }, + ), + Action: func(ctx context.Context, cmd *cli.Command) error { + repoName := cmd.String("repo") + client := cfg.apiClient(clicompat.APIFlagsFromCmd(cmd), cmd.Writer) query := `query GetCodeownersFile( $repoName: String! ) { repository(name: $repoName) { ingestedCodeowners { - ...CodeownersFileFields + ...CodeownersFileFields } } } @@ -60,28 +54,20 @@ Examples: } } if ok, err := client.NewRequest(query, map[string]any{ - "repoName": *repoFlag, - }).Do(context.Background(), &result); err != nil || !ok { + "repoName": repoName, + }).Do(ctx, &result); err != nil || !ok { return err } if result.Repository == nil { - return cmderrors.ExitCode(2, errors.Newf("repository %q not found", *repoFlag)) + return cmderrors.ExitCode(2, errors.Newf("repository %q not found", repoName)) } if result.Repository.IngestedCodeowners == nil { - return cmderrors.ExitCode(2, errors.Newf("no codeowners data found for %q", *repoFlag)) + return cmderrors.ExitCode(2, errors.Newf("no codeowners data found for %q", repoName)) } - fmt.Fprintf(os.Stdout, "%s", result.Repository.IngestedCodeowners.Contents) - - return nil - } - - // Register the command. - codeownersCommands = append(codeownersCommands, &command{ - flagSet: flagSet, - handler: handler, - usageFunc: usageFunc, - }) -} + _, err := fmt.Fprint(cmd.Writer, result.Repository.IngestedCodeowners.Contents) + return err + }, +}) diff --git a/cmd/src/codeowners_update.go b/cmd/src/codeowners_update.go index 222482e656..c0c7c01ed0 100644 --- a/cmd/src/codeowners_update.go +++ b/cmd/src/codeowners_update.go @@ -2,73 +2,57 @@ package main import ( "context" - "flag" - "fmt" - "io" "strings" "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/cmderrors" + "github.com/urfave/cli/v3" ) -func init() { - usage := ` -Examples: - - Update a codeowners file for the repository "github.com/sourcegraph/sourcegraph": - - $ src codeowners update -repo='github.com/sourcegraph/sourcegraph' -f CODEOWNERS +const codeownersUpdateExamples = ` +Update a codeowners file for a repository. - Update a codeowners file for the repository "github.com/sourcegraph/sourcegraph" from stdin: +Examples: - $ src codeowners update -repo='github.com/sourcegraph/sourcegraph' -f - + $ src codeowners update -repo='github.com/sourcegraph/sourcegraph' -f CODEOWNERS + $ src codeowners update -repo='github.com/sourcegraph/sourcegraph' -f - ` - flagSet := flag.NewFlagSet("update", flag.ExitOnError) - usageFunc := func() { - fmt.Fprintf(flag.CommandLine.Output(), "Usage of 'src codeowners %s':\n", flagSet.Name()) - flagSet.PrintDefaults() - fmt.Println(usage) - } - var ( - repoFlag = flagSet.String("repo", "", "The repository to attach the data to") - fileFlag = flagSet.String("file", "", "File path to read ownership information from (- for stdin)") - fileShortFlag = flagSet.String("f", "", "File path to read ownership information from (- for stdin). Alias for -file") - apiFlags = api.NewFlags(flagSet) - ) - - handler := func(args []string) error { - if err := flagSet.Parse(args); err != nil { - return err - } - - if *repoFlag == "" { - return errors.New("provide a repo name using -repo") - } - - if *fileFlag == "" && *fileShortFlag == "" { - return errors.New("provide a file using -file") - } - if *fileFlag != "" && *fileShortFlag != "" { - return errors.New("have to provide either -file or -f") - } - if *fileShortFlag != "" { - *fileFlag = *fileShortFlag - } - - file, err := readFile(*fileFlag) +var codeownersUpdateCommand = clicompat.Wrap(&cli.Command{ + Name: "update", + Usage: "update a codeowners file", + UsageText: "src codeowners update [options]", + Description: codeownersUpdateExamples, + HideVersion: true, + Flags: clicompat.WithAPIFlags( + &cli.StringFlag{ + Name: "repo", + Usage: "The repository to attach the data to", + Required: true, + Validator: requiresNotEmpty("provide a repo name using -repo"), + }, + &cli.StringFlag{ + Name: "file", + Aliases: []string{"f"}, + Usage: "File path to read ownership information from (- for stdin)", + TakesFile: true, + Required: true, + Validator: requiresNotEmpty("provide a file using -file"), + }, + ), + Action: func(ctx context.Context, cmd *cli.Command) error { + repoName := cmd.String("repo") + fileName := cmd.String("file") + + content, err := readFile(fileName) if err != nil { return err } - content, err := io.ReadAll(file) - if err != nil { - return err - } - - client := cfg.apiClient(apiFlags, flagSet.Output()) + client := cfg.apiClient(clicompat.APIFlagsFromCmd(cmd), cmd.Writer) query := `mutation UpdateCodeownersFile( $repoName: String!, @@ -87,14 +71,14 @@ Examples: UpdateCodeownersFile CodeownersIngestedFile } if ok, err := client.NewRequest(query, map[string]any{ - "repoName": *repoFlag, + "repoName": repoName, "content": string(content), - }).Do(context.Background(), &result); err != nil || !ok { + }).Do(ctx, &result); err != nil || !ok { var gqlErr api.GraphQlErrors if errors.As(err, &gqlErr) { for _, e := range gqlErr { if strings.Contains(e.Error(), "repo not found:") { - return cmderrors.ExitCode(2, errors.Newf("repository %q not found", *repoFlag)) + return cmderrors.ExitCode(2, errors.Newf("repository %q not found", repoName)) } if strings.Contains(e.Error(), "could not update codeowners file: codeowners file not found:") { return cmderrors.ExitCode(2, errors.New("no codeowners data has been found for this repository")) @@ -105,12 +89,5 @@ Examples: } return nil - } - - // Register the command. - codeownersCommands = append(codeownersCommands, &command{ - flagSet: flagSet, - handler: handler, - usageFunc: usageFunc, - }) -} + }, +}) diff --git a/cmd/src/run_migration_compat.go b/cmd/src/run_migration_compat.go index 52baca0fc5..fd476ac75d 100644 --- a/cmd/src/run_migration_compat.go +++ b/cmd/src/run_migration_compat.go @@ -16,11 +16,14 @@ import ( ) var migratedCommands = map[string]*cli.Command{ - "abc": abcCommand, - "api": apiCommand, - "auth": authCommand, - "login": loginCommand, - "version": versionCommand, + "abc": abcCommand, + "api": apiCommand, + "auth": authCommand, + "codeowners": codeownersCommand, + // instead of writing lots of plumbing to handle an alias, lets just register it explicitly for now + "codeowner": codeownersCommand, + "login": loginCommand, + "version": versionCommand, } func maybeRunMigratedCommand() (isMigrated bool, exitCode int, err error) {