diff --git a/file.go b/file.go index 28f600b..8fdd8a4 100644 --- a/file.go +++ b/file.go @@ -353,7 +353,7 @@ func (f *FileWalker) walkDirectoryRecursive(iteration int, return err } - gitIgnore := gitignore.New(bytes.NewReader(c), abs, nil) + gitIgnore := gitignore.New(bytes.NewReader(c), filepath.ToSlash(abs), nil) gitignores = append(gitignores, gitIgnore) } } @@ -435,6 +435,22 @@ func (f *FileWalker) walkDirectoryRecursive(iteration int, } } } + if !f.IgnoreGitIgnore { + gitdir := os.Getenv("GIT_DIR") + if gitdir == "" { + gitdir = filepath.Join(directory, ".git") + } + file := filepath.Join(gitdir, "info", "exclude") + if content, err := os.ReadFile(file); err == nil { + abs, err := filepath.Abs(directory) + if err == nil { + gitExclude := gitignore.New(bytes.NewReader(content), abs, nil) + if gitExclude != nil { + gitignores = append(gitignores, gitExclude) + } + } + } + } // If we have custom ignore patterns defined we should concatenate them and treat them as a single gitignore file if len(f.CustomIgnorePatterns) > 0 { @@ -456,7 +472,7 @@ func (f *FileWalker) walkDirectoryRecursive(iteration int, for _, file := range files { shouldIgnore := false var skipReason SkipReason - joined := filepath.Join(directory, file.Name()) + joined := filepath.ToSlash(filepath.Join(directory, file.Name())) for _, ignore := range gitignores { // we have the following situations @@ -634,7 +650,7 @@ func (f *FileWalker) walkDirectoryRecursive(iteration int, for _, dir := range dirs { var shouldIgnore bool var skipReason SkipReason - joined := filepath.Join(directory, dir.Name()) + joined := filepath.ToSlash(filepath.Join(directory, dir.Name())) // Check against the ignore files we have if the file we are looking at // should be ignored diff --git a/file_test.go b/file_test.go index a384d0b..23c9f32 100644 --- a/file_test.go +++ b/file_test.go @@ -1500,3 +1500,134 @@ func TestSkipHandlerNilIsIgnored(t *testing.T) { t.Error("Expected 0 files") } } + +func TestCRLFGitignore(t *testing.T) { + dir := t.TempDir() + + content := "vendor/\r\n*.log\r\nbuild/\r\n" + if err := os.WriteFile(filepath.Join(dir, ".gitignore"), []byte(content), 0644); err != nil { + t.Fatal(err) + } + + os.MkdirAll(filepath.Join(dir, "vendor", "pkg"), 0755) + os.WriteFile(filepath.Join(dir, "vendor", "pkg", "lib.go"), []byte("package p"), 0644) + os.WriteFile(filepath.Join(dir, "debug.log"), []byte("log"), 0644) + os.WriteFile(filepath.Join(dir, "main.go"), []byte("package main"), 0644) + + queue := make(chan *File, 100) + walker := NewFileWalker(dir, queue) + go walker.Start() + + var found []string + for f := range queue { + rel, _ := filepath.Rel(dir, f.Location) + found = append(found, filepath.ToSlash(rel)) + } + + foundMain := false + for _, p := range found { + if p == "main.go" { + foundMain = true + } + if strings.HasPrefix(p, "vendor/") { + t.Errorf("vendor/ should be gitignored but got: %s", p) + } + if strings.HasSuffix(p, ".log") { + t.Errorf("*.log should be gitignored but got: %s", p) + } + } + if !foundMain { + t.Error("expected main.go to be found but it was not") + } +} + +func TestWindowsPathNormalization(t *testing.T) { + dir := t.TempDir() + + os.WriteFile(filepath.Join(dir, ".gitignore"), []byte("build/\n"), 0644) + os.MkdirAll(filepath.Join(dir, "build"), 0755) + os.WriteFile(filepath.Join(dir, "build", "out.bin"), []byte("bin"), 0644) + os.WriteFile(filepath.Join(dir, "main.go"), []byte("package main"), 0644) + + queue := make(chan *File, 100) + walker := NewFileWalker(dir, queue) + go walker.Start() + + var found []string + for f := range queue { + rel, _ := filepath.Rel(dir, f.Location) + found = append(found, filepath.ToSlash(rel)) + } + + foundMain := false + for _, p := range found { + if p == "main.go" { + foundMain = true + } + if strings.HasPrefix(p, "build/") { + t.Errorf("build/ should be gitignored but got: %s", p) + } + } + if !foundMain { + t.Error("expected main.go to be found but it was not") + } +} +func TestGitInfoExclude(t *testing.T) { + testDir, _ := os.MkdirTemp(os.TempDir(), randSeq(10)) + _ = os.MkdirAll(filepath.Join(testDir, ".git", "info"), 0755) + _ = os.WriteFile(filepath.Join(testDir, ".git", "info", "exclude"), []byte("secret.txt\n"), 0644) + _, _ = os.Create(filepath.Join(testDir, "secret.txt")) + _, _ = os.Create(filepath.Join(testDir, "visible.txt")) + + fileListQueue := make(chan *File, 10) + walker := NewFileWalker(testDir, fileListQueue) + walker.IgnoreGitIgnore = false + _ = walker.Start() + + count := 0 + for range fileListQueue { + count++ + } + + if count != 1 { + t.Errorf("expected 1 file but got %d", count) + } +} +func TestGitInfoExcludeNoGitDir(t *testing.T) { + testDir, _ := os.MkdirTemp(os.TempDir(), randSeq(10)) + _, _ = os.Create(filepath.Join(testDir, "visible.txt")) + + fileListQueue := make(chan *File, 10) + walker := NewFileWalker(testDir, fileListQueue) + walker.IgnoreGitIgnore = false + _ = walker.Start() + + count := 0 + for range fileListQueue { + count++ + } + + if count != 1 { + t.Errorf("expected 1 file but got %d", count) + } +} +func TestGitInfoExcludeIgnoredWhenGitIgnoreDisabled(t *testing.T) { + testDir, _ := os.MkdirTemp(os.TempDir(), randSeq(10)) + _ = os.MkdirAll(filepath.Join(testDir, ".git", "info"), 0755) + _ = os.WriteFile(filepath.Join(testDir, ".git", "info", "exclude"), []byte("secret.txt\n"), 0644) + _, _ = os.Create(filepath.Join(testDir, "secret.txt")) + + fileListQueue := make(chan *File, 10) + walker := NewFileWalker(testDir, fileListQueue) + walker.IgnoreGitIgnore = true + _ = walker.Start() + + count := 0 + for range fileListQueue { + count++ + } + + if count != 1 { + t.Errorf("expected 1 file but got %d", count) + } +} diff --git a/go-gitignore/gitignore.go b/go-gitignore/gitignore.go index e0b5df2..9e59536 100644 --- a/go-gitignore/gitignore.go +++ b/go-gitignore/gitignore.go @@ -249,6 +249,7 @@ func (i *ignore) Match(path string) Match { func (i *ignore) MatchIsDir(path string, _isdir bool) Match { // ensure we have the absolute path for the given file + path = filepath.ToSlash(path) // normalize before cache lookup if v, ok := matchIsDirCache.Load(path); ok { return i.Absolute(v.(string), _isdir) } @@ -258,6 +259,7 @@ func (i *ignore) MatchIsDir(path string, _isdir bool) Match { i._errors(NewError(_err, Position{})) return nil } + _path = filepath.ToSlash(_path) // ensure stored value is slash-form matchIsDirCache.Store(path, _path) // attempt to match the absolute path diff --git a/go-gitignore/lexer.go b/go-gitignore/lexer.go index db3b041..3714367 100644 --- a/go-gitignore/lexer.go +++ b/go-gitignore/lexer.go @@ -319,7 +319,6 @@ func (l *lexer) eol() ([]rune, Error) { // carriage return - we expect to see a newline next case _CR: - _line = append(_line, _next) _next, _err = l.read() if _err != nil { return _line, _err diff --git a/go-gitignore/repository.go b/go-gitignore/repository.go index 1a69cdf..5e91e6a 100644 --- a/go-gitignore/repository.go +++ b/go-gitignore/repository.go @@ -238,7 +238,8 @@ func (r *repository) Relative(path string, isdir bool) Match { // move up the path hierarchy var _last string for { - _file := filepath.Join(r._base, _parent, r._file) + _file := r._base + string(os.PathSeparator) + + filepath.FromSlash(_parent) + string(os.PathSeparator) + r._file _ignore := NewWithCache(_file, r._cache, r._errors) if _ignore != nil { _match := _ignore.Relative(_local, isdir) diff --git a/go.sum b/go.sum index dde6c7d..726e4cc 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,12 @@ github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 h1:y5HC9v93H5EPKqaS1UYVg1uYah5Xf51mBfIoWehClUQ= github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964/go.mod h1:Xd9hchkHSWYkEqJwUGisez3G1QY8Ryz0sdWrLPMGjLk= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=