This improves the ignore handling so that directories can be fully
ignored (skipped in the watcher) in more cases. Specifically, where the
previous rule was that any complex `!`-pattern would disable skipping
directories, the new rule is that only matches on patterns *after* such
a `!`-pattern disable skipping. That is, the following now does the
intuitive thing:
```
/foo
/bar
!whatever
*
```
- `/foo/**` and `/bar/**` are completely skipped, since there is no
chance anything underneath them could ever be not-ignored
- `!whatever` toggles the "can't skip directories any more" flag
- Anything that matches `*` can't skip directories, because it's
possible we can have `whatever` match something deeper.
To enable this, some refactoring was necessary:
- The "can skip dirs" flag is now a property of the match result, not of
the pattern set as a whole.
- That meant returning a boolean is not good enough, we need to actually
return the entire `Result` (or, like, two booleans but that seemed
uglier and more annoying to use)
- `ShouldIgnore(string) boolean` went away with
`Match(string).IsIgnored()` being the obvious replacement (API
simplification!)
- The watcher then needed to import the `ignore` package (for the
`Result` type), but `fs` imports the watcher and `ignore` imports `fs`.
That's a cycle, so I broke out `Result` into a package of its own so
that it can be safely imported everywhere in things like `type Matcher
interface { Match(string) result.Result }`. There's a fair amount of
stuttering in `result.Result` and maybe we should go with something like
`ignoreresult.R` or so, leaving this open for discussion.
Tests refactored to suit, I think this change is in fact quite well
covered by the existing ones...
Also some noise because a few of the changed files were quite old and
got the `gofumpt` treatment by my editor. Sorry not sorry.
---------
Co-authored-by: Simon Frei <freisim93@gmail.com>
This commit is contained in:
+37
-44
@@ -79,11 +79,17 @@ func (p Pattern) allowsSkippingIgnoredDirs() bool {
|
||||
if p.pattern[0] != '/' {
|
||||
return false
|
||||
}
|
||||
if strings.Contains(p.pattern[1:], "/") {
|
||||
// A "/**" at the end is allowed and doesn't have any bearing on the
|
||||
// below checks; remove it before checking.
|
||||
pattern := strings.TrimSuffix(p.pattern, "/**")
|
||||
if len(pattern) == 0 {
|
||||
return true
|
||||
}
|
||||
if strings.Contains(pattern[1:], "/") {
|
||||
return false
|
||||
}
|
||||
// Double asterisk everywhere in the path except at the end is bad
|
||||
return !strings.Contains(strings.TrimSuffix(p.pattern, "**"), "**")
|
||||
return !strings.Contains(strings.TrimSuffix(pattern, "**"), "**")
|
||||
}
|
||||
|
||||
// The ChangeDetector is responsible for determining if files have changed
|
||||
@@ -99,16 +105,15 @@ type ChangeDetector interface {
|
||||
}
|
||||
|
||||
type Matcher struct {
|
||||
fs fs.Filesystem
|
||||
lines []string // exact lines read from .stignore
|
||||
patterns []Pattern // patterns including those from included files
|
||||
withCache bool
|
||||
matches *cache
|
||||
curHash string
|
||||
stop chan struct{}
|
||||
changeDetector ChangeDetector
|
||||
skipIgnoredDirs bool
|
||||
mut sync.Mutex
|
||||
fs fs.Filesystem
|
||||
lines []string // exact lines read from .stignore
|
||||
patterns []Pattern // patterns including those from included files
|
||||
withCache bool
|
||||
matches *cache
|
||||
curHash string
|
||||
stop chan struct{}
|
||||
changeDetector ChangeDetector
|
||||
mut sync.Mutex
|
||||
}
|
||||
|
||||
// An Option can be passed to New()
|
||||
@@ -131,10 +136,9 @@ func WithChangeDetector(cd ChangeDetector) Option {
|
||||
|
||||
func New(fs fs.Filesystem, opts ...Option) *Matcher {
|
||||
m := &Matcher{
|
||||
fs: fs,
|
||||
stop: make(chan struct{}),
|
||||
mut: sync.NewMutex(),
|
||||
skipIgnoredDirs: true,
|
||||
fs: fs,
|
||||
stop: make(chan struct{}),
|
||||
mut: sync.NewMutex(),
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt(m)
|
||||
@@ -198,23 +202,6 @@ func (m *Matcher) parseLocked(r io.Reader, file string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
m.skipIgnoredDirs = true
|
||||
var previous string
|
||||
for _, p := range patterns {
|
||||
// We automatically add patterns with a /** suffix, which normally
|
||||
// means that we cannot skip directories. However if the same
|
||||
// pattern without the /** already exists (which is true for
|
||||
// automatically added patterns) we can skip.
|
||||
if l := len(p.pattern); l > 3 && p.pattern[:len(p.pattern)-3] == previous {
|
||||
continue
|
||||
}
|
||||
if !p.allowsSkippingIgnoredDirs() {
|
||||
m.skipIgnoredDirs = false
|
||||
break
|
||||
}
|
||||
previous = p.pattern
|
||||
}
|
||||
|
||||
m.curHash = newHash
|
||||
m.patterns = patterns
|
||||
if m.withCache {
|
||||
@@ -228,10 +215,10 @@ func (m *Matcher) parseLocked(r io.Reader, file string) error {
|
||||
func (m *Matcher) Match(file string) (result ignoreresult.R) {
|
||||
switch {
|
||||
case fs.IsTemporary(file):
|
||||
return ignoreresult.Ignored
|
||||
return ignoreresult.IgnoreAndSkip
|
||||
|
||||
case fs.IsInternal(file):
|
||||
return ignoreresult.Ignored
|
||||
return ignoreresult.IgnoreAndSkip
|
||||
|
||||
case file == ".":
|
||||
return ignoreresult.NotIgnored
|
||||
@@ -257,19 +244,31 @@ func (m *Matcher) Match(file string) (result ignoreresult.R) {
|
||||
}()
|
||||
}
|
||||
|
||||
// Check all the patterns for a match.
|
||||
// Check all the patterns for a match. Track whether the patterns so far
|
||||
// allow skipping matched directories or not. As soon as we hit an
|
||||
// exclude pattern (with some exceptions), we can't skip directories
|
||||
// anymore.
|
||||
file = filepath.ToSlash(file)
|
||||
var lowercaseFile string
|
||||
canSkipDir := true
|
||||
for _, pattern := range m.patterns {
|
||||
if canSkipDir && !pattern.allowsSkippingIgnoredDirs() {
|
||||
canSkipDir = false
|
||||
}
|
||||
|
||||
res := pattern.result
|
||||
if canSkipDir {
|
||||
res = res.WithSkipDir()
|
||||
}
|
||||
if pattern.result.IsCaseFolded() {
|
||||
if lowercaseFile == "" {
|
||||
lowercaseFile = strings.ToLower(file)
|
||||
}
|
||||
if pattern.match.Match(lowercaseFile) {
|
||||
return pattern.result
|
||||
return res
|
||||
}
|
||||
} else if pattern.match.Match(file) {
|
||||
return pattern.result
|
||||
return res
|
||||
}
|
||||
}
|
||||
|
||||
@@ -327,12 +326,6 @@ func (m *Matcher) clean(d time.Duration) {
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Matcher) SkipIgnoredDirs() bool {
|
||||
m.mut.Lock()
|
||||
defer m.mut.Unlock()
|
||||
return m.skipIgnoredDirs
|
||||
}
|
||||
|
||||
func hashPatterns(patterns []Pattern) string {
|
||||
h := sha256.New()
|
||||
for _, pat := range patterns {
|
||||
|
||||
Reference in New Issue
Block a user