From d04229383a1e57c9d174569eabc5131630f2ccdf Mon Sep 17 00:00:00 2001 From: Erik Seliger Date: Sun, 26 Feb 2023 22:04:24 +0100 Subject: [PATCH 1/3] Implement CLI for ingested codeowners This requires some backend changes that I proposed on the schema PR, but it might help with testing the implementation. --- cmd/src/codeowners.go | 67 +++++++++++++++++++++++++ cmd/src/codeowners_create.go | 96 ++++++++++++++++++++++++++++++++++++ cmd/src/codeowners_delete.go | 74 +++++++++++++++++++++++++++ cmd/src/codeowners_get.go | 83 +++++++++++++++++++++++++++++++ cmd/src/codeowners_update.go | 96 ++++++++++++++++++++++++++++++++++++ cmd/src/main.go | 1 + 6 files changed, 417 insertions(+) create mode 100644 cmd/src/codeowners.go create mode 100644 cmd/src/codeowners_create.go create mode 100644 cmd/src/codeowners_delete.go create mode 100644 cmd/src/codeowners_get.go create mode 100644 cmd/src/codeowners_update.go diff --git a/cmd/src/codeowners.go b/cmd/src/codeowners.go new file mode 100644 index 0000000000..47b9cee19b --- /dev/null +++ b/cmd/src/codeowners.go @@ -0,0 +1,67 @@ +package main + +import ( + "flag" + "fmt" + "io" + "os" +) + +var codeownersCommands commander + +func init() { + usage := `'src codeowners' is a tool that manages ingested code ownership data in a Sourcegraph instance. + +Usage: + + src codeowners command [command options] + +The commands are: + + 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. +` + + 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 + repository { + name + } +} +` + +type CodeownersIngestedFile struct { + Contents string `json:"contents"` + Repository struct { + Name string `json:"name"` + } `json:"repository"` +} + +func readFile(f string) (io.Reader, error) { + if f == "-" { + return os.Stdin, nil + } + return os.Open(f) +} diff --git a/cmd/src/codeowners_create.go b/cmd/src/codeowners_create.go new file mode 100644 index 0000000000..a7f8ff4b1c --- /dev/null +++ b/cmd/src/codeowners_create.go @@ -0,0 +1,96 @@ +package main + +import ( + "context" + "flag" + "fmt" + "io" + + "github.com/sourcegraph/sourcegraph/lib/errors" + + "github.com/sourcegraph/src-cli/internal/api" +) + +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 + + Create a codeowners file for the repository "github.com/sourcegraph/sourcegraph" from stdin: + + $ 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("f", "", "File path to read ownership information from (- for stdin)") + 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") + } + + if *fileFlag == "" { + return errors.New("provide a file") + } + + file, err := readFile(*fileFlag) + if err != nil { + return err + } + + content, err := io.ReadAll(file) + if err != nil { + return err + } + + client := cfg.apiClient(apiFlags, flagSet.Output()) + + query := `mutation CreateCodeownersFile( + $repoName: String!, + $content: String! +) { + addCodeownersFile( + repoName: $repoName, + fileContents: $content, + ) { + ...CodeownersFileFields + } +} +` + codeownersFragment + + var result struct { + AddCodeownersFile CodeownersIngestedFile + } + if ok, err := client.NewRequest(query, map[string]interface{}{ + "repoName": *repoFlag, + "content": string(content), + }).Do(context.Background(), &result); err != nil || !ok { + return err + } + + 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 new file mode 100644 index 0000000000..7897f6109c --- /dev/null +++ b/cmd/src/codeowners_delete.go @@ -0,0 +1,74 @@ +package main + +import ( + "context" + "flag" + "fmt" + + "github.com/sourcegraph/sourcegraph/lib/errors" + + "github.com/sourcegraph/src-cli/internal/api" +) + +func init() { + usage := ` +Examples: + + Delete a codeowners file for the repository "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") + } + + client := cfg.apiClient(apiFlags, flagSet.Output()) + + query := `mutation DeleteCodeownersFile( + $repoName: String!, + $content: String! +) { + deleteCodeownersFile( + repoName: $repoName, + ) { + alwaysNil + } +} +` + + var result struct { + DeleteCodeownersFile CodeownersIngestedFile + } + if ok, err := client.NewRequest(query, map[string]interface{}{ + "repoName": *repoFlag, + }).Do(context.Background(), &result); err != nil || !ok { + return err + } + + 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 new file mode 100644 index 0000000000..4761df2043 --- /dev/null +++ b/cmd/src/codeowners_get.go @@ -0,0 +1,83 @@ +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/cmderrors" +) + +func init() { + usage := ` +Examples: + + Read the current codeowners file for the repository "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") + } + + client := cfg.apiClient(apiFlags, flagSet.Output()) + + query := `mutation GetCodeownersFile( + $repoName: String! +) { + repo(name: $repoName) { + ingestedCodeownersFile { + ...CodeownersFileFields + } + } +} +` + codeownersFragment + + var result struct { + Repo *struct { + IngestedCodeownersFile CodeownersIngestedFile + } + } + if ok, err := client.NewRequest(query, map[string]interface{}{ + "repoName": *repoFlag, + }).Do(context.Background(), &result); err != nil || !ok { + return err + } + + if result.Repo == nil { + return cmderrors.ExitCode(2, errors.Newf("repository %q not found", *repoFlag)) + } + + fmt.Fprintf(os.Stdout, "%s", result.Repo.IngestedCodeownersFile.Contents) + + return nil + } + + // Register the command. + codeownersCommands = append(codeownersCommands, &command{ + flagSet: flagSet, + handler: handler, + usageFunc: usageFunc, + }) +} diff --git a/cmd/src/codeowners_update.go b/cmd/src/codeowners_update.go new file mode 100644 index 0000000000..1bcff258ec --- /dev/null +++ b/cmd/src/codeowners_update.go @@ -0,0 +1,96 @@ +package main + +import ( + "context" + "flag" + "fmt" + "io" + + "github.com/sourcegraph/sourcegraph/lib/errors" + + "github.com/sourcegraph/src-cli/internal/api" +) + +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 + + Update a codeowners file for the repository "github.com/sourcegraph/sourcegraph" from stdin: + + $ 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("f", "", "File path to read ownership information from (- for stdin)") + 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") + } + + if *fileFlag == "" { + return errors.New("provide a file") + } + + file, err := readFile(*fileFlag) + if err != nil { + return err + } + + content, err := io.ReadAll(file) + if err != nil { + return err + } + + client := cfg.apiClient(apiFlags, flagSet.Output()) + + query := `mutation UpdateCodeownersFile( + $repoName: String!, + $content: String! +) { + updateCodeownersFile( + repoName: $repoName, + fileContents: $content, + ) { + ...CodeownersFileFields + } +} +` + codeownersFragment + + var result struct { + UpdateCodeownersFile CodeownersIngestedFile + } + if ok, err := client.NewRequest(query, map[string]interface{}{ + "repoName": *repoFlag, + "content": string(content), + }).Do(context.Background(), &result); err != nil || !ok { + return err + } + + return nil + } + + // Register the command. + codeownersCommands = append(codeownersCommands, &command{ + flagSet: flagSet, + handler: handler, + usageFunc: usageFunc, + }) +} diff --git a/cmd/src/main.go b/cmd/src/main.go index 177a622672..269f20b424 100644 --- a/cmd/src/main.go +++ b/cmd/src/main.go @@ -47,6 +47,7 @@ The commands are: serve-git serves your local git repositories over HTTP for Sourcegraph to pull upload upload an index to a Sourcegraph instance users,user manages users + codeowners manages code ownership information version display and compare the src-cli version against the recommended version for your instance Use "src [command] -h" for more information about a command. From 58875d911fa6acc4c9b080f263d8a7f1307479f6 Mon Sep 17 00:00:00 2001 From: Erik Seliger Date: Thu, 2 Mar 2023 23:15:27 +0100 Subject: [PATCH 2/3] Fixups --- cmd/src/codeowners_create.go | 16 +++++++++++++++- cmd/src/codeowners_delete.go | 18 +++++++++++++++--- cmd/src/codeowners_get.go | 18 +++++++++++------- cmd/src/codeowners_update.go | 17 +++++++++++++++-- 4 files changed, 56 insertions(+), 13 deletions(-) diff --git a/cmd/src/codeowners_create.go b/cmd/src/codeowners_create.go index a7f8ff4b1c..f90f299159 100644 --- a/cmd/src/codeowners_create.go +++ b/cmd/src/codeowners_create.go @@ -5,10 +5,12 @@ import ( "flag" "fmt" "io" + "strings" "github.com/sourcegraph/sourcegraph/lib/errors" "github.com/sourcegraph/src-cli/internal/api" + "github.com/sourcegraph/src-cli/internal/cmderrors" ) func init() { @@ -65,9 +67,10 @@ Examples: $repoName: String!, $content: String! ) { - addCodeownersFile( + addCodeownersFile(input: { repoName: $repoName, fileContents: $content, + } ) { ...CodeownersFileFields } @@ -81,6 +84,17 @@ Examples: "repoName": *repoFlag, "content": string(content), }).Do(context.Background(), &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)) + } + 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")) + } + } + } return err } diff --git a/cmd/src/codeowners_delete.go b/cmd/src/codeowners_delete.go index 7897f6109c..4915d83662 100644 --- a/cmd/src/codeowners_delete.go +++ b/cmd/src/codeowners_delete.go @@ -4,10 +4,12 @@ import ( "context" "flag" "fmt" + "strings" "github.com/sourcegraph/sourcegraph/lib/errors" "github.com/sourcegraph/src-cli/internal/api" + "github.com/sourcegraph/src-cli/internal/cmderrors" ) func init() { @@ -43,11 +45,10 @@ Examples: query := `mutation DeleteCodeownersFile( $repoName: String!, - $content: String! ) { - deleteCodeownersFile( + deleteCodeownersFiles(repositories: [{ repoName: $repoName, - ) { + }]) { alwaysNil } } @@ -59,6 +60,17 @@ Examples: if ok, err := client.NewRequest(query, map[string]interface{}{ "repoName": *repoFlag, }).Do(context.Background(), &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)) + } + if strings.Contains(e.Error(), "codeowners file not found:") { + return cmderrors.ExitCode(2, errors.Newf("no data found for repository %q", *repoFlag)) + } + } + } return err } diff --git a/cmd/src/codeowners_get.go b/cmd/src/codeowners_get.go index 4761df2043..329d3b0943 100644 --- a/cmd/src/codeowners_get.go +++ b/cmd/src/codeowners_get.go @@ -43,11 +43,11 @@ Examples: client := cfg.apiClient(apiFlags, flagSet.Output()) - query := `mutation GetCodeownersFile( + query := `query GetCodeownersFile( $repoName: String! ) { - repo(name: $repoName) { - ingestedCodeownersFile { + repository(name: $repoName) { + ingestedCodeowners { ...CodeownersFileFields } } @@ -55,8 +55,8 @@ Examples: ` + codeownersFragment var result struct { - Repo *struct { - IngestedCodeownersFile CodeownersIngestedFile + Repository *struct { + IngestedCodeowners *CodeownersIngestedFile } } if ok, err := client.NewRequest(query, map[string]interface{}{ @@ -65,11 +65,15 @@ Examples: return err } - if result.Repo == nil { + if result.Repository == nil { return cmderrors.ExitCode(2, errors.Newf("repository %q not found", *repoFlag)) } - fmt.Fprintf(os.Stdout, "%s", result.Repo.IngestedCodeownersFile.Contents) + if result.Repository.IngestedCodeowners == nil { + return cmderrors.ExitCode(2, errors.Newf("no codeowners data found for %q", *repoFlag)) + } + + fmt.Fprintf(os.Stdout, "%s", result.Repository.IngestedCodeowners.Contents) return nil } diff --git a/cmd/src/codeowners_update.go b/cmd/src/codeowners_update.go index 1bcff258ec..767cdd890d 100644 --- a/cmd/src/codeowners_update.go +++ b/cmd/src/codeowners_update.go @@ -5,10 +5,12 @@ import ( "flag" "fmt" "io" + "strings" "github.com/sourcegraph/sourcegraph/lib/errors" "github.com/sourcegraph/src-cli/internal/api" + "github.com/sourcegraph/src-cli/internal/cmderrors" ) func init() { @@ -65,10 +67,10 @@ Examples: $repoName: String!, $content: String! ) { - updateCodeownersFile( + updateCodeownersFile(input: { repoName: $repoName, fileContents: $content, - ) { + }) { ...CodeownersFileFields } } @@ -81,6 +83,17 @@ Examples: "repoName": *repoFlag, "content": string(content), }).Do(context.Background(), &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)) + } + 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")) + } + } + } return err } From 7b6a713ec81ce2503a267f6d6dd980ab55b2d59d Mon Sep 17 00:00:00 2001 From: Erik Seliger Date: Fri, 3 Mar 2023 15:15:54 +0100 Subject: [PATCH 3/3] Fixups --- cmd/src/codeowners_create.go | 18 +++++++++++++----- cmd/src/codeowners_delete.go | 2 +- cmd/src/codeowners_get.go | 2 +- cmd/src/codeowners_update.go | 19 +++++++++++++------ 4 files changed, 28 insertions(+), 13 deletions(-) diff --git a/cmd/src/codeowners_create.go b/cmd/src/codeowners_create.go index f90f299159..7daf00ba21 100644 --- a/cmd/src/codeowners_create.go +++ b/cmd/src/codeowners_create.go @@ -33,8 +33,10 @@ Examples: fmt.Println(usage) } var ( - repoFlag = flagSet.String("repo", "", "The repository to attach the data to") - fileFlag = flagSet.String("f", "", "File path to read ownership information from (- for stdin)") + 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) ) @@ -44,11 +46,17 @@ Examples: } if *repoFlag == "" { - return errors.New("provide a repo name") + return errors.New("provide a repo name using -repo") } - if *fileFlag == "" { - return errors.New("provide a file") + 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) diff --git a/cmd/src/codeowners_delete.go b/cmd/src/codeowners_delete.go index 4915d83662..00ab581023 100644 --- a/cmd/src/codeowners_delete.go +++ b/cmd/src/codeowners_delete.go @@ -38,7 +38,7 @@ Examples: } if *repoFlag == "" { - return errors.New("provide a repo name") + return errors.New("provide a repo name using -repo") } client := cfg.apiClient(apiFlags, flagSet.Output()) diff --git a/cmd/src/codeowners_get.go b/cmd/src/codeowners_get.go index 329d3b0943..ce4e78a855 100644 --- a/cmd/src/codeowners_get.go +++ b/cmd/src/codeowners_get.go @@ -38,7 +38,7 @@ Examples: } if *repoFlag == "" { - return errors.New("provide a repo name") + return errors.New("provide a repo name using -repo") } client := cfg.apiClient(apiFlags, flagSet.Output()) diff --git a/cmd/src/codeowners_update.go b/cmd/src/codeowners_update.go index 767cdd890d..d27292cd0c 100644 --- a/cmd/src/codeowners_update.go +++ b/cmd/src/codeowners_update.go @@ -33,9 +33,10 @@ Examples: fmt.Println(usage) } var ( - repoFlag = flagSet.String("repo", "", "The repository to attach the data to") - fileFlag = flagSet.String("f", "", "File path to read ownership information from (- for stdin)") - apiFlags = api.NewFlags(flagSet) + 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 { @@ -44,11 +45,17 @@ Examples: } if *repoFlag == "" { - return errors.New("provide a repo name") + return errors.New("provide a repo name using -repo") } - if *fileFlag == "" { - return errors.New("provide a file") + 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)