diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ca55abcb..5f45a837 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,7 @@ # First: Install pre-commit https://pre-commit.com/#install # Then, run `pre-commit install` repos: - - repo: git://github.com/caitlinelfring/pre-commit-golang + - repo: https://github.com/caitlinelfring/pre-commit-golang rev: v0.4.0 hooks: - id: go-fmt diff --git a/cmd/root.go b/cmd/root.go index e4822d03..d311ebae 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -45,13 +45,14 @@ import ( var ( // flags - exitOneOnFailure bool - cfgFile string - debug bool - stdin bool - outputName string - noIgnore bool - disableDefaultRules bool + exitOneOnFailure bool + cfgFile string + debug bool + stdin bool + outputName string + noIgnore bool + disableDefaultRules bool + disableNestedIgnores bool // Version is populated by goreleaser during build // Version... @@ -109,7 +110,7 @@ func rootRunE(cmd *cobra.Command, args []string) error { if err != nil { return err } - ignorer, err = ignore.NewIgnore(fs, cfg.IgnoreFiles) + ignorer, err = ignore.NewIgnore(fs, cfg.IgnoreFiles, disableNestedIgnores) if err != nil { return err } @@ -155,6 +156,7 @@ func init() { rootCmd.PersistentFlags().BoolVar(&noIgnore, "no-ignore", false, "Ignored files in .gitignore, .ignore, .wokeignore, .git/info/exclude, and inline ignores are processed") rootCmd.PersistentFlags().StringVarP(&outputName, "output", "o", printer.OutFormatText, fmt.Sprintf("Output type [%s]", printer.OutFormatsString)) rootCmd.PersistentFlags().BoolVar(&disableDefaultRules, "disable-default-rules", false, "Disable the default ruleset") + rootCmd.PersistentFlags().BoolVar(&disableNestedIgnores, "disable-nested-ignores", false, "Disable nested woke ignore traversal") } // GetRootCmd returns the rootCmd, which should only be used by the docs generator in cmd/docs/main.go diff --git a/cmd/root_test.go b/cmd/root_test.go index 87071e7e..d3dca57d 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -20,9 +20,12 @@ import ( // run profiling with // go test -v -cpuprofile cpu.prof -memprofile mem.prof -bench=. ./cmd // memory: -// go tool pprof mem.prof +// +// go tool pprof mem.prof +// // cpu: -// go tool pprof cpu.prof +// +// go tool pprof cpu.prof func BenchmarkRootRunE(b *testing.B) { zerolog.SetGlobalLevel(zerolog.NoLevel) output.Stdout = io.Discard diff --git a/docs/ignore.md b/docs/ignore.md index bf5d6503..dca63221 100644 --- a/docs/ignore.md +++ b/docs/ignore.md @@ -79,6 +79,8 @@ func main() { `woke` will apply ignore rules from nested ignore files to any child files/folders, similar to a nested `.gitignore` file. Nested ignore files work for any ignore file type listed above. +>Note: To disable nested ignore file functionality, run `woke` with the `--disable-nested-ignores` flag. + ```txt project │ README.md diff --git a/pkg/ignore/ignore.go b/pkg/ignore/ignore.go index 6be035de..0c6c5c29 100644 --- a/pkg/ignore/ignore.go +++ b/pkg/ignore/ignore.go @@ -1,6 +1,9 @@ package ignore import ( + "bufio" + "errors" + "io" "os" "path/filepath" "strings" @@ -65,7 +68,7 @@ func GetRootGitDir(workingDir string) (filesystem billy.Filesystem, err error) { // NewIgnore produces an Ignore object, with compiled lines from defaultIgnoreFiles // which you can match files against -func NewIgnore(filesystem billy.Filesystem, lines []string) (ignore *Ignore, err error) { +func NewIgnore(filesystem billy.Filesystem, lines []string, disableNestedIgnores bool) (ignore *Ignore, err error) { start := time.Now() defer func() { log.Debug(). @@ -79,8 +82,16 @@ func NewIgnore(filesystem billy.Filesystem, lines []string) (ignore *Ignore, err } var ps []gitignore.Pattern - if ps, err = gitignore.ReadPatterns(filesystem, nil, defaultIgnoreFiles); err != nil { - return + + // if opted-out of nested wokeignore traversal, only use top-level ignore files + if disableNestedIgnores { + for _, filename := range defaultIgnoreFiles { + lines = append(lines, readIgnoreFile(filesystem, filename)...) + } + } else { + if ps, err = gitignore.ReadPatterns(filesystem, nil, defaultIgnoreFiles); err != nil { + return + } } // get domain for git ignore rules supplied from the lines argument @@ -90,11 +101,35 @@ func NewIgnore(filesystem billy.Filesystem, lines []string) (ignore *Ignore, err ps = append(ps, pattern) } - ignore = &Ignore{ + return &Ignore{ matcher: gitignore.NewMatcher(ps), + }, nil +} + +func readIgnoreFile(filesystem billy.Filesystem, file string) []string { + openFile, err := filesystem.Open(file) + + if err != nil { + _event := log.Warn() + if errors.Is(err, os.ErrNotExist) { + _event = log.Debug() + } + _event.Err(err).Str("file", file).Msg("skipping ignorefile") + return []string{} + } + + defer openFile.Close() + var buffer []byte + buffer, err = io.ReadAll(bufio.NewReader(openFile)) + + if err != nil { + _event := log.Warn() + _event.Err(err).Str("file", file).Msg("skipping ignorefile") + return []string{} } - return + log.Debug().Str("file", file).Msg("adding ignorefile") + return strings.Split(strings.TrimSpace(string(buffer)), "\n") } // Match returns true if the provided file matches any of the defined ignores diff --git a/pkg/ignore/ignore_test.go b/pkg/ignore/ignore_test.go index e7be706f..0423c49c 100644 --- a/pkg/ignore/ignore_test.go +++ b/pkg/ignore/ignore_test.go @@ -75,6 +75,16 @@ func (suite *IgnoreTestSuite) SetupTest() { err = f.Close() suite.NoError(err) + err = fs.MkdirAll("nestedIgnoreFolder", os.ModePerm) + suite.NoError(err) + + f, err = fs.Create(fs.Join("nestedIgnoreFolder", ".wokeignore")) + suite.NoError(err) + _, err = f.Write([]byte("*.NESTEDIGNORE\n")) + suite.NoError(err) + err = f.Close() + suite.NoError(err) + suite.GFS = fs } @@ -82,8 +92,8 @@ func BenchmarkIgnore(b *testing.B) { zerolog.SetGlobalLevel(zerolog.NoLevel) fs, clean := TempFileSystem() defer clean() - for i := 0; i < 10; i++ { - for j := 0; j < 10; j++ { + for i := 0; i < 50; i++ { + for j := 0; j < 50; j++ { err := fs.MkdirAll(fs.Join(fmt.Sprintf("%d", i), fmt.Sprintf("%d", j)), os.ModePerm) assert.NoError(b, err) f, err := fs.Create(".wokeignore") @@ -102,11 +112,20 @@ func BenchmarkIgnore(b *testing.B) { } b.ResetTimer() - for i := 0; i < b.N; i++ { - ignorer, err := NewIgnore(fs, []string{}) - assert.NoError(b, err) - ignorer.Match(filepath.Join("not", "foo"), false) - } + b.Run("nested-ignores-enabled", func(b *testing.B) { + for i := 0; i < b.N; i++ { + ignorer, err := NewIgnore(fs, []string{}, false) + assert.NoError(b, err) + ignorer.Match(filepath.Join("not", "foo"), false) + } + }) + b.Run("nested-ignores-disabled", func(b *testing.B) { + for i := 0; i < b.N; i++ { + ignorer, err := NewIgnore(fs, []string{}, true) + assert.NoError(b, err) + ignorer.Match(filepath.Join("not", "foo"), false) + } + }) } func (suite *IgnoreTestSuite) TestGetDomainFromWorkingDir() { @@ -135,8 +154,19 @@ func (suite *IgnoreTestSuite) TestGetRootGitDirNotExist() { suite.NoError(err) suite.Equal(fs.Root(), rootFs.Root()) } -func (suite *IgnoreTestSuite) TestIgnore_Match() { - i, err := NewIgnore(suite.GFS, []string{"my/files/*"}) + +func (suite *IgnoreTestSuite) TestIgnoreLines_Match() { + i, err := NewIgnore(suite.GFS, []string{"my/files/*"}, false) + suite.NoError(err) + suite.NotNil(i) + + suite.False(i.Match(filepath.Join("not", "foo"), false)) + suite.True(i.Match(filepath.Join("my", "files", "file1"), false)) + suite.False(i.Match(filepath.Join("my", "files"), false)) +} + +func (suite *IgnoreTestSuite) TestIgnoreLinesNoTraversal_Match() { + i, err := NewIgnore(suite.GFS, []string{"my/files/*"}, true) suite.NoError(err) suite.NotNil(i) @@ -147,11 +177,31 @@ func (suite *IgnoreTestSuite) TestIgnore_Match() { // Test all default ignore files, except for .git/info/exclude, since // that uses a .git directory that we cannot check in. -func (suite *IgnoreTestSuite) TestIgnoreDefaultIgoreFiles_Match() { - i, err := NewIgnore(suite.GFS, []string{"*.FROMARGUMENT"}) +func (suite *IgnoreTestSuite) TestIgnoreDefaultIgnoreFiles_Match() { + i, err := NewIgnore(suite.GFS, []string{"*.FROMARGUMENT"}, false) + suite.NoError(err) + suite.NotNil(i) + + // Test top-level ignore files all match + suite.testCommonIgnoreDefaultIgnoreFilesMatch(i) + + // Test match from the nested ./nestedIgnoreFolder/.wokeignore when nested ignores is enabled + suite.True(i.Match(filepath.Join("nestedIgnoreFolder", "testdata", "test.NESTEDIGNORE"), false)) +} + +func (suite *IgnoreTestSuite) TestIgnoreDefaultIgnoreFilesNoTraversal_Match() { + i, err := NewIgnore(suite.GFS, []string{"*.FROMARGUMENT"}, true) suite.NoError(err) suite.NotNil(i) + // Test top-level ignore files all match + suite.testCommonIgnoreDefaultIgnoreFilesMatch(i) + + // Test no match from the nested ./nestedIgnoreFolder/.wokeignore when nested ignores is disabled + suite.False(i.Match(filepath.Join("nestedIgnoreFolder", "testdata", "test.NESTEDIGNORE"), false)) +} + +func (suite *IgnoreTestSuite) testCommonIgnoreDefaultIgnoreFilesMatch(i *Ignore) { suite.False(i.Match(filepath.Join("testdata", "notfoo"), false)) suite.True(i.Match(filepath.Join("testdata", "test.FROMARGUMENT"), false)) // From .gitignore suite.True(i.Match(filepath.Join("testdata", "test.DS_Store"), false)) // From .gitignore @@ -162,6 +212,14 @@ func (suite *IgnoreTestSuite) TestIgnoreDefaultIgoreFiles_Match() { suite.False(i.Match(filepath.Join("testdata", "test.NOTIGNORED"), false)) // From .notincluded - making sure only default are included } +func (suite *IgnoreTestSuite) TestReadIgnoreFile() { + ignoreLines := readIgnoreFile(suite.GFS, ".gitignore") + suite.Equal([]string{"*.DS_Store"}, ignoreLines) + + noIgnoreLines := readIgnoreFile(suite.GFS, "missing.gitignore") + suite.Equal([]string{}, noIgnoreLines) +} + // In order for 'go test' to run this suite, we need to create // a normal test function and pass our suite to suite.Run func TestIgnoreTestSuite(t *testing.T) { diff --git a/pkg/parser/parser_test.go b/pkg/parser/parser_test.go index 639395e7..69e1a989 100644 --- a/pkg/parser/parser_test.go +++ b/pkg/parser/parser_test.go @@ -43,7 +43,7 @@ func testParser() (parser *Parser, err error) { return } fs := osfs.New(cwd) - ignorer, err := ignore.NewIgnore(fs, []string{}) + ignorer, err := ignore.NewIgnore(fs, []string{}, false) if err != nil { return } @@ -149,7 +149,7 @@ func parsePathTests(t *testing.T) { cwd, err := os.Getwd() assert.NoError(t, err) fs := osfs.New(cwd) - ignorer, err := ignore.NewIgnore(fs, []string{filepath.ToSlash(f.Name())}) + ignorer, err := ignore.NewIgnore(fs, []string{filepath.ToSlash(f.Name())}, false) assert.NoError(t, err) p.Ignorer = ignorer pr := new(testPrinter) @@ -193,7 +193,7 @@ func parsePathTests(t *testing.T) { cwd, err := os.Getwd() assert.NoError(t, err) fs := osfs.New(cwd) - ignorer, err := ignore.NewIgnore(fs, []string{"*_test.go"}) + ignorer, err := ignore.NewIgnore(fs, []string{"*_test.go"}, false) assert.NoError(t, err) p.Ignorer = ignorer pr := new(testPrinter)