这是indexloc提供的服务,不要输入任何密码
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions internal/fsgitignore/gitignore.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"io"
"io/fs"
"os"
"os/exec"
"path/filepath"
"strings"
"sync"
Expand Down Expand Up @@ -89,6 +90,62 @@ func handleIfExists(fsys fs.FS, path, filename string, handler func(r io.Reader)
return nil
}

// getGlobalGitignorePath returns the path to the global gitignore file.
// It first checks for ~/.gitignore, then falls back to git config core.excludesFile.
func getGlobalGitignorePath() (string, error) {
// Get home directory first
home, err := os.UserHomeDir()
if err != nil {
return "", err
}

// Check for default ~/.gitignore first
defaultPath := filepath.Join(home, ".gitignore")
if _, err := os.Stat(defaultPath); err == nil {
return defaultPath, nil
}

// Fall back to checking git config
cmd := exec.Command("git", "config", "--global", "--get", "core.excludesFile")
output, err := cmd.Output()
if err == nil {
path := strings.TrimSpace(string(output))
if path != "" {
// Expand ~ to home directory if needed
if strings.HasPrefix(path, "~/") {
path = filepath.Join(home, path[2:])
}
return path, nil
}
}

// No global gitignore file found
return "", nil
}

// GetGlobalIgnorePatterns parses and returns global gitignore patterns from the user's global gitignore file.
func GetGlobalIgnorePatterns() ([]gitignore.Pattern, error) {
globalPath, err := getGlobalGitignorePath()
if err != nil {
return nil, err
}

if globalPath == "" {
return nil, nil // No global gitignore file found
}

file, err := os.Open(globalPath)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
return nil, nil // No global gitignore file is fine
}
return nil, err
}
defer file.Close()

return ParseIgnoreFile(file, nil), nil
}

//go:embed gitignore-defaults
var defaultIgnoreFile []byte

Expand Down
149 changes: 149 additions & 0 deletions internal/fsgitignore/gitignore_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
package fsgitignore

import (
"os"
"path/filepath"
"strings"
"testing"

"github.com/go-git/go-git/v5/plumbing/format/gitignore"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestGetGlobalIgnorePatterns(t *testing.T) {
tests := []struct {
name string
setupFunc func(t *testing.T) (cleanup func())
expectedError bool
expectedPatterns bool
}{
{
name: "no global gitignore file",
setupFunc: func(t *testing.T) func() {
// Create a temporary home directory that doesn't have .gitignore
tempDir := t.TempDir()
oldHome := os.Getenv("HOME")
os.Setenv("HOME", tempDir)
return func() {
os.Setenv("HOME", oldHome)
}
},
expectedError: false,
expectedPatterns: false,
},
{
name: "global gitignore file exists",
setupFunc: func(t *testing.T) func() {
// Create a temporary home directory with .gitignore
tempDir := t.TempDir()
gitignoreContent := `# Global gitignore
*.log
.DS_Store
`
require.NoError(t, os.WriteFile(filepath.Join(tempDir, ".gitignore"), []byte(gitignoreContent), 0600))

oldHome := os.Getenv("HOME")
os.Setenv("HOME", tempDir)
return func() {
os.Setenv("HOME", oldHome)
}
},
expectedError: false,
expectedPatterns: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cleanup := tt.setupFunc(t)
defer cleanup()

patterns, err := GetGlobalIgnorePatterns()

if tt.expectedError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}

if tt.expectedPatterns {
assert.NotEmpty(t, patterns)
// Check that at least one pattern was parsed
assert.True(t, len(patterns) > 0)
} else {
assert.Empty(t, patterns)
}
})
}
}

func TestGetGlobalGitignorePath(t *testing.T) {
tests := []struct {
name string
setupFunc func(t *testing.T) (cleanup func())
expectPath string
expectEmpty bool
}{
{
name: "no gitignore file found",
setupFunc: func(t *testing.T) func() {
tempDir := t.TempDir()
oldHome := os.Getenv("HOME")
os.Setenv("HOME", tempDir)
return func() {
os.Setenv("HOME", oldHome)
}
},
expectEmpty: true,
},
{
name: "default gitignore file exists",
setupFunc: func(t *testing.T) func() {
tempDir := t.TempDir()
require.NoError(t, os.WriteFile(filepath.Join(tempDir, ".gitignore"), []byte("*.log"), 0600))
oldHome := os.Getenv("HOME")
os.Setenv("HOME", tempDir)
return func() {
os.Setenv("HOME", oldHome)
}
},
expectPath: "/.gitignore",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cleanup := tt.setupFunc(t)
defer cleanup()

path, err := getGlobalGitignorePath()
assert.NoError(t, err)

if tt.expectEmpty {
assert.Empty(t, path)
} else if tt.expectPath != "" {
assert.True(t, strings.HasSuffix(path, tt.expectPath))
}
})
}
}

func TestParseIgnoreFile(t *testing.T) {
content := `# This is a comment
*.log
# Another comment
.DS_Store
build/
`
reader := strings.NewReader(content)
patterns := ParseIgnoreFile(reader, nil)

assert.Len(t, patterns, 3) // Should have 3 patterns (comments and empty lines ignored)

// Test that patterns work correctly by testing matches
assert.NotEqual(t, gitignore.NoMatch, patterns[0].Match([]string{"test.log"}, false))
assert.NotEqual(t, gitignore.NoMatch, patterns[1].Match([]string{".DS_Store"}, false))
assert.NotEqual(t, gitignore.NoMatch, patterns[2].Match([]string{"build"}, true))
}
9 changes: 9 additions & 0 deletions pkg/files/tree.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,15 @@ func GetTree(fsys fs.FS, cfg TreeConfig) ([]string, error) {
ignorePatterns = append(ignorePatterns, fsgitignore.ParsePatterns(cfg.IgnoreDirs, []string{})...)
}

// Add global gitignore patterns
if !cfg.DisableGitIgnore {
globalPatterns, err := fsgitignore.GetGlobalIgnorePatterns()
if err == nil && globalPatterns != nil {
ignorePatterns = append(ignorePatterns, globalPatterns...)
}
// Note: we silently ignore errors reading global gitignore to avoid breaking the tree operation
}

var walk func(currentPath, prefix string, depth int, maxEntries float64) error
walk = func(currentPath, prefix string, depth int, maxEntries float64) error {
if cfg.MaxDepth > 0 && depth > cfg.MaxDepth {
Expand Down
5 changes: 5 additions & 0 deletions pkg/rules/analyze.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,11 @@ func (a *Analyzer) collectDirectories(ctx context.Context, fsys fs.FS, root stri
if len(a.cnf.IgnoreDirs) > 0 {
ignorePatterns = append(ignorePatterns, fsgitignore.ParsePatterns(a.cnf.IgnoreDirs, fsgitignore.Split(root))...)
}
// Add global gitignore patterns
globalPatterns, err := fsgitignore.GetGlobalIgnorePatterns()
if err == nil && globalPatterns != nil {
ignorePatterns = append(ignorePatterns, globalPatterns...)
}
return fs.WalkDir(fsys, root, func(path string, d fs.DirEntry, err error) error {
select {
case <-ctx.Done():
Expand Down
43 changes: 43 additions & 0 deletions pkg/rules/analyze_osfs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package rules_test
import (
"embed"
"os"
"path/filepath"
"testing"

"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -43,6 +44,48 @@ func TestAnalyze_OSFS_MockRules(t *testing.T) {
}, result)
}

// Test global gitignore support with real filesystem
func TestAnalyze_OSFS_GlobalGitignore(t *testing.T) {
// Create temporary directory for mock home
tmpDir := t.TempDir()
homeDir := filepath.Join(tmpDir, "home")
projectDir := filepath.Join(tmpDir, "project")

require.NoError(t, os.MkdirAll(homeDir, 0755))
require.NoError(t, os.MkdirAll(projectDir, 0755))
require.NoError(t, os.MkdirAll(filepath.Join(projectDir, "global-ignored"), 0755))

// Create global gitignore file
globalGitignore := filepath.Join(homeDir, ".gitignore")
require.NoError(t, os.WriteFile(globalGitignore, []byte("global-ignored/\n"), 0600))

// Create a project file that would be detected if not ignored
composerJSON := filepath.Join(projectDir, "global-ignored", "composer.json")
require.NoError(t, os.WriteFile(composerJSON, []byte(`{"require": {"php": "^8.0"}}`), 0600))

// Set HOME to our temporary directory
t.Setenv("HOME", homeDir)

// Set up analyzer
rulesets, err := rules.LoadFromYAMLDir(testRulesetsDir, "testdata/rulesets")
require.NoError(t, err)
cache, err := eval.NewFileCache("testdata/expr.cache")
require.NoError(t, err)
defer cache.Save() //nolint:errcheck

analyzer, err := rules.NewAnalyzer(rulesets, &rules.AnalyzerConfig{
CELExpressionCache: cache,
})
require.NoError(t, err)

// Analyze the project
reports, err := analyzer.Analyze(t.Context(), os.DirFS(projectDir), ".")
require.NoError(t, err)

// Should be empty because global-ignored/ should be ignored
assert.Empty(t, reports)
}

// Benchmark analysis on a real filesystem, but with mocked files and rulesets.
func BenchmarkAnalyze_OSFS_MockRules(b *testing.B) {
rulesets, err := rules.LoadFromYAMLDir(testRulesetsDir, "testdata/rulesets")
Expand Down
1 change: 1 addition & 0 deletions pkg/rules/analyze_testfs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ func setupAnalyzerWithEmbeddedConfig(t require.TestingT, ignore []string) *rules

// Test analysis on the test filesystem, but with real rulesets.
func TestAnalyze_TestFS_ActualRules(t *testing.T) {

analyzer := setupAnalyzerWithEmbeddedConfig(t, []string{"arg-ignore"})

reports, err := analyzer.Analyze(t.Context(), testFs, ".")
Expand Down