Skip to content

Commit 6a2b778

Browse files
committed
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 a6b0577 commit 6a2b778

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
@@ -17,6 +17,7 @@ type pattern struct {
1717
parsedValues []string
1818
events chan eventHolder
1919
failureCount int
20+
isExclude bool
2021

2122
watcher *watcher.Watcher
2223
}
@@ -31,8 +32,17 @@ func (p *pattern) startSession() {
3132

3233
// this method prepares the pattern struct (aka /path/*pattern)
3334
func (p *pattern) parse() (err error) {
34-
// first we clean the value
35-
absPattern, err := fastabs.FastAbs(p.value)
35+
// detect exclusion before resolving to absolute
36+
raw := strings.TrimSpace(p.value)
37+
if strings.HasPrefix(raw, "!") {
38+
p.isExclude = true
39+
raw = strings.TrimSpace(strings.TrimPrefix(raw, "!"))
40+
} else {
41+
p.isExclude = false
42+
}
43+
44+
// then we clean the value
45+
absPattern, err := fastabs.FastAbs(raw)
3646
if err != nil {
3747
return err
3848
}
@@ -72,7 +82,7 @@ func (p *pattern) parse() (err error) {
7282
return nil
7383
}
7484

75-
func (p *pattern) allowReload(event *watcher.Event) bool {
85+
func (p *pattern) matchesEvent(event *watcher.Event) bool {
7686
if !isValidEventType(event.EffectType) || !isValidPathType(event) {
7787
return false
7888
}
@@ -83,6 +93,15 @@ func (p *pattern) allowReload(event *watcher.Event) bool {
8393
return p.isValidPattern(event.PathName) || p.isValidPattern(event.AssociatedPathName)
8494
}
8595

96+
func (p *pattern) allowReload(event *watcher.Event) bool {
97+
// Excludes never trigger reload by themselves, but they still match events for filtering.
98+
if p.isExclude {
99+
return false
100+
}
101+
102+
return p.matchesEvent(event)
103+
}
104+
86105
func (p *pattern) handle(event *watcher.Event) {
87106
// If the watcher prematurely sends the die@ event, retry watching
88107
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
@@ -323,6 +323,44 @@ func TestAnAssociatedEventTriggersTheWatcher(t *testing.T) {
323323
assert.Equal(t, e, (<-w.events).event)
324324
}
325325

326+
func TestGlobalWatcherIsExcludedEvent(t *testing.T) {
327+
pg := &PatternGroup{Patterns: []string{"/app/**/*.php", "!/app/vendor/**"}}
328+
329+
include := &pattern{patternGroup: pg, value: "/app/**/*.php"}
330+
exclude := &pattern{patternGroup: pg, value: "!/app/vendor/**"}
331+
332+
require.NoError(t, include.parse())
333+
require.NoError(t, exclude.parse())
334+
335+
gw := &globalWatcher{
336+
groups: []*PatternGroup{pg},
337+
watchers: []*pattern{include, exclude},
338+
excludes: map[*PatternGroup][]*pattern{pg: {exclude}},
339+
}
340+
341+
inVendor := &watcher.Event{PathName: "/app/vendor/pkg/file.php"}
342+
assert.True(t, include.matchesEvent(inVendor))
343+
assert.True(t, gw.isExcludedEvent(pg, inVendor))
344+
345+
notInVendor := &watcher.Event{PathName: "/app/src/file.php"}
346+
assert.True(t, include.matchesEvent(notInVendor))
347+
assert.False(t, gw.isExcludedEvent(pg, notInVendor))
348+
}
349+
350+
func TestExcludeMatchesAssociatedPath(t *testing.T) {
351+
pg := &PatternGroup{Patterns: []string{"/app/**/*.php", "!/app/vendor/**"}}
352+
353+
exclude := &pattern{patternGroup: pg, value: "!/app/vendor/**"}
354+
require.NoError(t, exclude.parse())
355+
356+
gw := &globalWatcher{
357+
excludes: map[*PatternGroup][]*pattern{pg: {exclude}},
358+
}
359+
360+
e := &watcher.Event{PathName: "/tmp/temporary", AssociatedPathName: "/app/vendor/x/file.php"}
361+
assert.True(t, gw.isExcludedEvent(pg, e))
362+
}
363+
326364
func relativeDir(t *testing.T, relativePath string) string {
327365
dir, err := filepath.Abs("./" + relativePath)
328366
assert.NoError(t, err)

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)