Skip to content

Commit 3db4982

Browse files
authored
Merge pull request #29 from mithun/issue-28
Implement `-check` flag and exit codes
2 parents 9fa18aa + 27146d5 commit 3db4982

File tree

6 files changed

+191
-42
lines changed

6 files changed

+191
-42
lines changed

.travis.yml

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1-
dist: xenial
1+
dist: bionic
22
git:
33
depth: 3
44
language: go
55
go:
6-
- "1.x"
7-
- "1.10.x"
6+
- 1.13.x
87
before_script:
98
- go get golang.org/x/lint/golint
109
script:
10+
- go mod tidy
1111
- gofmt -d -e -l -s .
1212
- golint -set_exit_status ./...
1313
- go test -v ./...

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ to any file that already has one.
1818
-f custom license file (no default)
1919
-l license type: apache, bsd, mit (defaults to "apache")
2020
-y year (defaults to current year)
21+
-check check only mode: verify presence of license headers and exit with non-zero code if missing
2122

2223
The pattern argument can be provided multiple times, and may also refer
2324
to single files.

go.mod

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
module github.com/google/addlicense
2+
3+
go 1.13
4+
5+
require golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e

go.sum

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY=
2+
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=

main.go

+83-39
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package main
1818

1919
import (
2020
"bytes"
21+
"errors"
2122
"flag"
2223
"fmt"
2324
"html/template"
@@ -26,8 +27,9 @@ import (
2627
"os"
2728
"path/filepath"
2829
"strings"
29-
"sync"
3030
"time"
31+
32+
"golang.org/x/sync/errgroup"
3133
)
3234

3335
const helpText = `Usage: addlicense [flags] pattern [pattern ...]
@@ -45,11 +47,12 @@ Flags:
4547
`
4648

4749
var (
48-
holder = flag.String("c", "Google LLC", "copyright holder")
49-
license = flag.String("l", "apache", "license type: apache, bsd, mit")
50-
licensef = flag.String("f", "", "license file")
51-
year = flag.String("y", fmt.Sprint(time.Now().Year()), "copyright year(s)")
52-
verbose = flag.Bool("v", false, "verbose mode: print the name of the files that are modified")
50+
holder = flag.String("c", "Google LLC", "copyright holder")
51+
license = flag.String("l", "apache", "license type: apache, bsd, mit")
52+
licensef = flag.String("f", "", "license file")
53+
year = flag.String("y", fmt.Sprint(time.Now().Year()), "copyright year(s)")
54+
verbose = flag.Bool("v", false, "verbose mode: print the name of the files that are modified")
55+
checkonly = flag.Bool("check", false, "check only mode: verify presence of license headers and exit with non-zero code if missing")
5356
)
5457

5558
func main() {
@@ -92,23 +95,48 @@ func main() {
9295
ch := make(chan *file, 1000)
9396
done := make(chan struct{})
9497
go func() {
95-
var wg sync.WaitGroup
98+
var wg errgroup.Group
9699
for f := range ch {
97-
wg.Add(1)
98-
go func(f *file) {
99-
defer wg.Done()
100-
modified, err := addLicense(f.path, f.mode, t, data)
101-
if err != nil {
102-
log.Printf("%s: %v", f.path, err)
103-
return
104-
}
105-
if *verbose && modified {
106-
log.Printf("%s modified", f.path)
100+
f := f // https://golang.org/doc/faq#closures_and_goroutines
101+
wg.Go(func() error {
102+
if *checkonly {
103+
// Check if file extension is known
104+
lic, err := licenseHeader(f.path, t, data)
105+
if err != nil {
106+
log.Printf("%s: %v", f.path, err)
107+
return err
108+
}
109+
if lic == nil { // Unknown fileExtension
110+
return nil
111+
}
112+
// Check if file has a license
113+
isMissingLicenseHeader, err := fileHasLicense(f.path)
114+
if err != nil {
115+
log.Printf("%s: %v", f.path, err)
116+
return err
117+
}
118+
if isMissingLicenseHeader {
119+
fmt.Printf("%s\n", f.path)
120+
return errors.New("missing license header")
121+
}
122+
} else {
123+
modified, err := addLicense(f.path, f.mode, t, data)
124+
if err != nil {
125+
log.Printf("%s: %v", f.path, err)
126+
return err
127+
}
128+
if *verbose && modified {
129+
log.Printf("%s modified", f.path)
130+
}
107131
}
108-
}(f)
132+
return nil
133+
})
109134
}
110-
wg.Wait()
135+
err := wg.Wait()
111136
close(done)
137+
if err != nil {
138+
os.Exit(1)
139+
}
112140
}()
113141

114142
for _, d := range flag.Args() {
@@ -138,11 +166,45 @@ func walk(ch chan<- *file, start string) {
138166
}
139167

140168
func addLicense(path string, fmode os.FileMode, tmpl *template.Template, data *copyrightData) (bool, error) {
169+
var lic []byte
170+
var err error
171+
lic, err = licenseHeader(path, tmpl, data)
172+
if err != nil || lic == nil {
173+
return false, err
174+
}
175+
176+
b, err := ioutil.ReadFile(path)
177+
if err != nil || hasLicense(b) {
178+
return false, err
179+
}
180+
181+
line := hashBang(b)
182+
if len(line) > 0 {
183+
b = b[len(line):]
184+
if line[len(line)-1] != '\n' {
185+
line = append(line, '\n')
186+
}
187+
lic = append(line, lic...)
188+
}
189+
b = append(lic, b...)
190+
return true, ioutil.WriteFile(path, b, fmode)
191+
}
192+
193+
// fileHasLicense reports whether the file at path contains a license header.
194+
func fileHasLicense(path string) (bool, error) {
195+
b, err := ioutil.ReadFile(path)
196+
if err != nil || hasLicense(b) {
197+
return false, err
198+
}
199+
return true, nil
200+
}
201+
202+
func licenseHeader(path string, tmpl *template.Template, data *copyrightData) ([]byte, error) {
141203
var lic []byte
142204
var err error
143205
switch fileExtension(path) {
144206
default:
145-
return false, nil
207+
return nil, nil
146208
case ".c", ".h":
147209
lic, err = prefix(tmpl, data, "/*", " * ", " */")
148210
case ".js", ".jsx", ".tsx", ".css", ".tf", ".ts":
@@ -164,25 +226,7 @@ func addLicense(path string, fmode os.FileMode, tmpl *template.Template, data *c
164226
case ".ml", ".mli", ".mll", ".mly":
165227
lic, err = prefix(tmpl, data, "(**", " ", "*)")
166228
}
167-
if err != nil || lic == nil {
168-
return false, err
169-
}
170-
171-
b, err := ioutil.ReadFile(path)
172-
if err != nil || hasLicense(b) {
173-
return false, err
174-
}
175-
176-
line := hashBang(b)
177-
if len(line) > 0 {
178-
b = b[len(line):]
179-
if line[len(line)-1] != '\n' {
180-
line = append(line, '\n')
181-
}
182-
lic = append(line, lic...)
183-
}
184-
b = append(lic, b...)
185-
return true, ioutil.WriteFile(path, b, fmode)
229+
return lic, err
186230
}
187231

188232
func fileExtension(name string) string {

main_test.go

+97
Original file line numberDiff line numberDiff line change
@@ -87,3 +87,100 @@ func TestMultiyear(t *testing.T) {
8787
}
8888
run(t, "diff", samplefile, sampleLicensed)
8989
}
90+
91+
func TestWriteErrors(t *testing.T) {
92+
if os.Getenv("RUNME") != "" {
93+
main()
94+
return
95+
}
96+
97+
tmp := tempDir(t)
98+
t.Logf("tmp dir: %s", tmp)
99+
samplefile := filepath.Join(tmp, "file.c")
100+
101+
run(t, "cp", "testdata/initial/file.c", samplefile)
102+
run(t, "chmod", "0444", samplefile)
103+
cmd := exec.Command(os.Args[0],
104+
"-test.run=TestWriteErrors",
105+
"-l", "apache", "-c", "Google LLC", "-y", "2018",
106+
samplefile,
107+
)
108+
cmd.Env = []string{"RUNME=1"}
109+
out, err := cmd.CombinedOutput()
110+
if err == nil {
111+
run(t, "chmod", "0644", samplefile)
112+
t.Fatalf("TestWriteErrors exited with a zero exit code.\n%s", out)
113+
}
114+
run(t, "chmod", "0644", samplefile)
115+
}
116+
117+
func TestReadErrors(t *testing.T) {
118+
if os.Getenv("RUNME") != "" {
119+
main()
120+
return
121+
}
122+
123+
tmp := tempDir(t)
124+
t.Logf("tmp dir: %s", tmp)
125+
samplefile := filepath.Join(tmp, "file.c")
126+
127+
run(t, "cp", "testdata/initial/file.c", samplefile)
128+
run(t, "chmod", "a-r", samplefile)
129+
cmd := exec.Command(os.Args[0],
130+
"-test.run=TestReadErrors",
131+
"-l", "apache", "-c", "Google LLC", "-y", "2018",
132+
samplefile,
133+
)
134+
cmd.Env = []string{"RUNME=1"}
135+
out, err := cmd.CombinedOutput()
136+
if err == nil {
137+
run(t, "chmod", "0644", samplefile)
138+
t.Fatalf("TestWriteErrors exited with a zero exit code.\n%s", out)
139+
}
140+
run(t, "chmod", "0644", samplefile)
141+
}
142+
143+
func TestCheckSuccess(t *testing.T) {
144+
if os.Getenv("RUNME") != "" {
145+
main()
146+
return
147+
}
148+
149+
tmp := tempDir(t)
150+
t.Logf("tmp dir: %s", tmp)
151+
samplefile := filepath.Join(tmp, "file.c")
152+
153+
run(t, "cp", "testdata/expected/file.c", samplefile)
154+
cmd := exec.Command(os.Args[0],
155+
"-test.run=TestCheckSuccess",
156+
"-l", "apache", "-c", "Google LLC", "-y", "2018",
157+
"-check", samplefile,
158+
)
159+
cmd.Env = []string{"RUNME=1"}
160+
if out, err := cmd.CombinedOutput(); err != nil {
161+
t.Fatalf("%v\n%s", err, out)
162+
}
163+
}
164+
165+
func TestCheckFail(t *testing.T) {
166+
if os.Getenv("RUNME") != "" {
167+
main()
168+
return
169+
}
170+
171+
tmp := tempDir(t)
172+
t.Logf("tmp dir: %s", tmp)
173+
samplefile := filepath.Join(tmp, "file.c")
174+
175+
run(t, "cp", "testdata/initial/file.c", samplefile)
176+
cmd := exec.Command(os.Args[0],
177+
"-test.run=TestCheckFail",
178+
"-l", "apache", "-c", "Google LLC", "-y", "2018",
179+
"-check", samplefile,
180+
)
181+
cmd.Env = []string{"RUNME=1"}
182+
out, err := cmd.CombinedOutput()
183+
if err == nil {
184+
t.Fatalf("TestCheckFail exited with a zero exit code.\n%s", out)
185+
}
186+
}

0 commit comments

Comments
 (0)