-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathdotignore.go
156 lines (129 loc) · 4.19 KB
/
dotignore.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
package dotignore
import (
"errors"
"fmt"
"io"
"os"
"path/filepath"
"regexp"
"strings"
"github.com/codeglyph/go-dotignore/internal"
)
type ignorePattern struct {
pattern string
regexPattern *regexp.Regexp
parentDirs []string
negate bool
}
// PatternMatcher provides methods to parse, store, and evaluate ignore patterns against file paths.
type PatternMatcher struct {
ignorePatterns []ignorePattern
}
// NewPatternMatcher initializes a new PatternMatcher instance from a list of string patterns.
func NewPatternMatcher(patterns []string) (*PatternMatcher, error) {
ignorePatterns, err := buildIgnorePatterns(patterns)
if err != nil {
return nil, err
}
return &PatternMatcher{
ignorePatterns: ignorePatterns,
}, nil
}
// NewPatternMatcherFromReader initializes a new PatternMatcher instance from an io.Reader.
func NewPatternMatcherFromReader(reader io.Reader) (*PatternMatcher, error) {
patterns, err := internal.ReadLines(reader)
if err != nil {
return nil, fmt.Errorf("failed to parse patterns: %w", err)
}
return NewPatternMatcher(patterns)
}
// NewPatternMatcherFromFile reads a file containing ignore patterns and returns a PatternMatcher instance.
func NewPatternMatcherFromFile(filepath string) (*PatternMatcher, error) {
fileReader, err := os.Open(filepath)
if err != nil {
return nil, fmt.Errorf("failed to open file: %w", err)
}
defer fileReader.Close()
patterns, err := internal.ReadLines(fileReader)
if err != nil {
return nil, fmt.Errorf("failed to parse patterns: %w", err)
}
return NewPatternMatcher(patterns)
}
// Matches checks if the given file path matches any of the ignore patterns in the PatternMatcher.
func (p *PatternMatcher) Matches(file string) (bool, error) {
file = filepath.Clean(file)
if file == "." {
return false, nil
}
return matches(file, p.ignorePatterns)
}
func buildIgnorePatterns(patterns []string) ([]ignorePattern, error) {
var ignorePatterns []ignorePattern
for _, pattern := range patterns {
pattern := strings.TrimSpace(pattern)
if pattern == "" || strings.HasPrefix(pattern, "#") {
continue
}
// normalize pattern
pattern = filepath.Clean(pattern)
isNegation := strings.HasPrefix(pattern, "!")
if isNegation && len(pattern) == 1 {
// A single '!' is invalid
return nil, errors.New("invalid pattern: '!'")
}
if isNegation {
pattern = pattern[1:]
}
patternDirs := strings.Split(pattern, "/")
regexPattern, err := internal.BuildRegex(pattern)
if err != nil {
return nil, err
}
ignorePatterns = append(ignorePatterns, ignorePattern{
pattern: pattern,
regexPattern: regexPattern,
parentDirs: patternDirs,
negate: isNegation,
})
}
return ignorePatterns, nil
}
// matches checks if the file matches patterns efficiently.
func matches(file string, ignorePatterns []ignorePattern) (bool, error) {
// Normalize the file path to use OS-specific separators
normalizedFile := filepath.FromSlash(file)
// Split the parent path into components
parentPath := filepath.Dir(normalizedFile)
parentDirs := strings.Split(parentPath, string(filepath.Separator))
matched := false
for _, pattern := range ignorePatterns {
matches, err := matchWithRegex(normalizedFile, pattern)
if err != nil {
return false, err
}
// If there's no direct match, check parent directories for a match
if !matches && parentPath != "." && len(pattern.parentDirs) <= len(parentDirs) {
subPath := strings.Join(parentDirs[:len(pattern.parentDirs)], string(filepath.Separator))
subPathRegex, _ := internal.BuildRegex(subPath)
subPathIgnorePattern := ignorePattern{
pattern: subPath,
regexPattern: subPathRegex,
}
matches, _ = matchWithRegex(strings.Join(pattern.parentDirs, string(filepath.Separator)), subPathIgnorePattern)
}
// Update match status based on negation
if matches {
matched = !pattern.negate
}
}
return matched, nil
}
// matchWithRegex converts a pattern to a regular expression and checks if it matches the path.
func matchWithRegex(path string, ignorePat ignorePattern) (bool, error) {
if _, err := filepath.Match(ignorePat.pattern, path); err != nil {
return false, err
}
matched := ignorePat.regexPattern.MatchString(path)
return matched, nil
}