Skip to content

Commit c744b15

Browse files
committed
feat: Do not filter tracked files even if they match gitignore ...
1 parent ffc0aeb commit c744b15

File tree

2 files changed

+182
-0
lines changed

2 files changed

+182
-0
lines changed

pkg/utils/file_filter.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"strings"
1111
"time"
1212

13+
"github.com/go-git/go-git/v5"
1314
"github.com/rs/zerolog"
1415
gitignore "github.com/sabhiram/go-gitignore"
1516
"golang.org/x/sync/semaphore"
@@ -112,6 +113,10 @@ func (fw *FileFilter) GetFilteredFiles(filesCh chan string, globs []string) chan
112113

113114
// create pattern matcher used to match filesToFilter to glob patterns
114115
globPatternMatcher := gitignore.CompileIgnoreLines(globs...)
116+
117+
// get git-tracked files to avoid filtering them out
118+
gitTrackedFiles := fw.getGitTrackedFiles()
119+
115120
go func() {
116121
ctx := context.Background()
117122
availableThreads := semaphore.NewWeighted(fw.max_threads)
@@ -126,6 +131,16 @@ func (fw *FileFilter) GetFilteredFiles(filesCh chan string, globs []string) chan
126131
}
127132
go func(f string) {
128133
defer availableThreads.Release(1)
134+
// normalize path to absolute for consistent comparison
135+
absPath, err := filepath.Abs(f)
136+
if err != nil {
137+
absPath = filepath.Clean(f)
138+
}
139+
// files tracked in git should not be filtered, even if they match gitignore patterns
140+
if gitTrackedFiles[absPath] {
141+
filteredFilesCh <- f
142+
return
143+
}
129144
// filesToFilter that do not match the glob pattern are filtered
130145
if !globPatternMatcher.MatchesPath(f) {
131146
filteredFilesCh <- f
@@ -143,6 +158,49 @@ func (fw *FileFilter) GetFilteredFiles(filesCh chan string, globs []string) chan
143158
return filteredFilesCh
144159
}
145160

161+
// getGitTrackedFiles returns a map of absolute file paths that are tracked in git (in the index/staging area)
162+
func (fw *FileFilter) getGitTrackedFiles() map[string]bool {
163+
trackedFiles := make(map[string]bool)
164+
165+
// open the git repository
166+
repo, err := git.PlainOpenWithOptions(fw.path, &git.PlainOpenOptions{
167+
DetectDotGit: true,
168+
})
169+
if err != nil {
170+
fw.logger.Debug().Msgf("failed to open git repository: %v", err)
171+
return trackedFiles
172+
}
173+
174+
// get the worktree to find the root path and access the index
175+
worktree, err := repo.Worktree()
176+
if err != nil {
177+
fw.logger.Debug().Msgf("failed to get worktree: %v", err)
178+
return trackedFiles
179+
}
180+
repoRoot := worktree.Filesystem.Root()
181+
182+
// get the index (staging area) - this contains all tracked files
183+
// A file is tracked in git once it's added to the index, even before commit
184+
idx, err := repo.Storer.Index()
185+
if err != nil {
186+
fw.logger.Debug().Msgf("failed to get git index: %v", err)
187+
return trackedFiles
188+
}
189+
190+
// iterate through all entries in the index
191+
for _, entry := range idx.Entries {
192+
absolutePath := filepath.Join(repoRoot, entry.Name)
193+
// ensure the path is absolute and cleaned for consistent comparison
194+
absolutePath, err = filepath.Abs(absolutePath)
195+
if err != nil {
196+
absolutePath = filepath.Clean(filepath.Join(repoRoot, entry.Name))
197+
}
198+
trackedFiles[absolutePath] = true
199+
}
200+
201+
return trackedFiles
202+
}
203+
146204
// buildGlobs iterates a list of ignore filesToFilter and returns a list of glob patterns that can be used to test for ignored filesToFilter
147205
func (fw *FileFilter) buildGlobs(ignoreFiles []string) ([]string, error) {
148206
var globs = make([]string, 0)

pkg/utils/file_filter_test.go

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ import (
77
"runtime"
88
"strings"
99
"testing"
10+
"time"
1011

12+
"github.com/go-git/go-git/v5"
13+
"github.com/go-git/go-git/v5/plumbing/object"
1114
"github.com/rs/zerolog/log"
1215
"github.com/stretchr/testify/assert"
1316
)
@@ -707,3 +710,124 @@ func TestDotSnykExclude_isExpired(t *testing.T) {
707710
})
708711
}
709712
}
713+
714+
func TestFileFilter_GitTrackedFilesNotFiltered(t *testing.T) {
715+
t.Run("committed files are not filtered even if they match gitignore", func(t *testing.T) {
716+
tempDir := t.TempDir()
717+
718+
// Initialize a git repository
719+
repo, err := git.PlainInit(tempDir, false)
720+
assert.NoError(t, err)
721+
722+
// Create a file that will be tracked and matches gitignore pattern
723+
trackedIgnoredFile := filepath.Join(tempDir, "ignored.log")
724+
createFileInPath(t, trackedIgnoredFile, []byte("tracked but ignored"))
725+
726+
// Create another file that will NOT be tracked and matches gitignore pattern
727+
untrackedIgnoredFile := filepath.Join(tempDir, "untracked.log")
728+
createFileInPath(t, untrackedIgnoredFile, []byte("untracked and ignored"))
729+
730+
// Create a regular file that doesn't match gitignore
731+
regularFile := filepath.Join(tempDir, "regular.txt")
732+
createFileInPath(t, regularFile, []byte("regular file"))
733+
734+
// Add and commit the tracked file BEFORE creating gitignore
735+
worktree, err := repo.Worktree()
736+
assert.NoError(t, err)
737+
738+
_, err = worktree.Add("ignored.log")
739+
assert.NoError(t, err)
740+
_, err = worktree.Add("regular.txt")
741+
assert.NoError(t, err)
742+
743+
_, err = worktree.Commit("initial commit", &git.CommitOptions{
744+
Author: &object.Signature{
745+
Name: "Test",
746+
Email: "test@test.com",
747+
When: time.Now(),
748+
},
749+
})
750+
assert.NoError(t, err)
751+
752+
// Now create .gitignore that ignores *.log files
753+
gitignorePath := filepath.Join(tempDir, ".gitignore")
754+
createFileInPath(t, gitignorePath, []byte("*.log\n"))
755+
756+
// Create file filter and get filtered files
757+
fileFilter := NewFileFilter(tempDir, &log.Logger)
758+
rules, err := fileFilter.GetRules([]string{".gitignore"})
759+
assert.NoError(t, err)
760+
761+
allFiles := fileFilter.GetAllFiles()
762+
filteredFiles := fileFilter.GetFilteredFiles(allFiles, rules)
763+
764+
// Collect filtered files
765+
var filteredFilesList []string
766+
for file := range filteredFiles {
767+
filteredFilesList = append(filteredFilesList, file)
768+
}
769+
770+
// The tracked file (ignored.log) should NOT be filtered out
771+
assert.Contains(t, filteredFilesList, trackedIgnoredFile, "git tracked file should not be filtered even if it matches gitignore")
772+
773+
// The untracked file (untracked.log) SHOULD be filtered out
774+
assert.NotContains(t, filteredFilesList, untrackedIgnoredFile, "untracked file matching gitignore should be filtered")
775+
776+
// The regular file should be present
777+
assert.Contains(t, filteredFilesList, regularFile, "regular file should not be filtered")
778+
779+
// The gitignore file should be present
780+
assert.Contains(t, filteredFilesList, gitignorePath, ".gitignore should not be filtered")
781+
})
782+
783+
t.Run("staged but uncommitted files are not filtered even if they match gitignore", func(t *testing.T) {
784+
tempDir := t.TempDir()
785+
786+
// Initialize a git repository
787+
repo, err := git.PlainInit(tempDir, false)
788+
assert.NoError(t, err)
789+
790+
// Create .gitignore first
791+
gitignorePath := filepath.Join(tempDir, ".gitignore")
792+
createFileInPath(t, gitignorePath, []byte("*.log\n"))
793+
794+
// Create a file that matches gitignore pattern but will be staged (git add)
795+
stagedIgnoredFile := filepath.Join(tempDir, "staged.log")
796+
createFileInPath(t, stagedIgnoredFile, []byte("staged but ignored"))
797+
798+
// Create another file that will NOT be staged and matches gitignore pattern
799+
unstagedIgnoredFile := filepath.Join(tempDir, "unstaged.log")
800+
createFileInPath(t, unstagedIgnoredFile, []byte("unstaged and ignored"))
801+
802+
// Stage the file (git add) but do NOT commit
803+
worktree, err := repo.Worktree()
804+
assert.NoError(t, err)
805+
806+
_, err = worktree.Add("staged.log")
807+
assert.NoError(t, err)
808+
// Note: we do NOT commit here - the file is only staged
809+
810+
// Create file filter and get filtered files
811+
fileFilter := NewFileFilter(tempDir, &log.Logger)
812+
rules, err := fileFilter.GetRules([]string{".gitignore"})
813+
assert.NoError(t, err)
814+
815+
allFiles := fileFilter.GetAllFiles()
816+
filteredFiles := fileFilter.GetFilteredFiles(allFiles, rules)
817+
818+
// Collect filtered files
819+
var filteredFilesList []string
820+
for file := range filteredFiles {
821+
filteredFilesList = append(filteredFilesList, file)
822+
}
823+
824+
// The staged file (staged.log) should NOT be filtered out - it's in the index
825+
assert.Contains(t, filteredFilesList, stagedIgnoredFile, "staged file should not be filtered even if it matches gitignore")
826+
827+
// The unstaged file (unstaged.log) SHOULD be filtered out
828+
assert.NotContains(t, filteredFilesList, unstagedIgnoredFile, "unstaged file matching gitignore should be filtered")
829+
830+
// The gitignore file should be present
831+
assert.Contains(t, filteredFilesList, gitignorePath, ".gitignore should not be filtered")
832+
})
833+
}

0 commit comments

Comments
 (0)