Skip to content

Commit c7501c9

Browse files
jordikroondunglas
authored andcommitted
feat: add support for exclusion patterns with '!' prefix
Add exclusion pattern support to allow users to exclude specific paths from triggering reloads. Patterns prefixed with '!' are now parsed as exclusions and stored separately. The globalWatcher checks if events match any exclusion patterns before processing them, preventing excluded paths from triggering reloads while still allowing them to be matched for filtering purposes. - Add isExclude field to track exclusion patterns - Parse '!' prefix during pattern initialization - Rename allowReload to matchesEvent for clarity on matching logic - Extract exclusion check into separate allowReload wrapper - Add isExcludedEvent method to globalWatcher for event filtering - Filter excluded events before debouncing in event listener
1 parent cd1f4e0 commit c7501c9

3 files changed

Lines changed: 83 additions & 4 deletions

File tree

internal/watcher/pattern.go

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ type pattern struct {
1919
parsedValues []string
2020
events chan eventHolder
2121
failureCount int
22+
isExclude bool
2223

2324
watcher *watcher.Watcher
2425
}
@@ -33,8 +34,17 @@ func (p *pattern) startSession() {
3334

3435
// this method prepares the pattern struct (aka /path/*pattern)
3536
func (p *pattern) parse() (err error) {
36-
// first we clean the value
37-
absPattern, err := fastabs.FastAbs(p.value)
37+
// detect exclusion before resolving to absolute
38+
raw := strings.TrimSpace(p.value)
39+
if strings.HasPrefix(raw, "!") {
40+
p.isExclude = true
41+
raw = strings.TrimSpace(strings.TrimPrefix(raw, "!"))
42+
} else {
43+
p.isExclude = false
44+
}
45+
46+
// then we clean the value
47+
absPattern, err := fastabs.FastAbs(raw)
3848
if err != nil {
3949
return err
4050
}
@@ -81,7 +91,7 @@ func (p *pattern) parse() (err error) {
8191
return nil
8292
}
8393

84-
func (p *pattern) allowReload(event *watcher.Event) bool {
94+
func (p *pattern) matchesEvent(event *watcher.Event) bool {
8595
if !isValidEventType(event.EffectType) || !isValidPathType(event) {
8696
return false
8797
}
@@ -92,6 +102,15 @@ func (p *pattern) allowReload(event *watcher.Event) bool {
92102
return p.isValidPattern(event.PathName) || p.isValidPattern(event.AssociatedPathName)
93103
}
94104

105+
func (p *pattern) allowReload(event *watcher.Event) bool {
106+
// Excludes never trigger reload by themselves, but they still match events for filtering.
107+
if p.isExclude {
108+
return false
109+
}
110+
111+
return p.matchesEvent(event)
112+
}
113+
95114
func (p *pattern) handle(event *watcher.Event) {
96115
// If the watcher prematurely sends the die@ event, retry watching
97116
if event.PathType == watcher.PathTypeWatcher && strings.HasPrefix(event.PathName, "e/self/die@") && watcherIsActive.Load() {

internal/watcher/pattern_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,44 @@ func TestAnAssociatedEventTriggersTheWatcher(t *testing.T) {
355355
assert.Equal(t, e, (<-w.events).event)
356356
}
357357

358+
func TestGlobalWatcherIsExcludedEvent(t *testing.T) {
359+
pg := &PatternGroup{Patterns: []string{"/app/**/*.php", "!/app/vendor/**"}}
360+
361+
include := &pattern{patternGroup: pg, value: "/app/**/*.php"}
362+
exclude := &pattern{patternGroup: pg, value: "!/app/vendor/**"}
363+
364+
require.NoError(t, include.parse())
365+
require.NoError(t, exclude.parse())
366+
367+
gw := &globalWatcher{
368+
groups: []*PatternGroup{pg},
369+
watchers: []*pattern{include, exclude},
370+
excludes: map[*PatternGroup][]*pattern{pg: {exclude}},
371+
}
372+
373+
inVendor := &watcher.Event{PathName: "/app/vendor/pkg/file.php"}
374+
assert.True(t, include.matchesEvent(inVendor))
375+
assert.True(t, gw.isExcludedEvent(pg, inVendor))
376+
377+
notInVendor := &watcher.Event{PathName: "/app/src/file.php"}
378+
assert.True(t, include.matchesEvent(notInVendor))
379+
assert.False(t, gw.isExcludedEvent(pg, notInVendor))
380+
}
381+
382+
func TestExcludeMatchesAssociatedPath(t *testing.T) {
383+
pg := &PatternGroup{Patterns: []string{"/app/**/*.php", "!/app/vendor/**"}}
384+
385+
exclude := &pattern{patternGroup: pg, value: "!/app/vendor/**"}
386+
require.NoError(t, exclude.parse())
387+
388+
gw := &globalWatcher{
389+
excludes: map[*PatternGroup][]*pattern{pg: {exclude}},
390+
}
391+
392+
e := &watcher.Event{PathName: "/tmp/temporary", AssociatedPathName: "/app/vendor/x/file.php"}
393+
assert.True(t, gw.isExcludedEvent(pg, e))
394+
}
395+
358396
func relativeDir(t *testing.T, relativePath string) string {
359397
t.Helper()
360398

internal/watcher/watcher.go

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ type eventHolder struct {
5050
type globalWatcher struct {
5151
groups []*PatternGroup
5252
watchers []*pattern
53+
excludes map[*PatternGroup][]*pattern
5354
events chan eventHolder
5455
stop chan struct{}
5556
}
@@ -138,11 +139,18 @@ func (p *pattern) retryWatching() {
138139
func (g *globalWatcher) startWatching() error {
139140
g.events = make(chan eventHolder)
140141
g.stop = make(chan struct{})
142+
g.excludes = make(map[*PatternGroup][]*pattern)
141143

142144
if err := g.parseFilePatterns(); err != nil {
143145
return err
144146
}
145147

148+
for _, w := range g.watchers {
149+
if w.isExclude {
150+
g.excludes[w.patternGroup] = append(g.excludes[w.patternGroup], w)
151+
}
152+
}
153+
146154
for _, w := range g.watchers {
147155
w.events = g.events
148156
w.startSession()
@@ -170,6 +178,17 @@ func (g *globalWatcher) stopWatching() {
170178
}
171179
}
172180

181+
func (g *globalWatcher) isExcludedEvent(pg *PatternGroup, e *watcher.Event) bool {
182+
excludes := g.excludes[pg]
183+
for _, ex := range excludes {
184+
if ex.matchesEvent(e) {
185+
return true
186+
}
187+
}
188+
189+
return false
190+
}
191+
173192
func (g *globalWatcher) listenForFileEvents() {
174193
timer := time.NewTimer(debounceDuration)
175194
timer.Stop()
@@ -182,8 +201,11 @@ func (g *globalWatcher) listenForFileEvents() {
182201
case <-g.stop:
183202
return
184203
case eh := <-g.events:
185-
timer.Reset(debounceDuration)
204+
if g.isExcludedEvent(eh.patternGroup, eh.event) {
205+
continue
206+
}
186207

208+
timer.Reset(debounceDuration)
187209
eventsPerGroup[eh.patternGroup] = append(eventsPerGroup[eh.patternGroup], eh.event)
188210
case <-timer.C:
189211
timer.Stop()

0 commit comments

Comments
 (0)