mirror of
https://github.com/docker/compose.git
synced 2026-08-27 03:45:29 +00:00
fix(watch): eliminate cross-trigger ignore contamination in file watcher
Remove normalizeWatchRoots, which merged ignore rules across all related roots, causing one service's ignores to suppress ignore for other services Separate OS-level watch paths (pathsToWatch) from trigger roots (notifyList) and keep the original trigger roots in notifyList with each root carries its own isolated PathMatcher Update shouldNotify to notify if any root allow Apply the same fixes to the Darwin watcher Fix a duplicate-path registration bug, remove path matcher intersection and trigger root updates in greatestExistingAncestors Signed-off-by: ManManavadaria <manmanavadaria@gmail.com>
This commit is contained in:
parent
423b409111
commit
f3cd3b8ccf
7 changed files with 189 additions and 238 deletions
|
|
@ -265,9 +265,10 @@ func (s *composeService) watch(ctx context.Context, project *types.Project, opti
|
|||
|
||||
if existingMatcher, exists := ignoresByWatchPath[trigger.Path]; exists {
|
||||
ignore = watch.NewIntersectMatcher(existingMatcher, ignore)
|
||||
} else {
|
||||
paths = append(paths, trigger.Path)
|
||||
}
|
||||
ignoresByWatchPath[trigger.Path] = ignore
|
||||
paths = append(paths, trigger.Path)
|
||||
}
|
||||
serviceWatchRules, err := getWatchRules(config, service)
|
||||
if err != nil {
|
||||
|
|
@ -603,7 +604,8 @@ func (s *composeService) handleWatchBatch(ctx context.Context, project *types.Pr
|
|||
}
|
||||
options.LogTo.Log(
|
||||
api.WatchLogger,
|
||||
fmt.Sprintf("service(s) %q restarted", services))
|
||||
fmt.Sprintf("service(s) %q restarted", services),
|
||||
)
|
||||
}
|
||||
|
||||
eg, ctx := errgroup.WithContext(ctx)
|
||||
|
|
@ -763,7 +765,8 @@ func (s *composeService) initialSync(ctx context.Context, project *types.Project
|
|||
dockerIgnores,
|
||||
watch.EphemeralPathMatcher(),
|
||||
dotGitIgnore,
|
||||
triggerIgnore)
|
||||
triggerIgnore,
|
||||
)
|
||||
|
||||
pathsToCopy, err := s.initialSyncFiles(ctx, project, service, trigger, ignoreInitialSync)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -20,8 +20,6 @@ import (
|
|||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
pathutil "github.com/docker/compose/v5/internal/paths"
|
||||
)
|
||||
|
||||
func greatestExistingAncestor(path string) (string, error) {
|
||||
|
|
@ -42,47 +40,14 @@ func greatestExistingAncestor(path string) (string, error) {
|
|||
return path, nil
|
||||
}
|
||||
|
||||
func greatestExistingAncestors(paths []string, ignoreList map[string]PathMatcher) ([]string, error) {
|
||||
result := []string{}
|
||||
func greatestExistingAncestors(paths []string) ([]string, error) {
|
||||
result := make([]string, 0, len(paths))
|
||||
for _, path := range paths {
|
||||
newP, err := greatestExistingAncestor(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("finding ancestor of %s: %w", path, err)
|
||||
}
|
||||
result = append(result, newP)
|
||||
if path != newP {
|
||||
ignore := ignoreList[path]
|
||||
if oldMatcher, exists := ignoreList[newP]; exists {
|
||||
ignore = NewIntersectMatcher(oldMatcher, ignore)
|
||||
}
|
||||
ignoreList[newP] = ignore
|
||||
delete(ignoreList, path)
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func normalizeWatchRoots(paths []string, ignore map[string]PathMatcher) (map[string]bool, map[string]PathMatcher, error) {
|
||||
notifyList := make(map[string]bool, len(paths))
|
||||
normalizedIgnores := make(map[string]PathMatcher, len(paths))
|
||||
|
||||
for _, root := range paths {
|
||||
root, err := filepath.Abs(root)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
notifyList[root] = true
|
||||
|
||||
matchers := make([]PathMatcher, 0, len(ignore))
|
||||
for triggerPath, matcher := range ignore {
|
||||
if matcher == nil {
|
||||
continue
|
||||
}
|
||||
if root == triggerPath || pathutil.IsChild(root, triggerPath) || pathutil.IsChild(triggerPath, root) {
|
||||
matchers = append(matchers, matcher)
|
||||
}
|
||||
}
|
||||
normalizedIgnores[root] = NewIntersectMatcher(matchers...)
|
||||
}
|
||||
return notifyList, normalizedIgnores, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@
|
|||
package watch
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
|
|
@ -47,149 +46,20 @@ func TestGreatestExistingAncestorsMovesIgnoreToAncestor(t *testing.T) {
|
|||
f := NewTempDirFixture(t)
|
||||
|
||||
missing := f.JoinPath("missing", "child", "file.txt")
|
||||
ignore, err := DockerIgnoreTesterFromContents(f.Path(), "vendor/\n")
|
||||
assert.NilError(t, err)
|
||||
|
||||
ignoreList := map[string]PathMatcher{missing: ignore}
|
||||
paths, err := greatestExistingAncestors([]string{missing}, ignoreList)
|
||||
paths, err := greatestExistingAncestors([]string{missing})
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, 1, len(paths))
|
||||
assert.Equal(t, f.Path(), paths[0])
|
||||
assert.Assert(t, ignoreList[f.Path()] != nil)
|
||||
_, exists := ignoreList[missing]
|
||||
assert.Assert(t, !exists)
|
||||
}
|
||||
|
||||
func TestGreatestExistingAncestorsIntersectsIgnoreOnAncestor(t *testing.T) {
|
||||
f := NewTempDirFixture(t)
|
||||
|
||||
missing := f.JoinPath("missing", "child", "file.txt")
|
||||
vendorIgnore, err := DockerIgnoreTesterFromContents(f.Path(), "vendor/\n")
|
||||
assert.NilError(t, err)
|
||||
tmpIgnore, err := DockerIgnoreTesterFromContents(f.Path(), "tmp/\n")
|
||||
assert.NilError(t, err)
|
||||
|
||||
ignoreList := map[string]PathMatcher{
|
||||
f.Path(): vendorIgnore,
|
||||
missing: tmpIgnore,
|
||||
}
|
||||
paths, err := greatestExistingAncestors([]string{missing}, ignoreList)
|
||||
paths, err := greatestExistingAncestors([]string{missing})
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, 1, len(paths))
|
||||
assert.Equal(t, f.Path(), paths[0])
|
||||
|
||||
inter, ok := ignoreList[f.Path()].(intersectPathMatcher)
|
||||
assert.Assert(t, ok)
|
||||
assert.Equal(t, 2, len(inter.Matchers))
|
||||
}
|
||||
|
||||
func TestNormalizeWatchRootsAbsolutizesPaths(t *testing.T) {
|
||||
rel := "."
|
||||
abs, err := filepath.Abs(rel)
|
||||
assert.NilError(t, err)
|
||||
|
||||
notifyList, _, err := normalizeWatchRoots([]string{rel}, nil)
|
||||
assert.NilError(t, err)
|
||||
assert.Assert(t, notifyList[abs])
|
||||
}
|
||||
|
||||
func TestNormalizeWatchRootsAssignsRelatedIgnores(t *testing.T) {
|
||||
f := NewTempDirFixture(t)
|
||||
|
||||
root := f.Path()
|
||||
child := f.JoinPath("child")
|
||||
vendorIgnore, err := DockerIgnoreTesterFromContents(root, "vendor/\n")
|
||||
assert.NilError(t, err)
|
||||
unrelatedIgnore, err := DockerIgnoreTesterFromContents(root, "build/\n")
|
||||
assert.NilError(t, err)
|
||||
|
||||
ignores := map[string]PathMatcher{
|
||||
root: vendorIgnore,
|
||||
child: vendorIgnore,
|
||||
"/other": unrelatedIgnore,
|
||||
}
|
||||
notifyList, normalizedIgnores, err := normalizeWatchRoots([]string{root, child}, ignores)
|
||||
assert.NilError(t, err)
|
||||
assert.Assert(t, notifyList[root])
|
||||
assert.Assert(t, notifyList[child])
|
||||
|
||||
vendorFile := filepath.Join(root, "vendor", "mod.go")
|
||||
matches, err := normalizedIgnores[root].Matches(vendorFile)
|
||||
assert.NilError(t, err)
|
||||
assert.Assert(t, matches)
|
||||
|
||||
matches, err = normalizedIgnores[child].Matches(vendorFile)
|
||||
assert.NilError(t, err)
|
||||
assert.Assert(t, matches)
|
||||
|
||||
buildFile := filepath.Join(root, "build", "out")
|
||||
matches, err = normalizedIgnores[root].Matches(buildFile)
|
||||
assert.NilError(t, err)
|
||||
assert.Assert(t, !matches)
|
||||
}
|
||||
|
||||
func TestNormalizeWatchRootsSkipsNilMatchers(t *testing.T) {
|
||||
f := NewTempDirFixture(t)
|
||||
|
||||
root := f.Path()
|
||||
notifyList, normalizedIgnores, err := normalizeWatchRoots([]string{root}, map[string]PathMatcher{root: nil})
|
||||
assert.NilError(t, err)
|
||||
assert.Assert(t, notifyList[root])
|
||||
_, ok := normalizedIgnores[root].(EmptyMatcher)
|
||||
assert.Assert(t, ok)
|
||||
}
|
||||
|
||||
func TestNormalizeWatchRootsUsesEmptyMatcherWithoutIgnores(t *testing.T) {
|
||||
f := NewTempDirFixture(t)
|
||||
|
||||
root := f.Path()
|
||||
_, normalizedIgnores, err := normalizeWatchRoots([]string{root}, nil)
|
||||
assert.NilError(t, err)
|
||||
_, ok := normalizedIgnores[root].(EmptyMatcher)
|
||||
assert.Assert(t, ok)
|
||||
}
|
||||
|
||||
func TestNormalizeWatchRootsInheritsParentIgnoreForChild(t *testing.T) {
|
||||
f := NewTempDirFixture(t)
|
||||
|
||||
root := f.Path()
|
||||
child := f.JoinPath("pkg")
|
||||
vendorIgnore, err := DockerIgnoreTesterFromContents(root, "pkg/vendor/\n")
|
||||
assert.NilError(t, err)
|
||||
|
||||
_, normalizedIgnores, err := normalizeWatchRoots([]string{child}, map[string]PathMatcher{root: vendorIgnore})
|
||||
assert.NilError(t, err)
|
||||
|
||||
vendorFile := filepath.Join(child, "vendor", "x.go")
|
||||
matches, err := normalizedIgnores[child].Matches(vendorFile)
|
||||
assert.NilError(t, err)
|
||||
assert.Assert(t, matches)
|
||||
}
|
||||
|
||||
func TestNormalizeWatchRootsIntersectsNestedIgnores(t *testing.T) {
|
||||
f := NewTempDirFixture(t)
|
||||
|
||||
root := f.Path()
|
||||
child := f.JoinPath("pkg")
|
||||
vendorIgnore, err := DockerIgnoreTesterFromContents(root, "vendor/\n")
|
||||
assert.NilError(t, err)
|
||||
tmpIgnore, err := DockerIgnoreTesterFromContents(root, "pkg/tmp/\n")
|
||||
assert.NilError(t, err)
|
||||
|
||||
ignores := map[string]PathMatcher{
|
||||
root: vendorIgnore,
|
||||
child: tmpIgnore,
|
||||
}
|
||||
_, normalizedIgnores, err := normalizeWatchRoots([]string{root, child}, ignores)
|
||||
assert.NilError(t, err)
|
||||
|
||||
vendorFile := filepath.Join(root, "vendor", "x.go")
|
||||
matches, err := normalizedIgnores[root].Matches(vendorFile)
|
||||
assert.NilError(t, err)
|
||||
assert.Assert(t, !matches, "nested ignores must all match for parent root")
|
||||
|
||||
tmpUnderChild := filepath.Join(child, "tmp", "a")
|
||||
matches, err = normalizedIgnores[child].Matches(tmpUnderChild)
|
||||
assert.NilError(t, err)
|
||||
assert.Assert(t, !matches, "nested ignores must all match for child root")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,10 +39,13 @@ type fseventNotify struct {
|
|||
errors chan error
|
||||
stop chan struct{}
|
||||
|
||||
pathsWereWatching map[string]any
|
||||
// ignore maps each pathsWereWatching root to the merged PathMatcher for paths under it.
|
||||
ignore map[string]PathMatcher
|
||||
closeOnce sync.Once
|
||||
// watchPaths are the paths registered with the FSEvents stream.
|
||||
watchPaths []string
|
||||
// notifyList holds the original trigger paths.
|
||||
notifyList map[string]bool
|
||||
// ignore maps each trigger path (from notifyList) to its own isolated PathMatcher
|
||||
ignore map[string]PathMatcher
|
||||
closeOnce sync.Once
|
||||
}
|
||||
|
||||
func (d *fseventNotify) loop() {
|
||||
|
|
@ -58,8 +61,8 @@ func (d *fseventNotify) loop() {
|
|||
for _, e := range events {
|
||||
e.Path = filepath.Join(string(os.PathSeparator), e.Path)
|
||||
|
||||
_, isPathWereWatching := d.pathsWereWatching[e.Path]
|
||||
if e.Flags&fsevents.ItemIsDir == fsevents.ItemIsDir && e.Flags&fsevents.ItemCreated == fsevents.ItemCreated && isPathWereWatching {
|
||||
isTriggerRoot := d.notifyList[e.Path]
|
||||
if e.Flags&fsevents.ItemIsDir == fsevents.ItemIsDir && e.Flags&fsevents.ItemCreated == fsevents.ItemCreated && isTriggerRoot {
|
||||
// This is the first create for the path that we're watching. We always get exactly one of these
|
||||
// even after we get the HistoryDone event. Skip it.
|
||||
continue
|
||||
|
|
@ -81,27 +84,18 @@ func (d *fseventNotify) addStreamPath(name string) {
|
|||
}
|
||||
|
||||
func (d *fseventNotify) Start() error {
|
||||
notifyRoots := make([]string, 0, len(d.pathsWereWatching))
|
||||
for path := range d.pathsWereWatching {
|
||||
notifyRoots = append(notifyRoots, path)
|
||||
}
|
||||
if len(notifyRoots) == 0 {
|
||||
if len(d.watchPaths) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
pathsToWatch, err := greatestExistingAncestors(notifyRoots, d.ignore)
|
||||
watchPaths, err := greatestExistingAncestors(d.watchPaths)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pathsToWatch = pathutil.EncompassingPaths(pathsToWatch)
|
||||
watchPaths = pathutil.EncompassingPaths(watchPaths)
|
||||
|
||||
_, normalizedIgnores, err := normalizeWatchRoots(notifyRoots, d.ignore)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.ignore = normalizedIgnores
|
||||
d.stream.Paths = nil
|
||||
for _, path := range pathsToWatch {
|
||||
for _, path := range watchPaths {
|
||||
d.addStreamPath(path)
|
||||
}
|
||||
|
||||
|
|
@ -138,19 +132,17 @@ func (d *fseventNotify) Errors() chan error {
|
|||
}
|
||||
|
||||
func (d *fseventNotify) shouldNotify(path string) bool {
|
||||
|
||||
if _, ok := d.pathsWereWatching[path]; ok {
|
||||
if d.notifyList[path] {
|
||||
stat, err := os.Lstat(path)
|
||||
isDir := err == nil && stat.IsDir()
|
||||
return !isDir
|
||||
}
|
||||
|
||||
for root := range d.pathsWereWatching {
|
||||
for root := range d.notifyList {
|
||||
if pathutil.IsChild(root, path) {
|
||||
if d.shouldIgnore(root, path) {
|
||||
return false
|
||||
if !d.shouldIgnore(root, path) {
|
||||
return true
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
|
|
@ -186,17 +178,21 @@ func newWatcher(paths []string, ignore map[string]PathMatcher) (Notify, error) {
|
|||
stop: make(chan struct{}),
|
||||
}
|
||||
|
||||
watchRoots := pathutil.EncompassingPaths(paths)
|
||||
notifyList, normalizedIgnores, err := normalizeWatchRoots(watchRoots, ignore)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("newWatcher: %w", err)
|
||||
}
|
||||
dw.ignore = normalizedIgnores
|
||||
dw.pathsWereWatching = make(map[string]any, len(notifyList))
|
||||
for path := range notifyList {
|
||||
dw.pathsWereWatching[path] = struct{}{}
|
||||
watchPaths := pathutil.EncompassingPaths(paths)
|
||||
|
||||
notifyList := make(map[string]bool, len(paths))
|
||||
for _, path := range paths {
|
||||
abs, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("newWatcher: %w", err)
|
||||
}
|
||||
notifyList[abs] = true
|
||||
}
|
||||
|
||||
dw.ignore = ignore
|
||||
dw.notifyList = notifyList
|
||||
dw.watchPaths = watchPaths
|
||||
|
||||
return dw, nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -28,8 +28,8 @@ import (
|
|||
|
||||
func newFseventNotifyFixture(repo string, ignore map[string]PathMatcher) *fseventNotify {
|
||||
return &fseventNotify{
|
||||
pathsWereWatching: map[string]any{repo: struct{}{}},
|
||||
ignore: ignore,
|
||||
notifyList: map[string]bool{repo: true},
|
||||
ignore: ignore,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -144,6 +144,47 @@ func TestFseventNotifyShouldNotifyIntersectMatcher(t *testing.T) {
|
|||
assert.Assert(t, !d2.shouldNotify(buildFile), "expected path ignored by every intersect matcher not to notify")
|
||||
}
|
||||
|
||||
func TestFseventNotifyShouldNotifyAnyRootSaysOK(t *testing.T) {
|
||||
repoRoot := t.TempDir()
|
||||
srcRoot := filepath.Join(repoRoot, "src")
|
||||
assert.NilError(t, os.MkdirAll(srcRoot, 0o755))
|
||||
|
||||
// Service A watches repoRoot and ignores the entire src/ subtree.
|
||||
// Service B watches repoRoot/src and ignores only node_modules/.
|
||||
// A path is notified if ANY containing root's matcher does not suppress it.
|
||||
parentIgnore, err := DockerIgnoreTesterFromContents(repoRoot, "src/\n")
|
||||
assert.NilError(t, err)
|
||||
childIgnore, err := DockerIgnoreTesterFromContents(srcRoot, "node_modules/\n")
|
||||
assert.NilError(t, err)
|
||||
|
||||
d := &fseventNotify{
|
||||
notifyList: map[string]bool{repoRoot: true, srcRoot: true},
|
||||
ignore: map[string]PathMatcher{
|
||||
repoRoot: parentIgnore,
|
||||
srcRoot: childIgnore,
|
||||
},
|
||||
}
|
||||
|
||||
// srcRoot does not ignore foo.ts, so it is notified even though repoRoot ignores src/.
|
||||
fooFile := filepath.Join(srcRoot, "foo.ts")
|
||||
assert.NilError(t, os.WriteFile(fooFile, []byte("x"), 0o644))
|
||||
assert.Assert(t, d.shouldNotify(fooFile),
|
||||
"file under child root must be notified; srcRoot does not ignore it even though repoRoot ignores src/")
|
||||
|
||||
// Every containing root ignores this path (repoRoot via src/, srcRoot via node_modules/).
|
||||
nodeModulesFile := filepath.Join(srcRoot, "node_modules", "dep.js")
|
||||
assert.NilError(t, os.MkdirAll(filepath.Dir(nodeModulesFile), 0o755))
|
||||
assert.NilError(t, os.WriteFile(nodeModulesFile, []byte("x"), 0o644))
|
||||
assert.Assert(t, !d.shouldNotify(nodeModulesFile),
|
||||
"node_modules file must not be notified; all containing roots ignore it")
|
||||
|
||||
// repoRoot does not ignore main.go (outside src/), so it is notified.
|
||||
otherFile := filepath.Join(repoRoot, "main.go")
|
||||
assert.NilError(t, os.WriteFile(otherFile, []byte("x"), 0o644))
|
||||
assert.Assert(t, d.shouldNotify(otherFile),
|
||||
"file outside src/ must be notified; repoRoot does not ignore it")
|
||||
}
|
||||
|
||||
func TestFseventNotifyShouldIgnoreDockerignoreDirectory(t *testing.T) {
|
||||
repo := t.TempDir()
|
||||
ignore, err := DockerIgnoreTesterFromContents(repo, "bazel-bin/\n!bazel-bin/app-binary\n")
|
||||
|
|
|
|||
|
|
@ -37,15 +37,15 @@ import (
|
|||
//
|
||||
// All OS-specific codepaths are handled by fsnotify.
|
||||
type naiveNotify struct {
|
||||
// Paths that we're watching that should be passed up to the caller.
|
||||
// Note that we may have to watch ancestors of these paths
|
||||
// in order to fulfill the API promise.
|
||||
// pathsToWatch are the actual paths registered with the OS watcher.
|
||||
pathsToWatch []string
|
||||
// notifyList holds the original trigger paths.
|
||||
//
|
||||
// We often need to check if paths are a child of a path in
|
||||
// the notify list. It might be better to store this in a tree
|
||||
// structure, so we can filter the list quickly.
|
||||
notifyList map[string]bool
|
||||
// ignore maps each notifyList root to the merged PathMatcher for paths under it.
|
||||
// ignore maps each trigger path (from notifyList) to its isolated PathMatcher.
|
||||
ignore map[string]PathMatcher
|
||||
|
||||
isWatcherRecursive bool
|
||||
|
|
@ -61,13 +61,7 @@ func (d *naiveNotify) Start() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
notifyRoots := make([]string, 0, len(d.notifyList))
|
||||
|
||||
for path := range d.notifyList {
|
||||
notifyRoots = append(notifyRoots, path)
|
||||
}
|
||||
|
||||
pathsToWatch, err := greatestExistingAncestors(notifyRoots, d.ignore)
|
||||
pathsToWatch, err := greatestExistingAncestors(d.pathsToWatch)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -75,11 +69,6 @@ func (d *naiveNotify) Start() error {
|
|||
pathsToWatch = pathutil.EncompassingPaths(pathsToWatch)
|
||||
}
|
||||
|
||||
_, d.ignore, err = normalizeWatchRoots(notifyRoots, d.ignore)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, name := range pathsToWatch {
|
||||
fi, err := os.Stat(name)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
|
|
@ -236,7 +225,9 @@ func (d *naiveNotify) shouldNotify(path string) bool {
|
|||
|
||||
for root := range d.notifyList {
|
||||
if pathutil.IsChild(root, path) {
|
||||
return !d.shouldIgnore(root, path)
|
||||
if !d.shouldIgnore(root, path) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
|
|
@ -249,11 +240,7 @@ func (d *naiveNotify) shouldSkipDir(path string) bool {
|
|||
}
|
||||
|
||||
// Only walk directories under a notifyList path or under an ancestor of one
|
||||
// (Start() may watch a parent when the target is missing or is a file).
|
||||
// Decide ancestor/descendant versus notifyList before applying ignores so one
|
||||
// root's patterns cannot block reaching another root.
|
||||
// When walking beneath a watched ancestor, prune subtrees only with that root's
|
||||
// matcher from d.ignore.
|
||||
// Ignore a path only if every parent root's ignore matcher agrees
|
||||
for root := range d.notifyList {
|
||||
if pathutil.IsChild(path, root) {
|
||||
return false
|
||||
|
|
@ -278,9 +265,6 @@ func (d *naiveNotify) shouldIgnoreEntireDir(dir, path string) bool {
|
|||
logrus.Debugf("error checking ignored directory %q: %v", path, err)
|
||||
return false
|
||||
}
|
||||
if matches {
|
||||
return true
|
||||
}
|
||||
return matches
|
||||
}
|
||||
return false
|
||||
|
|
@ -334,14 +318,19 @@ func newWatcher(paths []string, ignore map[string]PathMatcher) (Notify, error) {
|
|||
watchRoots = pathutil.EncompassingPaths(paths)
|
||||
}
|
||||
|
||||
notifyList, normalizedIgnores, err := normalizeWatchRoots(watchRoots, ignore)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("newWatcher: %w", err)
|
||||
notifyList := make(map[string]bool, len(paths))
|
||||
for _, path := range paths {
|
||||
abs, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("newWatcher: %w", err)
|
||||
}
|
||||
notifyList[abs] = true
|
||||
}
|
||||
|
||||
wmw := &naiveNotify{
|
||||
pathsToWatch: watchRoots,
|
||||
notifyList: notifyList,
|
||||
ignore: normalizedIgnores,
|
||||
ignore: ignore,
|
||||
watcher: fsw,
|
||||
events: fsw.Events,
|
||||
wrappedEvents: wrappedEvents,
|
||||
|
|
|
|||
|
|
@ -123,7 +123,8 @@ func inotifyNodes() (int, error) {
|
|||
pid := os.Getpid()
|
||||
|
||||
output, err := exec.Command("/bin/sh", "-c", fmt.Sprintf(
|
||||
"find /proc/%d/fd -lname anon_inode:inotify -printf '%%hinfo/%%f\n' | xargs cat | grep -c '^inotify'", pid)).Output()
|
||||
"find /proc/%d/fd -lname anon_inode:inotify -printf '%%hinfo/%%f\n' | xargs cat | grep -c '^inotify'", pid,
|
||||
)).Output()
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("error running command to determine number of watched files: %w\n %s", err, output)
|
||||
}
|
||||
|
|
@ -239,6 +240,92 @@ func TestShouldSkipDirDoesNotSkipAncestorOfWatchedPath(t *testing.T) {
|
|||
assert.Assert(t, !d.shouldSkipDir(parent), "expected parent directory to remain traversable when it contains a watched path")
|
||||
}
|
||||
|
||||
func TestShouldSkipDirRequiresAllContainingRootsToAgree(t *testing.T) {
|
||||
repoRoot := t.TempDir()
|
||||
srcRoot := filepath.Join(repoRoot, "src")
|
||||
|
||||
// Service A watches repoRoot but does NOT list node_modules in its ignores.
|
||||
// Service B watches src and ignores node_modules.
|
||||
// node_modules under src must NOT be skipped — because service A (which also
|
||||
// covers the path) has no rule for it. All containing roots must agree to skip.
|
||||
rootIgnore, err := DockerIgnoreTesterFromContents(repoRoot, "need_perm_dir/\n")
|
||||
assert.NilError(t, err)
|
||||
childIgnore, err := DockerIgnoreTesterFromContents(srcRoot, "node_modules/\n")
|
||||
assert.NilError(t, err)
|
||||
|
||||
d := &naiveNotify{
|
||||
ignore: map[string]PathMatcher{repoRoot: rootIgnore, srcRoot: childIgnore},
|
||||
notifyList: map[string]bool{repoRoot: true, srcRoot: true},
|
||||
}
|
||||
|
||||
nodeModulesDir := filepath.Join(srcRoot, "node_modules")
|
||||
assert.Assert(t, !d.shouldSkipDir(nodeModulesDir),
|
||||
"node_modules under child root must not be skipped when a containing root (repoRoot) has no matching ignore rule")
|
||||
|
||||
// A legitimate subdir under src is also not skipped.
|
||||
componentsDir := filepath.Join(srcRoot, "components")
|
||||
assert.Assert(t, !d.shouldSkipDir(componentsDir),
|
||||
"non-ignored directory under child root must remain watched")
|
||||
}
|
||||
|
||||
func TestShouldSkipDirNotVetoedByUnrelatedChildTrigger(t *testing.T) {
|
||||
repoRoot := t.TempDir()
|
||||
srcRoot := filepath.Join(repoRoot, "src")
|
||||
|
||||
// Service A watches repoRoot and ignores root-owned-dir/.
|
||||
// Service B watches repoRoot/src with an unrelated ignore.
|
||||
// root-owned-dir is outside src, so service B has no opinion about it.
|
||||
// The directory must still be skipped so the walker never enters it.
|
||||
rootIgnore, err := DockerIgnoreTesterFromContents(repoRoot, "root-owned-dir/\n")
|
||||
assert.NilError(t, err)
|
||||
childIgnore, err := DockerIgnoreTesterFromContents(srcRoot, "node_modules/\n")
|
||||
assert.NilError(t, err)
|
||||
|
||||
d := &naiveNotify{
|
||||
ignore: map[string]PathMatcher{repoRoot: rootIgnore, srcRoot: childIgnore},
|
||||
notifyList: map[string]bool{repoRoot: true, srcRoot: true},
|
||||
}
|
||||
|
||||
rootOwnedDir := filepath.Join(repoRoot, "root-owned-dir")
|
||||
assert.Assert(t, d.shouldSkipDir(rootOwnedDir),
|
||||
"root-owned-dir must be skipped; child trigger must not veto parent's ignore")
|
||||
}
|
||||
|
||||
func TestShouldNotifyAnyRootSaysOK(t *testing.T) {
|
||||
repoRoot := t.TempDir()
|
||||
srcRoot := filepath.Join(repoRoot, "src")
|
||||
|
||||
// Service A watches repoRoot and ignores the entire src/ subtree.
|
||||
// Service B watches repoRoot/src and ignores only node_modules/.
|
||||
// A path is notified if ANY containing root's matcher does not suppress it.
|
||||
parentIgnore, err := DockerIgnoreTesterFromContents(repoRoot, "src/\n")
|
||||
assert.NilError(t, err)
|
||||
childIgnore, err := DockerIgnoreTesterFromContents(srcRoot, "node_modules/\n")
|
||||
assert.NilError(t, err)
|
||||
|
||||
d := &naiveNotify{
|
||||
ignore: map[string]PathMatcher{repoRoot: parentIgnore, srcRoot: childIgnore},
|
||||
notifyList: map[string]bool{repoRoot: true, srcRoot: true},
|
||||
}
|
||||
|
||||
// A regular source file under src/ is notified because srcRoot's matcher does
|
||||
// not ignore it, even though repoRoot ignores all of src/.
|
||||
fooFile := filepath.Join(srcRoot, "foo.ts")
|
||||
assert.Assert(t, d.shouldNotify(fooFile),
|
||||
"file under child root must be notified; srcRoot does not ignore it even though repoRoot ignores src/")
|
||||
|
||||
// A file inside node_modules is not notified: every containing root ignores it
|
||||
// (repoRoot ignores src/, srcRoot ignores node_modules/).
|
||||
nodeModulesFile := filepath.Join(srcRoot, "node_modules", "dep.js")
|
||||
assert.Assert(t, !d.shouldNotify(nodeModulesFile),
|
||||
"node_modules file must not be notified; all containing roots ignore it")
|
||||
|
||||
// A file outside src/ is notified because repoRoot does not ignore it.
|
||||
otherFile := filepath.Join(repoRoot, "main.go")
|
||||
assert.Assert(t, d.shouldNotify(otherFile),
|
||||
"file outside src/ must be notified; repoRoot does not ignore it")
|
||||
}
|
||||
|
||||
func TestShouldSkipDirIntersectRequiresAllTriggersToAgree(t *testing.T) {
|
||||
repoRoot := t.TempDir()
|
||||
ignoreVendor, err := DockerIgnoreTesterFromContents(repoRoot, "vendor/\n")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue