Skip to content

Commit 98509d0

Browse files
authored
Add CLI (#20)
1 parent 8a95900 commit 98509d0

17 files changed

Lines changed: 318 additions & 24 deletions

File tree

.github/workflows/go.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ jobs:
3434
run: go build -v .
3535

3636
- name: Test
37-
run: go test -v -race -covermode atomic -coverprofile=profile.cov ./...
37+
run: go test -v -race -covermode atomic -coverprofile=profile.cov -tags=unit ./...
3838

3939
- name: Send coverage
4040
env:

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@
22
[![Go Report Card](https://goreportcard.com/badge/github.com/fmenezes/codeowners)](https://goreportcard.com/report/github.com/fmenezes/codeowners)
33
[![Coverage](https://coveralls.io/repos/github/fmenezes/codeowners/badge.svg?branch=master)](https://coveralls.io/github/fmenezes/codeowners?branch=master)
44

5-
# CODEOWNERS Decoder
5+
# CODEOWNERS
66

7-
CodeOwners decoder provides funcionality to evaluate CODEOWNERS file in Go.
7+
CodeOwners coder provides funcionality to evaluate CODEOWNERS file in Go. Also provices linter CLI.
88

99
## Documentation
1010

11-
To find documentation follow https://godoc.org/github.com/fmenezes/codeowners
11+
To find package documentation follow https://godoc.org/github.com/fmenezes/codeowners
1212

1313
## Compatibility
1414

checker.go

Lines changed: 55 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ package codeowners
22

33
import (
44
"fmt"
5-
"io"
5+
"os"
6+
"path/filepath"
7+
"strings"
68
)
79

810
var availableCheckers map[string]Checker
@@ -60,9 +62,21 @@ type CheckResult struct {
6062
}
6163

6264
// Check evaluates the file contents against the checkers and return the results back.
63-
func Check(r io.Reader, checkers ...string) ([]CheckResult, error) {
65+
func Check(directory string, checkers ...string) ([]CheckResult, error) {
66+
67+
fileLocation, result := findCodeownersFile(directory)
68+
if result != nil {
69+
return []CheckResult{*result}, nil
70+
}
71+
72+
file, err := os.Open(fileLocation)
73+
if err != nil {
74+
return nil, err
75+
}
76+
defer file.Close()
77+
6478
results := []CheckResult{}
65-
decoder := NewDecoder(r)
79+
decoder := NewDecoder(file)
6680
for decoder.More() {
6781
token, lineNo := decoder.Token()
6882
for _, checker := range checkers {
@@ -71,9 +85,45 @@ func Check(r io.Reader, checkers ...string) ([]CheckResult, error) {
7185
return nil, fmt.Errorf("'%s' not found", checker)
7286
}
7387
lineResults := c.CheckLine(lineNo, token.Path(), token.Owners()...)
74-
results = append(results, lineResults...)
88+
if lineResults != nil {
89+
results = append(results, lineResults...)
90+
}
7591
}
7692
}
7793

78-
return results, nil
94+
if len(results) > 0 {
95+
return results, nil
96+
}
97+
98+
return nil, nil
99+
}
100+
101+
func fileExists(file string) bool {
102+
info, err := os.Stat(file)
103+
return !os.IsNotExist(err) && !info.IsDir()
104+
}
105+
106+
func findCodeownersFile(dir string) (string, *CheckResult) {
107+
codeownersLocation := ""
108+
109+
filesFound := []string{}
110+
for _, fileLocation := range DefaultLocations {
111+
currentFile := filepath.Join(dir, fileLocation)
112+
if fileExists(currentFile) {
113+
filesFound = append(filesFound, fileLocation)
114+
if len(codeownersLocation) == 0 {
115+
codeownersLocation = currentFile
116+
}
117+
}
118+
}
119+
120+
if len(filesFound) == 0 {
121+
return "", &CheckResult{Message: "No CODEOWNERS file found", Severity: Error, CheckName: "NoCodeowners"}
122+
}
123+
124+
if len(filesFound) > 1 {
125+
return "", &CheckResult{Message: fmt.Sprintf("Multiple CODEOWNERS files found (%s)", strings.Join(filesFound, ", ")), Severity: Warning, CheckName: "MultipleCodeowners"}
126+
}
127+
128+
return codeownersLocation, nil
79129
}

checker_test.go

Lines changed: 64 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package codeowners_test
33
import (
44
"fmt"
55
"reflect"
6-
"strings"
76
"testing"
87

98
"github.com/fmenezes/codeowners"
@@ -60,7 +59,7 @@ func TestSeverityLevelLabels(t *testing.T) {
6059
}
6160

6261
func TestSimpleCheck(t *testing.T) {
63-
input := `filepattern @owner`
62+
input := "./test/data/pass"
6463
want := []codeowners.CheckResult{
6564
{
6665
LineNo: 1,
@@ -71,7 +70,7 @@ func TestSimpleCheck(t *testing.T) {
7170
}
7271

7372
codeowners.RegisterChecker(dummyCheckerName, dummyChecker{})
74-
got, err := codeowners.Check(strings.NewReader(input), dummyCheckerName)
73+
got, err := codeowners.Check(input, dummyCheckerName)
7574
if err != nil {
7675
t.Errorf("Input %s, Error %v", input, err)
7776
}
@@ -80,15 +79,73 @@ func TestSimpleCheck(t *testing.T) {
8079
}
8180
}
8281

83-
func ExampleCheck() {
84-
contents := strings.NewReader(`filepattern`)
85-
checks, err := codeowners.Check(contents, "NoOwner")
82+
func TestNoProblemsFound(t *testing.T) {
83+
input := "./test/data/pass"
84+
got, err := codeowners.Check(input)
85+
if err != nil {
86+
t.Errorf("Input %s, Error %v", input, err)
87+
}
88+
if got != nil {
89+
t.Errorf("Input %s, Want %v, Got %v", input, nil, got)
90+
}
91+
}
92+
93+
func TestCheckerNotFound(t *testing.T) {
94+
input := "./test/data/pass"
95+
_, err := codeowners.Check(input, "NonExistentChecker")
96+
if err == nil {
97+
t.Error("Should have errored")
98+
}
99+
}
100+
101+
func TestNoCodeownersCheck(t *testing.T) {
102+
input := "./test/data"
103+
want := []codeowners.CheckResult{
104+
{
105+
LineNo: 0,
106+
Message: "No CODEOWNERS file found",
107+
Severity: codeowners.Error,
108+
CheckName: "NoCodeowners",
109+
},
110+
}
111+
112+
got, err := codeowners.Check(input, dummyCheckerName)
113+
if err != nil {
114+
t.Errorf("Input %s, Error %v", input, err)
115+
}
116+
if !reflect.DeepEqual(want, got) {
117+
t.Errorf("Input %s, Want %v, Got %v", input, want, got)
118+
}
119+
}
120+
121+
func TestMultipleCodeownersCheck(t *testing.T) {
122+
input := "./test/data/multiple_codeowners"
123+
want := []codeowners.CheckResult{
124+
{
125+
LineNo: 0,
126+
Message: "Multiple CODEOWNERS files found (CODEOWNERS, docs/CODEOWNERS)",
127+
Severity: codeowners.Warning,
128+
CheckName: "MultipleCodeowners",
129+
},
130+
}
131+
132+
got, err := codeowners.Check(input, dummyCheckerName)
86133
if err != nil {
134+
t.Errorf("Input %s, Error %v", input, err)
135+
}
136+
if !reflect.DeepEqual(want, got) {
137+
t.Errorf("Input %s, Want %v, Got %v", input, want, got)
138+
}
139+
}
87140

141+
func ExampleCheck() {
142+
checks, err := codeowners.Check(".", codeowners.AvailableCheckers()...)
143+
if err != nil {
144+
panic(err)
88145
}
89146
for _, check := range checks {
90147
fmt.Printf("%d ::%s:: %s [%s]\n", check.LineNo, check.Severity, check.Message, check.CheckName)
91148
}
92149
//Output:
93-
//1 ::Error:: No owners specified [NoOwner]
150+
//0 ::Error:: No CODEOWNERS file found [NoCodeowners]
94151
}

checkers/noowner.go

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,18 @@ type NoOwner struct{}
1313

1414
// CheckLine runs this NoOwner's check against each line
1515
func (c NoOwner) CheckLine(lineNo int, pattern string, owners ...string) []codeowners.CheckResult {
16-
results := []codeowners.CheckResult{}
16+
var results []codeowners.CheckResult
1717

1818
if len(owners) == 0 {
19-
results = append(results, codeowners.CheckResult{
20-
LineNo: lineNo,
21-
Message: "No owners specified",
22-
Severity: codeowners.Error,
23-
CheckName: noOwnerCheckerName,
24-
})
25-
19+
results = []codeowners.CheckResult{
20+
{
21+
LineNo: lineNo,
22+
Message: "No owners specified",
23+
Severity: codeowners.Error,
24+
CheckName: noOwnerCheckerName,
25+
},
26+
}
2627
}
28+
2729
return results
2830
}

cmd/linter.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package main
2+
3+
import (
4+
"io"
5+
"path/filepath"
6+
"text/template"
7+
8+
"github.com/fmenezes/codeowners"
9+
_ "github.com/fmenezes/codeowners/checkers"
10+
)
11+
12+
type options struct {
13+
directory string
14+
format string
15+
}
16+
17+
func run(wr io.Writer, opt options) error {
18+
dir, err := filepath.Abs(opt.directory)
19+
if err != nil {
20+
return err
21+
}
22+
23+
format := "{{range .}}{{ .LineNo }} ::{{ .Severity }}:: {{ .Message }} [{{ .CheckName }}]\n{{end}}"
24+
if len(opt.format) > 0 {
25+
format = opt.format
26+
}
27+
tpl, err := template.New("main").Parse(format)
28+
if err != nil {
29+
return err
30+
}
31+
32+
checkers := codeowners.AvailableCheckers()
33+
34+
checks, _ := codeowners.Check(dir, checkers...)
35+
36+
if len(checks) > 0 {
37+
err = tpl.Execute(wr, checks)
38+
if err != nil {
39+
return err
40+
}
41+
} else {
42+
wr.Write([]byte("Everything ok ;)\n"))
43+
}
44+
45+
return nil
46+
}

cmd/linter_test.go

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"testing"
6+
)
7+
8+
func testRun(opt options) (string, error) {
9+
var output bytes.Buffer
10+
err := run(&output, opt)
11+
if err != nil {
12+
return "", err
13+
}
14+
return output.String(), nil
15+
}
16+
17+
func TestPass(t *testing.T) {
18+
opt := options{
19+
directory: "../test/data/pass",
20+
format: "",
21+
}
22+
23+
got, err := testRun(opt)
24+
if err != nil {
25+
t.Error(err)
26+
}
27+
28+
want := `Everything ok ;)
29+
`
30+
if got != want {
31+
t.Errorf("Input: %v Want: '%s' Got: '%s'", opt, want, got)
32+
}
33+
}
34+
35+
func TestNoOwners(t *testing.T) {
36+
opt := options{
37+
directory: "../test/data/no_owners",
38+
format: "",
39+
}
40+
41+
got, err := testRun(opt)
42+
if err != nil {
43+
t.Error(err)
44+
}
45+
46+
want := `1 ::Error:: No owners specified [NoOwner]
47+
`
48+
if got != want {
49+
t.Errorf("Input: %v Want: '%s' Got: '%s'", opt, want, got)
50+
}
51+
}
52+
53+
func TestCustomFormat(t *testing.T) {
54+
opt := options{
55+
directory: "../test/data/noowners",
56+
format: "test",
57+
}
58+
59+
got, err := testRun(opt)
60+
if err != nil {
61+
t.Error(err)
62+
}
63+
64+
want := `test`
65+
if got != want {
66+
t.Errorf("Input: %v Want: '%s' Got: '%s'", opt, want, got)
67+
}
68+
}
69+
70+
func TestInvalidFormat(t *testing.T) {
71+
opt := options{
72+
directory: "../test/data/noowners",
73+
format: " {{template \"one\"}} ",
74+
}
75+
76+
_, err := testRun(opt)
77+
if err == nil {
78+
t.Errorf("Should have errored")
79+
}
80+
81+
opt = options{
82+
directory: "../test/data/noowners",
83+
format: " {{ . ",
84+
}
85+
86+
_, err = testRun(opt)
87+
if err == nil {
88+
t.Errorf("Should have errored")
89+
}
90+
}
91+
92+
func TestInvalidDirectory(t *testing.T) {
93+
opt := options{
94+
directory: "'",
95+
format: "",
96+
}
97+
98+
got, err := testRun(opt)
99+
if err != nil {
100+
t.Error(err)
101+
}
102+
103+
want := `0 ::Error:: No CODEOWNERS file found [NoCodeowners]
104+
`
105+
if got != want {
106+
t.Errorf("Input: %v Want: '%s' Got: '%s'", opt, want, got)
107+
}
108+
}

0 commit comments

Comments
 (0)