fixes to file.go in codewalker for issues #133 and #404 in SCC repository, - #22
Conversation
when gocodewalker, walks through directories the .gitignore files were respected but .git/info/exclude was not now when walking a directory if .git/info/exists it's read, and ignored in the scc count Fixes boyter/scc#133
…c#404) filepath.Join produces backslashes on Windows, causing gitignore rules to silently fail. Apply filepath.ToSlash to all paths before passing them to MatchIsDir and gitignore.New. Adds regression tests for CRLF .gitignore files and path normalization.
|
Oh neat. I have been working on this as well, but you having done it already is a great benefit. Looking though it now. |
There was a problem hiding this comment.
Pull request overview
This PR aims to improve ignore handling consistency across platforms and add support for Git’s global exclude file, addressing SCC issues #133 and #404.
Changes:
- Normalize paths to forward-slash form when matching ignores to reduce Windows/Linux discrepancies.
- Add support for reading
$GIT_DIR/info/exclude(or.git/info/exclude) and applying it during walking. - Add tests for CRLF
.gitignoreparsing and for path normalization behavior.
Reviewed changes
Copilot reviewed 5 out of 6 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
file.go |
Normalizes walked paths before ignore matching; adds loading of .git/info/exclude. |
file_test.go |
Adds tests for CRLF .gitignore and “Windows path normalization”. |
go-gitignore/gitignore.go |
Normalizes inputs for MatchIsDir cache/matching. |
go-gitignore/lexer.go |
Changes CRLF end-of-line tokenization behavior. |
go-gitignore/repository.go |
Adjusts how per-directory ignore file paths are constructed. |
go.sum |
Adds new module sums (likely from updated/added test dependencies). |
Comments suppressed due to low confidence (2)
go-gitignore/gitignore.go:266
MatchIsDirnow normalizes both the cache key and the computed absolute path to slash form, butignore._baseis not normalized anywhere (e.g.NewWithErrorssets it viafilepath.Dirusing OS separators). On Windows this will breakAbsolute()'sstrings.HasPrefix(path, i._base)check becausepathbecomesC:/...while_baseisC:\.... Consider normalizing_baseto slash form at construction time (inNew,NewWithErrors, and repository constructors), or alternatively avoid converting_pathto slash form here and normalize only the relative portion used for pattern matching.
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)
}
_path, _err := filepath.Abs(path)
if _err != nil {
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
return i.Absolute(_path, _isdir)
go-gitignore/lexer.go:334
eol()consumes both\rand\nfor CRLF, but after this change it only appends\nto the returnedword. Since token positioning useslen(word)to compute the start column/offset, CRLF lines will now produce incorrectPositiondata (off by 1) for EOL tokens and downstream error reporting. Either append both runes ("\r\n") to_lineor adjust the position calculation to account for the extra consumed rune.
// carriage return - we expect to see a newline next
case _CR:
_next, _err = l.read()
if _err != nil {
return _line, _err
} else if _next != _NEWLINE {
l.unread(_next)
return _line, l.err(ErrCarriageReturnError)
}
fallthrough
// newline
case _NEWLINE:
_line = append(_line, _next)
return _line, nil
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| _file := r._base + string(os.PathSeparator) + | ||
| filepath.FromSlash(_parent) + string(os.PathSeparator) + r._file |
There was a problem hiding this comment.
This replaces filepath.Join with manual path concatenation. This is more error-prone on Windows (drive roots like C:\ or UNC paths) and can introduce redundant separators/./ segments that filepath.Join normally normalizes. Unless there’s a specific bug being worked around, prefer filepath.Join(r._base, _parent, r._file) (or at least filepath.Clean the constructed path) to preserve cross-platform path semantics.
| _file := r._base + string(os.PathSeparator) + | |
| filepath.FromSlash(_parent) + string(os.PathSeparator) + r._file | |
| _file := filepath.Join(r._base, _parent, r._file) |
| gitIgnore := gitignore.New(bytes.NewReader(c), filepath.ToSlash(abs), nil) | ||
| gitignores = append(gitignores, gitIgnore) |
There was a problem hiding this comment.
Only the .gitignore ignore instance is created with a slash-normalized base (filepath.ToSlash(abs)), but other ignore instances in this function (.ignore, .gitmodules, CustomIgnore, CustomIgnorePatterns, and the new info/exclude ignore) still use abs directly. With MatchIsDir now normalizing paths to slash form, any ignore with a non-normalized base will fail the HasPrefix check on Windows and effectively stop matching. Normalize the base consistently for all gitignore.New(...) calls (or normalize inside the gitignore library constructors).
| 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) | ||
| } |
There was a problem hiding this comment.
The $GIT_DIR/info/exclude file is being probed on every recursive directory walk, which adds an extra filesystem read per directory and can become expensive on large trees. Since the exclude file is repo-global, consider loading it once (e.g. only at iteration == 0, or only when a .git directory is present) and passing/caching the resulting GitIgnore down to child calls. Also, for consistency with the other ignore loaders, use the injected f.osReadFile/f.errorsHandler, and make the base path slash-normalized (to match the MatchIsDir normalization).
| 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) | |
| } | |
| shouldLoadGitExclude := gitdir != "" | |
| if gitdir == "" { | |
| gitdir = filepath.Join(directory, ".git") | |
| if info, err := os.Stat(gitdir); err == nil && info.IsDir() { | |
| shouldLoadGitExclude = true | |
| } | |
| } | |
| if shouldLoadGitExclude { | |
| file := filepath.Join(gitdir, "info", "exclude") | |
| if content, err := f.osReadFile(file); err == nil { | |
| abs, err := filepath.Abs(directory) | |
| if err != nil { | |
| if !f.errorsHandler(err) { | |
| return err | |
| } | |
| } else { | |
| abs = filepath.ToSlash(abs) | |
| gitExclude := gitignore.New(bytes.NewReader(content), abs, nil) | |
| if gitExclude != nil { | |
| gitignores = append(gitignores, gitExclude) | |
| } | |
| } | |
| } else if !errors.Is(err, fs.ErrNotExist) { | |
| if !f.errorsHandler(err) { | |
| return err | |
| } |
| 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) | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
New behavior is introduced here to load and apply $GIT_DIR/info/exclude, but there’s no test exercising it. Since file_test.go already has extensive coverage for ignore handling, please add a unit test that creates a temp repo with .git/info/exclude containing patterns and asserts those files are skipped (including a case where IgnoreGitIgnore is true/false, and optionally when GIT_DIR is set).
| 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) |
There was a problem hiding this comment.
TestWindowsPathNormalization is intended to prevent Windows-specific path separator regressions, but the repo’s CI workflow runs tests only on ubuntu-latest, so this test won’t exercise the backslash-vs-slash behavior it’s named for. Consider either adding a Windows job to the GitHub Actions matrix or adjusting the test to explicitly cover mixed-separator inputs in a way that’s meaningful on non-Windows runners.
|
Ill need to review the fuzz tests on this one, because they are failing which is not ideal, but could be a false positive. |
|
I apologize for seeing that our code pull requests has failed tests, I hope its an issue with my merging of two issues of the master branch on my fork, but I will run the tests on my fork, and see where we went wrong. |
…or string, also made the variable name for my temp directory follow snake case
|
Merged. Ill have to upload all my downstream things for it, but thank you for this. |
SCC would only read .gitignore files, but not .git/info/exclude, in file.go we explicitly check for the git/info using GIT_DIR environment variable , and we add it to the .gitignore. This fix was tested originally by creating a test directory, in our SCC fork, and reproducing the git/info/exclude, the user had, where the scc properly ignored the .git/info/exclude, and did not count it.
RichardSimison worked on issues 404, where Linux and windows were outputting different results. The issues that was identified, was Linux and windows have different slashes, so when filepath.join was used it produced backslashes on windows, causing .gitignores to fail. The fix was to apply filepath.ToSlash to all paths before passing
them to MatchIsDir and gitignore.New. Tests were created to check that windows styled line endings are parsed correctly with .gitignore files