Skip to content
Open
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
51 changes: 36 additions & 15 deletions pkg/devspace/pipeline/engine/basichandler/commands/run_watch.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ func (w *watcher) Watch(ctx context.Context, action func(ctx context.Context) er

// get folders from patterns
pathsToWatch := map[string]bool{}
watchPatterns := make([]watchPattern, 0, len(patterns))
for i, p := range patterns {
patternsSplitted := strings.Split(filepath.ToSlash(p), "/")
lastIndex := len(patternsSplitted) - 1
Expand Down Expand Up @@ -116,12 +117,13 @@ func (w *watcher) Watch(ctx context.Context, action func(ctx context.Context) er
}

pathsToWatch[absolutePath] = true
watchPatterns = append(watchPatterns, watchPattern{base: absolutePath, pattern: patterns[i]})
}

watchTree := notify.NewTree()
defer watchTree.Close()

globalChannel := make(chan string, 100)
globalChannel := make(chan watchEvent, 100)
for targetPath := range pathsToWatch {
stat, err := os.Stat(targetPath)
if err != nil {
Expand Down Expand Up @@ -160,18 +162,45 @@ func (w *watcher) Watch(ctx context.Context, action func(ctx context.Context) er
if err != nil {
log.Debugf("error converting path %s: %v", e.Path(), err)
} else {
globalChannel <- filepath.ToSlash(relPath)
globalChannel <- watchEvent{base: base, path: filepath.ToSlash(relPath)}
}
}
}
}(targetPath, eventChannel)
}

// start command
return w.handleCommand(ctx, patterns, excludes, action, globalChannel)
return w.handleCommand(ctx, watchPatterns, excludes, action, globalChannel)
}

func (w *watcher) handleCommand(ctx context.Context, patterns []string, excludes []string, action func(ctx context.Context) error, events chan string) error {
// watchPattern is a glob pattern that is relative to the directory (base) it is watched in.
type watchPattern struct {
base string
pattern string
}

// watchEvent is a file system event with a path relative to the watched base directory.
type watchEvent struct {
base string
path string
}

// matchesPatterns reports whether the event matches a pattern that belongs to the
// same base directory as the event. Patterns are relative to their own base, so they
// must not be matched against events coming from another watched directory.
func matchesPatterns(patterns []watchPattern, e watchEvent) bool {
for _, p := range patterns {
if p.base != e.base {
continue
}
if matched, _ := doublestar.Match(p.pattern, e.path); matched {
return true
}
}
return false
}

func (w *watcher) handleCommand(ctx context.Context, patterns []watchPattern, excludes []string, action func(ctx context.Context) error, events chan watchEvent) error {
hc := interp.HandlerCtx(ctx)
var t *tomb.Tomb
if !w.options.SkipInitial {
Expand All @@ -190,26 +219,18 @@ func (w *watcher) handleCommand(ctx context.Context, patterns []string, excludes
return nil
case e := <-events:
// check if match
hasMatched := false
for _, p := range patterns {
hasMatched, _ = doublestar.Match(p, e)
if hasMatched {
break
}
}

if hasMatched {
if matchesPatterns(patterns, e) {
excluded := false
for _, excludePath := range excludes {
excluded, _ = doublestar.Match(excludePath, e)
excluded, _ = doublestar.Match(excludePath, e.path)
if excluded {
break
}
}

if !excluded {
numEvents++
lastChange = e
lastChange = e.path
}
}
case <-time.After(time.Millisecond * 1200):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package commands

import "testing"

func TestMatchesPatterns(t *testing.T) {
patterns := []watchPattern{
{base: "/project", pattern: "devspace.yaml"},
{base: "/var/tmp/chart", pattern: "**"},
}

testCases := []struct {
name string
event watchEvent
want bool
}{
{"exact file in its own base", watchEvent{base: "/project", path: "devspace.yaml"}, true},
{"unrelated file in project base", watchEvent{base: "/project", path: "main.go"}, false},
{"nested unrelated file in project base", watchEvent{base: "/project", path: "src/main.go"}, false},
{"any file in chart base", watchEvent{base: "/var/tmp/chart", path: "templates/deploy.yaml"}, true},
{"unknown base", watchEvent{base: "/other", path: "devspace.yaml"}, false},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
if got := matchesPatterns(patterns, tc.event); got != tc.want {
t.Fatalf("matchesPatterns(%v) = %v, want %v", tc.event, got, tc.want)
}
})
}
}