Skip to content

Commit da9c13f

Browse files
fix: handle single slash pattern correctly in gitignore [IDE-1257] (#385)
1 parent 7b13eea commit da9c13f

File tree

2 files changed

+165
-0
lines changed

2 files changed

+165
-0
lines changed

pkg/utils/file_filter.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,12 @@ func parseIgnoreRuleToGlobs(rule string, filePath string) (globs []string) {
220220
rule = rule[1:]
221221
prefix = negation
222222
}
223+
224+
// Special case: "/" pattern has no effect in gitignore
225+
if rule == slash {
226+
return globs
227+
}
228+
223229
startingSlash := strings.HasPrefix(rule, slash)
224230
startingGlobstar := strings.HasPrefix(rule, all)
225231
endingSlash := strings.HasSuffix(rule, slash)

pkg/utils/file_filter_test.go

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,3 +376,162 @@ func createFileInPath(tb testing.TB, filePath string, content []byte) {
376376
err = os.WriteFile(filePath, content, 0777)
377377
assert.NoError(tb, err)
378378
}
379+
380+
func TestParseIgnoreRuleToGlobs(t *testing.T) {
381+
testCases := []struct {
382+
name string
383+
rule string
384+
baseDir string
385+
expectedGlobs []string
386+
}{
387+
{
388+
name: "single slash has no effect",
389+
rule: "/",
390+
baseDir: "/tmp/test",
391+
expectedGlobs: []string{},
392+
},
393+
{
394+
name: "negated single slash has no effect",
395+
rule: "!/",
396+
baseDir: "/tmp/test",
397+
expectedGlobs: []string{},
398+
},
399+
{
400+
name: "slash with star ignores everything",
401+
rule: "/*",
402+
baseDir: "/tmp/test",
403+
expectedGlobs: []string{
404+
"/tmp/test/*/**",
405+
"/tmp/test/*",
406+
},
407+
},
408+
{
409+
name: "root directory pattern",
410+
rule: "/foo",
411+
baseDir: "/tmp/test",
412+
expectedGlobs: []string{
413+
"/tmp/test/foo/**",
414+
"/tmp/test/foo",
415+
},
416+
},
417+
{
418+
name: "root directory with trailing slash",
419+
rule: "/foo/",
420+
baseDir: "/tmp/test",
421+
expectedGlobs: []string{
422+
"/tmp/test/foo/**",
423+
},
424+
},
425+
{
426+
name: "non-root directory pattern",
427+
rule: "foo",
428+
baseDir: "/tmp/test",
429+
expectedGlobs: []string{
430+
"/tmp/test/**/foo/**",
431+
"/tmp/test/**/foo",
432+
},
433+
},
434+
{
435+
name: "non-root directory with trailing slash",
436+
rule: "foo/",
437+
baseDir: "/tmp/test",
438+
expectedGlobs: []string{
439+
"/tmp/test/**/foo/**",
440+
},
441+
},
442+
}
443+
444+
for _, tc := range testCases {
445+
t.Run(tc.name, func(t *testing.T) {
446+
globs := parseIgnoreRuleToGlobs(tc.rule, tc.baseDir)
447+
assert.ElementsMatch(t, tc.expectedGlobs, globs,
448+
"Rule: %q, Expected: %v, Got: %v", tc.rule, tc.expectedGlobs, globs)
449+
})
450+
}
451+
}
452+
453+
func TestFileFilter_SlashPatternInGitIgnore(t *testing.T) {
454+
t.Run("gitignore with single slash has no effect", func(t *testing.T) {
455+
tempDir := t.TempDir()
456+
457+
// Create test files
458+
files := []string{
459+
"file1.txt",
460+
"file2.txt",
461+
"subdir/file3.txt",
462+
"subdir/nested/file4.txt",
463+
}
464+
465+
for _, file := range files {
466+
filePath := filepath.Join(tempDir, file)
467+
createFileInPath(t, filePath, []byte("test content"))
468+
}
469+
470+
// Create .gitignore with "/"
471+
gitignorePath := filepath.Join(tempDir, ".gitignore")
472+
createFileInPath(t, gitignorePath, []byte("/"))
473+
474+
// Test file filtering
475+
fileFilter := NewFileFilter(tempDir, &log.Logger)
476+
rules, err := fileFilter.GetRules([]string{".gitignore"})
477+
assert.NoError(t, err)
478+
479+
// Should only have default rules since "/" has no effect
480+
assert.Equal(t, fileFilter.defaultRules, rules, "Rules should only contain default rules")
481+
482+
// Get all files and filter them
483+
allFiles := fileFilter.GetAllFiles()
484+
filteredFiles := fileFilter.GetFilteredFiles(allFiles, rules)
485+
486+
// Collect filtered files
487+
var filteredFilesList []string
488+
for file := range filteredFiles {
489+
relPath, err := filepath.Rel(tempDir, file)
490+
assert.NoError(t, err)
491+
// Normalize path separators for cross-platform compatibility
492+
filteredFilesList = append(filteredFilesList, filepath.ToSlash(relPath))
493+
}
494+
495+
// With "/" pattern, no files should be ignored (all files pass through)
496+
expectedFiles := []string{".gitignore", "file1.txt", "file2.txt", "subdir/file3.txt", "subdir/nested/file4.txt"}
497+
assert.ElementsMatch(t, expectedFiles, filteredFilesList, "All files should pass through filter")
498+
})
499+
500+
t.Run("gitignore with /* ignores all files", func(t *testing.T) {
501+
tempDir := t.TempDir()
502+
503+
// Create test files
504+
files := []string{
505+
"file1.txt",
506+
"file2.txt",
507+
"subdir/file3.txt",
508+
}
509+
510+
for _, file := range files {
511+
filePath := filepath.Join(tempDir, file)
512+
createFileInPath(t, filePath, []byte("test content"))
513+
}
514+
515+
// Create .gitignore with "/*"
516+
gitignorePath := filepath.Join(tempDir, ".gitignore")
517+
createFileInPath(t, gitignorePath, []byte("/*"))
518+
519+
// Test file filtering
520+
fileFilter := NewFileFilter(tempDir, &log.Logger)
521+
rules, err := fileFilter.GetRules([]string{".gitignore"})
522+
assert.NoError(t, err)
523+
524+
// Get all files and filter them
525+
allFiles := fileFilter.GetAllFiles()
526+
filteredFiles := fileFilter.GetFilteredFiles(allFiles, rules)
527+
528+
// Collect filtered files
529+
var filteredFilesList []string
530+
for file := range filteredFiles {
531+
filteredFilesList = append(filteredFilesList, file)
532+
}
533+
534+
// With "/*" pattern, all files should be ignored
535+
assert.Empty(t, filteredFilesList, "All files should be filtered out with /* pattern")
536+
})
537+
}

0 commit comments

Comments
 (0)