Skip to content

Commit 6c2dd1b

Browse files
authored
Implement CLI for ingested codeowners (#943)
1 parent d2d29c8 commit 6c2dd1b

File tree

6 files changed

+475
-0
lines changed

6 files changed

+475
-0
lines changed

cmd/src/codeowners.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package main
2+
3+
import (
4+
"flag"
5+
"fmt"
6+
"io"
7+
"os"
8+
)
9+
10+
var codeownersCommands commander
11+
12+
func init() {
13+
usage := `'src codeowners' is a tool that manages ingested code ownership data in a Sourcegraph instance.
14+
15+
Usage:
16+
17+
src codeowners command [command options]
18+
19+
The commands are:
20+
21+
get returns the codeowners file for a repository, if exists
22+
create create a codeowners file
23+
update update a codeowners file
24+
delete delete a codeowners file
25+
26+
Use "src codeowners [command] -h" for more information about a command.
27+
`
28+
29+
flagSet := flag.NewFlagSet("codeowners", flag.ExitOnError)
30+
handler := func(args []string) error {
31+
codeownersCommands.run(flagSet, "src codeowners", usage, args)
32+
return nil
33+
}
34+
35+
// Register the command.
36+
commands = append(commands, &command{
37+
flagSet: flagSet,
38+
aliases: []string{"codeowner"},
39+
handler: handler,
40+
usageFunc: func() {
41+
fmt.Println(usage)
42+
},
43+
})
44+
}
45+
46+
const codeownersFragment = `
47+
fragment CodeownersFileFields on CodeownersIngestedFile {
48+
contents
49+
repository {
50+
name
51+
}
52+
}
53+
`
54+
55+
type CodeownersIngestedFile struct {
56+
Contents string `json:"contents"`
57+
Repository struct {
58+
Name string `json:"name"`
59+
} `json:"repository"`
60+
}
61+
62+
func readFile(f string) (io.Reader, error) {
63+
if f == "-" {
64+
return os.Stdin, nil
65+
}
66+
return os.Open(f)
67+
}

cmd/src/codeowners_create.go

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"flag"
6+
"fmt"
7+
"io"
8+
"strings"
9+
10+
"github.com/sourcegraph/sourcegraph/lib/errors"
11+
12+
"github.com/sourcegraph/src-cli/internal/api"
13+
"github.com/sourcegraph/src-cli/internal/cmderrors"
14+
)
15+
16+
func init() {
17+
usage := `
18+
Examples:
19+
20+
Create a codeowners file for the repository "github.com/sourcegraph/sourcegraph":
21+
22+
$ src codeowners create -repo='github.com/sourcegraph/sourcegraph' -f CODEOWNERS
23+
24+
Create a codeowners file for the repository "github.com/sourcegraph/sourcegraph" from stdin:
25+
26+
$ src codeowners create -repo='github.com/sourcegraph/sourcegraph' -f -
27+
`
28+
29+
flagSet := flag.NewFlagSet("create", flag.ExitOnError)
30+
usageFunc := func() {
31+
fmt.Fprintf(flag.CommandLine.Output(), "Usage of 'src codeowners %s':\n", flagSet.Name())
32+
flagSet.PrintDefaults()
33+
fmt.Println(usage)
34+
}
35+
var (
36+
repoFlag = flagSet.String("repo", "", "The repository to attach the data to")
37+
fileFlag = flagSet.String("file", "", "File path to read ownership information from (- for stdin)")
38+
fileShortFlag = flagSet.String("f", "", "File path to read ownership information from (- for stdin). Alias for -file")
39+
40+
apiFlags = api.NewFlags(flagSet)
41+
)
42+
43+
handler := func(args []string) error {
44+
if err := flagSet.Parse(args); err != nil {
45+
return err
46+
}
47+
48+
if *repoFlag == "" {
49+
return errors.New("provide a repo name using -repo")
50+
}
51+
52+
if *fileFlag == "" && *fileShortFlag == "" {
53+
return errors.New("provide a file using -file")
54+
}
55+
if *fileFlag != "" && *fileShortFlag != "" {
56+
return errors.New("have to provide either -file or -f")
57+
}
58+
if *fileShortFlag != "" {
59+
*fileFlag = *fileShortFlag
60+
}
61+
62+
file, err := readFile(*fileFlag)
63+
if err != nil {
64+
return err
65+
}
66+
67+
content, err := io.ReadAll(file)
68+
if err != nil {
69+
return err
70+
}
71+
72+
client := cfg.apiClient(apiFlags, flagSet.Output())
73+
74+
query := `mutation CreateCodeownersFile(
75+
$repoName: String!,
76+
$content: String!
77+
) {
78+
addCodeownersFile(input: {
79+
repoName: $repoName,
80+
fileContents: $content,
81+
}
82+
) {
83+
...CodeownersFileFields
84+
}
85+
}
86+
` + codeownersFragment
87+
88+
var result struct {
89+
AddCodeownersFile CodeownersIngestedFile
90+
}
91+
if ok, err := client.NewRequest(query, map[string]interface{}{
92+
"repoName": *repoFlag,
93+
"content": string(content),
94+
}).Do(context.Background(), &result); err != nil || !ok {
95+
var gqlErr api.GraphQlErrors
96+
if errors.As(err, &gqlErr) {
97+
for _, e := range gqlErr {
98+
if strings.Contains(e.Error(), "repo not found:") {
99+
return cmderrors.ExitCode(2, errors.Newf("repository %q not found", *repoFlag))
100+
}
101+
if strings.Contains(e.Error(), "codeowners file has already been ingested for this repository") {
102+
return cmderrors.ExitCode(2, errors.New("codeowners file has already been ingested for this repository"))
103+
}
104+
}
105+
}
106+
return err
107+
}
108+
109+
return nil
110+
}
111+
112+
// Register the command.
113+
codeownersCommands = append(codeownersCommands, &command{
114+
flagSet: flagSet,
115+
handler: handler,
116+
usageFunc: usageFunc,
117+
})
118+
}

cmd/src/codeowners_delete.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"flag"
6+
"fmt"
7+
"strings"
8+
9+
"github.com/sourcegraph/sourcegraph/lib/errors"
10+
11+
"github.com/sourcegraph/src-cli/internal/api"
12+
"github.com/sourcegraph/src-cli/internal/cmderrors"
13+
)
14+
15+
func init() {
16+
usage := `
17+
Examples:
18+
19+
Delete a codeowners file for the repository "github.com/sourcegraph/sourcegraph":
20+
21+
$ src codeowners delete -repo='github.com/sourcegraph/sourcegraph'
22+
`
23+
24+
flagSet := flag.NewFlagSet("delete", flag.ExitOnError)
25+
usageFunc := func() {
26+
fmt.Fprintf(flag.CommandLine.Output(), "Usage of 'src codeowners %s':\n", flagSet.Name())
27+
flagSet.PrintDefaults()
28+
fmt.Println(usage)
29+
}
30+
var (
31+
repoFlag = flagSet.String("repo", "", "The repository to delete the data for")
32+
apiFlags = api.NewFlags(flagSet)
33+
)
34+
35+
handler := func(args []string) error {
36+
if err := flagSet.Parse(args); err != nil {
37+
return err
38+
}
39+
40+
if *repoFlag == "" {
41+
return errors.New("provide a repo name using -repo")
42+
}
43+
44+
client := cfg.apiClient(apiFlags, flagSet.Output())
45+
46+
query := `mutation DeleteCodeownersFile(
47+
$repoName: String!,
48+
) {
49+
deleteCodeownersFiles(repositories: [{
50+
repoName: $repoName,
51+
}]) {
52+
alwaysNil
53+
}
54+
}
55+
`
56+
57+
var result struct {
58+
DeleteCodeownersFile CodeownersIngestedFile
59+
}
60+
if ok, err := client.NewRequest(query, map[string]interface{}{
61+
"repoName": *repoFlag,
62+
}).Do(context.Background(), &result); err != nil || !ok {
63+
var gqlErr api.GraphQlErrors
64+
if errors.As(err, &gqlErr) {
65+
for _, e := range gqlErr {
66+
if strings.Contains(e.Error(), "repo not found:") {
67+
return cmderrors.ExitCode(2, errors.Newf("repository %q not found", *repoFlag))
68+
}
69+
if strings.Contains(e.Error(), "codeowners file not found:") {
70+
return cmderrors.ExitCode(2, errors.Newf("no data found for repository %q", *repoFlag))
71+
}
72+
}
73+
}
74+
return err
75+
}
76+
77+
return nil
78+
}
79+
80+
// Register the command.
81+
codeownersCommands = append(codeownersCommands, &command{
82+
flagSet: flagSet,
83+
handler: handler,
84+
usageFunc: usageFunc,
85+
})
86+
}

cmd/src/codeowners_get.go

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"flag"
6+
"fmt"
7+
"os"
8+
9+
"github.com/sourcegraph/sourcegraph/lib/errors"
10+
11+
"github.com/sourcegraph/src-cli/internal/api"
12+
"github.com/sourcegraph/src-cli/internal/cmderrors"
13+
)
14+
15+
func init() {
16+
usage := `
17+
Examples:
18+
19+
Read the current codeowners file for the repository "github.com/sourcegraph/sourcegraph":
20+
21+
$ src codeowners get -repo='github.com/sourcegraph/sourcegraph'
22+
`
23+
24+
flagSet := flag.NewFlagSet("get", flag.ExitOnError)
25+
usageFunc := func() {
26+
fmt.Fprintf(flag.CommandLine.Output(), "Usage of 'src codeowners %s':\n", flagSet.Name())
27+
flagSet.PrintDefaults()
28+
fmt.Println(usage)
29+
}
30+
var (
31+
repoFlag = flagSet.String("repo", "", "The repository to attach the data to")
32+
apiFlags = api.NewFlags(flagSet)
33+
)
34+
35+
handler := func(args []string) error {
36+
if err := flagSet.Parse(args); err != nil {
37+
return err
38+
}
39+
40+
if *repoFlag == "" {
41+
return errors.New("provide a repo name using -repo")
42+
}
43+
44+
client := cfg.apiClient(apiFlags, flagSet.Output())
45+
46+
query := `query GetCodeownersFile(
47+
$repoName: String!
48+
) {
49+
repository(name: $repoName) {
50+
ingestedCodeowners {
51+
...CodeownersFileFields
52+
}
53+
}
54+
}
55+
` + codeownersFragment
56+
57+
var result struct {
58+
Repository *struct {
59+
IngestedCodeowners *CodeownersIngestedFile
60+
}
61+
}
62+
if ok, err := client.NewRequest(query, map[string]interface{}{
63+
"repoName": *repoFlag,
64+
}).Do(context.Background(), &result); err != nil || !ok {
65+
return err
66+
}
67+
68+
if result.Repository == nil {
69+
return cmderrors.ExitCode(2, errors.Newf("repository %q not found", *repoFlag))
70+
}
71+
72+
if result.Repository.IngestedCodeowners == nil {
73+
return cmderrors.ExitCode(2, errors.Newf("no codeowners data found for %q", *repoFlag))
74+
}
75+
76+
fmt.Fprintf(os.Stdout, "%s", result.Repository.IngestedCodeowners.Contents)
77+
78+
return nil
79+
}
80+
81+
// Register the command.
82+
codeownersCommands = append(codeownersCommands, &command{
83+
flagSet: flagSet,
84+
handler: handler,
85+
usageFunc: usageFunc,
86+
})
87+
}

0 commit comments

Comments
 (0)