diff --git a/.travis.yml b/.travis.yml index 9fdc7d8..606f878 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,13 +1,13 @@ -dist: xenial +dist: bionic git: depth: 3 language: go go: - - "1.x" - - "1.10.x" + - 1.13.x before_script: - go get golang.org/x/lint/golint script: + - go mod tidy - gofmt -d -e -l -s . - golint -set_exit_status ./... - go test -v ./... diff --git a/README.md b/README.md index 8abe9ff..23300c9 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ to any file that already has one. -f custom license file (no default) -l license type: apache, bsd, mit (defaults to "apache") -y year (defaults to current year) + -check check only mode: verify presence of license headers and exit with non-zero code if missing The pattern argument can be provided multiple times, and may also refer to single files. diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..3ab7c66 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module github.com/google/addlicense + +go 1.13 + +require golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..0d5bc22 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= diff --git a/main.go b/main.go index 4d08891..e753753 100644 --- a/main.go +++ b/main.go @@ -18,6 +18,7 @@ package main import ( "bytes" + "errors" "flag" "fmt" "html/template" @@ -26,8 +27,9 @@ import ( "os" "path/filepath" "strings" - "sync" "time" + + "golang.org/x/sync/errgroup" ) const helpText = `Usage: addlicense [flags] pattern [pattern ...] @@ -45,11 +47,12 @@ Flags: ` var ( - holder = flag.String("c", "Google LLC", "copyright holder") - license = flag.String("l", "apache", "license type: apache, bsd, mit") - licensef = flag.String("f", "", "license file") - year = flag.String("y", fmt.Sprint(time.Now().Year()), "copyright year(s)") - verbose = flag.Bool("v", false, "verbose mode: print the name of the files that are modified") + holder = flag.String("c", "Google LLC", "copyright holder") + license = flag.String("l", "apache", "license type: apache, bsd, mit") + licensef = flag.String("f", "", "license file") + year = flag.String("y", fmt.Sprint(time.Now().Year()), "copyright year(s)") + verbose = flag.Bool("v", false, "verbose mode: print the name of the files that are modified") + checkonly = flag.Bool("check", false, "check only mode: verify presence of license headers and exit with non-zero code if missing") ) func main() { @@ -92,23 +95,48 @@ func main() { ch := make(chan *file, 1000) done := make(chan struct{}) go func() { - var wg sync.WaitGroup + var wg errgroup.Group for f := range ch { - wg.Add(1) - go func(f *file) { - defer wg.Done() - modified, err := addLicense(f.path, f.mode, t, data) - if err != nil { - log.Printf("%s: %v", f.path, err) - return - } - if *verbose && modified { - log.Printf("%s modified", f.path) + f := f // https://golang.org/doc/faq#closures_and_goroutines + wg.Go(func() error { + if *checkonly { + // Check if file extension is known + lic, err := licenseHeader(f.path, t, data) + if err != nil { + log.Printf("%s: %v", f.path, err) + return err + } + if lic == nil { // Unknown fileExtension + return nil + } + // Check if file has a license + isMissingLicenseHeader, err := fileHasLicense(f.path) + if err != nil { + log.Printf("%s: %v", f.path, err) + return err + } + if isMissingLicenseHeader { + fmt.Printf("%s\n", f.path) + return errors.New("missing license header") + } + } else { + modified, err := addLicense(f.path, f.mode, t, data) + if err != nil { + log.Printf("%s: %v", f.path, err) + return err + } + if *verbose && modified { + log.Printf("%s modified", f.path) + } } - }(f) + return nil + }) } - wg.Wait() + err := wg.Wait() close(done) + if err != nil { + os.Exit(1) + } }() for _, d := range flag.Args() { @@ -138,11 +166,45 @@ func walk(ch chan<- *file, start string) { } func addLicense(path string, fmode os.FileMode, tmpl *template.Template, data *copyrightData) (bool, error) { + var lic []byte + var err error + lic, err = licenseHeader(path, tmpl, data) + if err != nil || lic == nil { + return false, err + } + + b, err := ioutil.ReadFile(path) + if err != nil || hasLicense(b) { + return false, err + } + + line := hashBang(b) + if len(line) > 0 { + b = b[len(line):] + if line[len(line)-1] != '\n' { + line = append(line, '\n') + } + lic = append(line, lic...) + } + b = append(lic, b...) + return true, ioutil.WriteFile(path, b, fmode) +} + +// fileHasLicense reports whether the file at path contains a license header. +func fileHasLicense(path string) (bool, error) { + b, err := ioutil.ReadFile(path) + if err != nil || hasLicense(b) { + return false, err + } + return true, nil +} + +func licenseHeader(path string, tmpl *template.Template, data *copyrightData) ([]byte, error) { var lic []byte var err error switch fileExtension(path) { default: - return false, nil + return nil, nil case ".c", ".h": lic, err = prefix(tmpl, data, "/*", " * ", " */") case ".js", ".jsx", ".tsx", ".css", ".tf", ".ts": @@ -164,25 +226,7 @@ func addLicense(path string, fmode os.FileMode, tmpl *template.Template, data *c case ".ml", ".mli", ".mll", ".mly": lic, err = prefix(tmpl, data, "(**", " ", "*)") } - if err != nil || lic == nil { - return false, err - } - - b, err := ioutil.ReadFile(path) - if err != nil || hasLicense(b) { - return false, err - } - - line := hashBang(b) - if len(line) > 0 { - b = b[len(line):] - if line[len(line)-1] != '\n' { - line = append(line, '\n') - } - lic = append(line, lic...) - } - b = append(lic, b...) - return true, ioutil.WriteFile(path, b, fmode) + return lic, err } func fileExtension(name string) string { diff --git a/main_test.go b/main_test.go index 334d170..8fef074 100644 --- a/main_test.go +++ b/main_test.go @@ -87,3 +87,100 @@ func TestMultiyear(t *testing.T) { } run(t, "diff", samplefile, sampleLicensed) } + +func TestWriteErrors(t *testing.T) { + if os.Getenv("RUNME") != "" { + main() + return + } + + tmp := tempDir(t) + t.Logf("tmp dir: %s", tmp) + samplefile := filepath.Join(tmp, "file.c") + + run(t, "cp", "testdata/initial/file.c", samplefile) + run(t, "chmod", "0444", samplefile) + cmd := exec.Command(os.Args[0], + "-test.run=TestWriteErrors", + "-l", "apache", "-c", "Google LLC", "-y", "2018", + samplefile, + ) + cmd.Env = []string{"RUNME=1"} + out, err := cmd.CombinedOutput() + if err == nil { + run(t, "chmod", "0644", samplefile) + t.Fatalf("TestWriteErrors exited with a zero exit code.\n%s", out) + } + run(t, "chmod", "0644", samplefile) +} + +func TestReadErrors(t *testing.T) { + if os.Getenv("RUNME") != "" { + main() + return + } + + tmp := tempDir(t) + t.Logf("tmp dir: %s", tmp) + samplefile := filepath.Join(tmp, "file.c") + + run(t, "cp", "testdata/initial/file.c", samplefile) + run(t, "chmod", "a-r", samplefile) + cmd := exec.Command(os.Args[0], + "-test.run=TestReadErrors", + "-l", "apache", "-c", "Google LLC", "-y", "2018", + samplefile, + ) + cmd.Env = []string{"RUNME=1"} + out, err := cmd.CombinedOutput() + if err == nil { + run(t, "chmod", "0644", samplefile) + t.Fatalf("TestWriteErrors exited with a zero exit code.\n%s", out) + } + run(t, "chmod", "0644", samplefile) +} + +func TestCheckSuccess(t *testing.T) { + if os.Getenv("RUNME") != "" { + main() + return + } + + tmp := tempDir(t) + t.Logf("tmp dir: %s", tmp) + samplefile := filepath.Join(tmp, "file.c") + + run(t, "cp", "testdata/expected/file.c", samplefile) + cmd := exec.Command(os.Args[0], + "-test.run=TestCheckSuccess", + "-l", "apache", "-c", "Google LLC", "-y", "2018", + "-check", samplefile, + ) + cmd.Env = []string{"RUNME=1"} + if out, err := cmd.CombinedOutput(); err != nil { + t.Fatalf("%v\n%s", err, out) + } +} + +func TestCheckFail(t *testing.T) { + if os.Getenv("RUNME") != "" { + main() + return + } + + tmp := tempDir(t) + t.Logf("tmp dir: %s", tmp) + samplefile := filepath.Join(tmp, "file.c") + + run(t, "cp", "testdata/initial/file.c", samplefile) + cmd := exec.Command(os.Args[0], + "-test.run=TestCheckFail", + "-l", "apache", "-c", "Google LLC", "-y", "2018", + "-check", samplefile, + ) + cmd.Env = []string{"RUNME=1"} + out, err := cmd.CombinedOutput() + if err == nil { + t.Fatalf("TestCheckFail exited with a zero exit code.\n%s", out) + } +}