diff --git a/fixtures/excludedpath/file.testdata b/fixtures/excludedpath/file.testdata new file mode 100644 index 0000000..2168920 --- /dev/null +++ b/fixtures/excludedpath/file.testdata @@ -0,0 +1,2 @@ +// Package excludedpath holds test data for the licensing cmd +package excludedpath diff --git a/golden/excludedpath/file.go b/golden/excludedpath/file.go new file mode 100644 index 0000000..30e3706 --- /dev/null +++ b/golden/excludedpath/file.go @@ -0,0 +1,19 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +// Package excludedpath holds test data for the licensing cmd +package excludedpath diff --git a/main.go b/main.go index 86e4b98..a9de7a3 100644 --- a/main.go +++ b/main.go @@ -23,6 +23,7 @@ import ( "io" "os" "path/filepath" + "strings" "github.com/elastic/go-licenser/licensing" ) @@ -78,10 +79,27 @@ var ( extension string args []string headerBytes []byte + exclude sliceFlag defaultExludedDirs = []string{"vendor", ".git"} ) +type sliceFlag []string + +func (f *sliceFlag) String() string { + var s string + for _, i := range *f { + s += i + " " + } + return s +} + +func (f *sliceFlag) Set(value string) error { + *f = append(*f, value) + return nil +} + func init() { + flag.Var(&exclude, "exclude", `path to exclude (can be specified multiple times).`) flag.BoolVar(&dryRun, "d", false, `skips rewriting files and returns exitcode 1 if any discrepancies are found.`) flag.StringVar(&extension, "ext", defaultExt, "sets the file extension to scan for.") flag.Usage = usageFlag @@ -94,15 +112,15 @@ func init() { } func main() { - err := run(args, defaultExludedDirs, extension, dryRun, os.Stdout) + err := run(args, exclude, extension, dryRun, os.Stdout) if err != nil && err.Error() != "" { - fmt.Println(err) + fmt.Fprint(os.Stderr, err) } os.Exit(Code(err)) } -func run(args, exclDirs []string, ext string, dry bool, out io.Writer) error { +func run(args, exclude []string, ext string, dry bool, out io.Writer) error { var path = defaultPath if len(args) > 0 { path = args[0] @@ -112,7 +130,7 @@ func run(args, exclDirs []string, ext string, dry bool, out io.Writer) error { return &Error{err: err, code: exitFailedToStatTree} } - return walk(path, ext, defaultExludedDirs, dry, out) + return walk(path, ext, exclude, dry, out) } func reportFile(out io.Writer, f string) { @@ -132,7 +150,13 @@ func walk(p, ext string, exclude []string, dry bool, out io.Writer) error { return walkErr } - if info.IsDir() && stringInSlice(info.Name(), exclude) { + var currentPath = cleanPathPrefixes( + strings.Replace(path, p, "", 1), + []string{"/"}, + ) + + var excludedDir = info.IsDir() && stringInSlice(info.Name(), defaultExludedDirs) + if needsExclusion(currentPath, exclude) || excludedDir { return filepath.SkipDir } diff --git a/main_test.go b/main_test.go index 6b04a08..69f064f 100644 --- a/main_test.go +++ b/main_test.go @@ -105,10 +105,10 @@ func dcopy(src, dest string, info os.FileInfo) error { func Test_run(t *testing.T) { type args struct { - args []string - exclDirs []string - ext string - dry bool + args []string + exclude []string + ext string + dry bool } tests := []struct { name string @@ -121,10 +121,10 @@ func Test_run(t *testing.T) { { name: "Run a diff prints a list of files that need the license header", args: args{ - args: []string{"testdata"}, - exclDirs: defaultExludedDirs, - ext: defaultExt, - dry: true, + args: []string{"testdata"}, + exclude: []string{"excludedpath"}, + ext: defaultExt, + dry: true, }, want: 1, err: &Error{code: 1}, @@ -142,10 +142,9 @@ testdata/singlelevel/wrapper.go: is missing the license header { name: "Run against an unexisting dir fails", args: args{ - args: []string{"ignore"}, - exclDirs: defaultExludedDirs, - ext: defaultExt, - dry: false, + args: []string{"ignore"}, + ext: defaultExt, + dry: false, }, want: 2, err: goosPathError(2, "ignore"), @@ -153,10 +152,10 @@ testdata/singlelevel/wrapper.go: is missing the license header { name: "Run with default mode rewrites the source files", args: args{ - args: []string{"testdata"}, - exclDirs: defaultExludedDirs, - ext: defaultExt, - dry: false, + args: []string{"testdata"}, + exclude: []string{"excludedpath"}, + ext: defaultExt, + dry: false, }, want: 0, wantGolden: true, @@ -169,7 +168,7 @@ testdata/singlelevel/wrapper.go: is missing the license header } var buf = new(bytes.Buffer) - var err = run(tt.args.args, tt.args.exclDirs, tt.args.ext, tt.args.dry, buf) + var err = run(tt.args.args, tt.args.exclude, tt.args.ext, tt.args.dry, buf) if !reflect.DeepEqual(err, tt.err) { t.Errorf("run() error = %v, wantErr %v", err, tt.err) return @@ -189,7 +188,7 @@ testdata/singlelevel/wrapper.go: is missing the license header if tt.wantGolden { if *update { copyFixtures(t, "golden") - if err := run([]string{"golden"}, tt.args.exclDirs, tt.args.ext, tt.args.dry, buf); err != nil { + if err := run([]string{"golden"}, tt.args.exclude, tt.args.ext, tt.args.dry, buf); err != nil { t.Fatal(err) } } diff --git a/path.go b/path.go new file mode 100644 index 0000000..59183bb --- /dev/null +++ b/path.go @@ -0,0 +1,51 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package main + +import "strings" + +func needsExclusion(path string, exclude []string) bool { + for _, excluded := range exclude { + excluded = cleanPathSuffixes(excluded, []string{"*", "/"}) + if strings.HasPrefix(path, excluded) { + return true + } + } + + return false +} + +func cleanPathSuffixes(path string, sufixes []string) string { + for _, suffix := range sufixes { + for strings.HasSuffix(path, suffix) && len(path) > 0 { + path = path[:len(path)-len(suffix)] + } + } + + return path +} + +func cleanPathPrefixes(path string, prefixes []string) string { + for _, prefix := range prefixes { + for strings.HasPrefix(path, prefix) && len(path) > 0 { + path = path[len(prefix):] + } + } + + return path +} diff --git a/path_test.go b/path_test.go new file mode 100644 index 0000000..e46139d --- /dev/null +++ b/path_test.go @@ -0,0 +1,223 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package main + +import ( + "testing" +) + +func Test_needsExclusion(t *testing.T) { + type args struct { + path string + exclude []string + } + tests := []struct { + name string + args args + want bool + }{ + { + name: "Path is excluded", + args: args{ + path: "apath/thatdoesNeed/exclusion", + exclude: []string{"apath"}, + }, + want: true, + }, + { + name: "Path is not excluded", + args: args{ + path: "apath/thatdoesNOTNeed/exclusion", + exclude: []string{"anotherpath"}, + }, + want: false, + }, + { + name: "Path is excluded", + args: args{ + path: "apath/thatdoesNeed/exclusion", + exclude: []string{"apath/thatdoesNeed"}, + }, + want: true, + }, + { + name: "Path is excluded", + args: args{ + path: "apath/thatdoesNeed/exclusion", + exclude: []string{"apath/thatdoesNeed/"}, + }, + want: true, + }, + { + name: "Path is excluded", + args: args{ + path: "apath/thatdoesNeed/exclusion", + exclude: []string{"apath/thatdoesNeed/*"}, + }, + want: true, + }, + { + name: "Path is excluded", + args: args{ + path: "apath/thatdoesNeed/exclusion", + exclude: []string{"apath/thatdoesNeed/exclusion"}, + }, + want: true, + }, + { + name: "Path is excluded", + args: args{ + path: "apath/thatdoesNeed/exclusion", + exclude: []string{"apath/thatdoesNeed/exclusion/*"}, + }, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := needsExclusion(tt.args.path, tt.args.exclude); got != tt.want { + t.Errorf("needsExclusion() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_cleanPathSuffixes(t *testing.T) { + type args struct { + path string + sufixes []string + } + tests := []struct { + name string + args args + want string + }{ + { + name: "Cleans the suffixes", + args: args{ + path: "apath/needsTheCLEANS/*", + sufixes: []string{"*", "/"}, + }, + want: "apath/needsTheCLEANS", + }, + { + name: "Cleans multiple suffixes multiple times", + args: args{ + path: "apath/needsTheCLEANS////***", + sufixes: []string{"*", "/"}, + }, + want: "apath/needsTheCLEANS", + }, + { + name: "Cleans the suffixes", + args: args{ + path: "apath/needsTheCLEANS/", + sufixes: []string{"/"}, + }, + want: "apath/needsTheCLEANS", + }, + { + name: "Cleans a single suffix multiple times", + args: args{ + path: "apath/needsTheCLEANS/////", + sufixes: []string{"/"}, + }, + want: "apath/needsTheCLEANS", + }, + { + name: "Cleans no suffixes if none are passed", + args: args{ + path: "apath/needsTheCLEANS/", + }, + want: "apath/needsTheCLEANS/", + }, + { + name: "empty string case", + args: args{ + path: "", + }, + want: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := cleanPathSuffixes(tt.args.path, tt.args.sufixes); got != tt.want { + t.Errorf("cleanPathSuffixes() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_cleanPathPrefixes(t *testing.T) { + type args struct { + path string + prefixes []string + } + tests := []struct { + name string + args args + want string + }{ + { + name: "Cleans path prefixes", + args: args{ + path: "/prefix", + prefixes: []string{"/"}, + }, + want: "prefix", + }, + { + name: "Cleans no path prefixes", + args: args{ + path: "prefix", + prefixes: []string{"/"}, + }, + want: "prefix", + }, + { + name: "Cleans path prefixes", + args: args{ + path: "xyzprefix", + prefixes: []string{"xyz"}, + }, + want: "prefix", + }, + { + name: "Cleans no prefixes (Empty prefixes)", + args: args{ + path: "something", + }, + want: "something", + }, + { + name: "Cleans no prefixes (Empty path)", + args: args{ + path: "", + prefixes: []string{"zyx"}, + }, + want: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := cleanPathPrefixes(tt.args.path, tt.args.prefixes); got != tt.want { + t.Errorf("cleanPathPrefixes() = %v, want %v", got, tt.want) + } + }) + } +}