diff --git a/pkg/compose/watch.go b/pkg/compose/watch.go index 8b53583d2e3..6b0588c1afa 100644 --- a/pkg/compose/watch.go +++ b/pkg/compose/watch.go @@ -22,14 +22,15 @@ import ( "time" "github.com/compose-spec/compose-go/types" - "github.com/docker/compose/v2/pkg/api" - "github.com/docker/compose/v2/pkg/utils" - "github.com/docker/compose/v2/pkg/watch" "github.com/jonboulle/clockwork" "github.com/mitchellh/mapstructure" "github.com/pkg/errors" "github.com/sirupsen/logrus" "golang.org/x/sync/errgroup" + + "github.com/docker/compose/v2/pkg/api" + "github.com/docker/compose/v2/pkg/utils" + "github.com/docker/compose/v2/pkg/watch" ) type DevelopmentConfig struct { @@ -82,10 +83,23 @@ func (s *composeService) Watch(ctx context.Context, project *types.Project, serv } bc := service.Build.Context - ignore, err := watch.LoadDockerIgnore(bc) + dockerIgnores, err := watch.LoadDockerIgnore(bc) + if err != nil { + return err + } + + // add a hardcoded set of ignores on top of what came from .dockerignore + // some of this should likely be configurable (e.g. there could be cases + // where you want `.git` to be synced) but this is suitable for now + dotGitIgnore, err := watch.NewDockerPatternMatcher("/", []string{".git/"}) if err != nil { return err } + ignore := watch.NewCompositeMatcher( + dockerIgnores, + watch.EphemeralPathMatcher, + dotGitIgnore, + ) watcher, err := watch.NewWatcher([]string{bc}, ignore) if err != nil { @@ -109,7 +123,7 @@ func (s *composeService) Watch(ctx context.Context, project *types.Project, serv path := event.Path() for _, trigger := range config.Watch { - logrus.Debugf("change deteced on %s - comparing with %s", path, trigger.Path) + logrus.Debugf("change detected on %s - comparing with %s", path, trigger.Path) if watch.IsChild(trigger.Path, path) { fmt.Fprintf(s.stderr(), "change detected on %s\n", path) @@ -126,7 +140,7 @@ func (s *composeService) Watch(ctx context.Context, project *types.Project, serv Destination: fmt.Sprintf("%s:%s", name, dest), } case WatchActionRebuild: - logrus.Debugf("modified file %s require image to be rebuilt", path) + logrus.Debugf("modified file %s requires image to be rebuilt", path) needRebuild <- name default: return fmt.Errorf("watch action %q is not supported", trigger) @@ -176,7 +190,7 @@ func (s *composeService) makeRebuildFn(ctx context.Context, project *types.Proje Services: services, }) if err != nil { - fmt.Fprintf(s.stderr(), "Build failed") + fmt.Fprintf(s.stderr(), "Build failed\n") } for i, service := range project.Services { if id, ok := imageIds[service.Name]; ok { @@ -196,7 +210,7 @@ func (s *composeService) makeRebuildFn(ctx context.Context, project *types.Proje }, }) if err != nil { - fmt.Fprintf(s.stderr(), "Application failed to start after update") + fmt.Fprintf(s.stderr(), "Application failed to start after update\n") } } } @@ -212,7 +226,7 @@ func (s *composeService) makeSyncFn(ctx context.Context, project *types.Project, if err != nil { return err } - fmt.Fprintf(s.stderr(), "%s updated\n", opt.Source) + fmt.Fprintf(s.stderr(), "%s updated\n", opt.Destination) } } } diff --git a/pkg/watch/ephemeral.go b/pkg/watch/ephemeral.go new file mode 100644 index 00000000000..2aa271503c1 --- /dev/null +++ b/pkg/watch/ephemeral.go @@ -0,0 +1,60 @@ +/* + Copyright 2020 Docker Compose CLI authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package watch + +// EphemeralPathMatcher filters out spurious changes that we don't want to +// rebuild on, like IDE temp/lock files. +// +// This isn't an ideal solution. In an ideal world, the user would put +// everything to ignore in their tiltignore/dockerignore files. This is a +// stop-gap so they don't have a terrible experience if those files aren't +// there or aren't in the right places. +// +// https://app.clubhouse.io/windmill/story/691/filter-out-ephemeral-file-changes +var EphemeralPathMatcher = initEphemeralPathMatcher() + +func initEphemeralPathMatcher() PathMatcher { + golandPatterns := []string{"**/*___jb_old___", "**/*___jb_tmp___", "**/.idea/**"} + emacsPatterns := []string{"**/.#*", "**/#*#"} + // if .swp is taken (presumably because multiple vims are running in that dir), + // vim will go with .swo, .swn, etc, and then even .svz, .svy! + // https://github.com/vim/vim/blob/ea781459b9617aa47335061fcc78403495260315/src/memline.c#L5076 + // ignoring .sw? seems dangerous, since things like .swf or .swi exist, but ignoring the first few + // seems safe and should catch most cases + vimPatterns := []string{"**/4913", "**/*~", "**/.*.swp", "**/.*.swx", "**/.*.swo", "**/.*.swn"} + // kate (the default text editor for KDE) uses a file similar to Vim's .swp + // files, but it doesn't have the "incrememnting" character problem mentioned + // above + katePatterns := []string{"**/.*.kate-swp"} + // go stdlib creates tmpfiles to determine umask for setting permissions + // during file creation; they are then immediately deleted + // https://github.com/golang/go/blob/0b5218cf4e3e5c17344ea113af346e8e0836f6c4/src/cmd/go/internal/work/exec.go#L1764 + goPatterns := []string{"**/*-go-tmp-umask"} + + var allPatterns []string + allPatterns = append(allPatterns, golandPatterns...) + allPatterns = append(allPatterns, emacsPatterns...) + allPatterns = append(allPatterns, vimPatterns...) + allPatterns = append(allPatterns, katePatterns...) + allPatterns = append(allPatterns, goPatterns...) + + matcher, err := NewDockerPatternMatcher("/", allPatterns) + if err != nil { + panic(err) + } + return matcher +} diff --git a/pkg/watch/notify.go b/pkg/watch/notify.go index f09ac98f73b..3a6398a0e53 100644 --- a/pkg/watch/notify.go +++ b/pkg/watch/notify.go @@ -106,3 +106,39 @@ func DesiredWindowsBufferSize() int { func IsWindowsShortReadError(err error) bool { return runtime.GOOS == "windows" && !errors.Is(err, fsnotify.ErrEventOverflow) } + +type CompositePathMatcher struct { + Matchers []PathMatcher +} + +func NewCompositeMatcher(matchers ...PathMatcher) PathMatcher { + if len(matchers) == 0 { + return EmptyMatcher{} + } + return CompositePathMatcher{Matchers: matchers} +} + +func (c CompositePathMatcher) Matches(f string) (bool, error) { + for _, t := range c.Matchers { + ret, err := t.Matches(f) + if err != nil { + return false, err + } + if ret { + return true, nil + } + } + return false, nil +} + +func (c CompositePathMatcher) MatchesEntireDir(f string) (bool, error) { + for _, t := range c.Matchers { + matches, err := t.MatchesEntireDir(f) + if matches || err != nil { + return matches, err + } + } + return false, nil +} + +var _ PathMatcher = CompositePathMatcher{} diff --git a/pkg/watch/watcher_darwin.go b/pkg/watch/watcher_darwin.go index 9329a294b78..558134555b2 100644 --- a/pkg/watch/watcher_darwin.go +++ b/pkg/watch/watcher_darwin.go @@ -20,7 +20,6 @@ package watch import ( - "fmt" "path/filepath" "time" @@ -53,7 +52,6 @@ func (d *fseventNotify) loop() { } for _, e := range events { - fmt.Println(e) e.Path = filepath.Join("/", e.Path) _, isPathWereWatching := d.pathsWereWatching[e.Path] @@ -67,6 +65,7 @@ func (d *fseventNotify) loop() { if err != nil { logrus.Infof("Error matching path %q: %v", e.Path, err) } else if ignore { + logrus.Tracef("Ignoring event for path: %v", e.Path) continue } diff --git a/pkg/watch/watcher_naive.go b/pkg/watch/watcher_naive.go index 60056df1a9e..e8c7e855f5b 100644 --- a/pkg/watch/watcher_naive.go +++ b/pkg/watch/watcher_naive.go @@ -129,6 +129,7 @@ func (d *naiveNotify) watchRecursively(dir string) error { } if shouldSkipDir { + logrus.Debugf("Ignoring directory and its contents (recursively): %s", path) return filepath.SkipDir } @@ -234,6 +235,7 @@ func (d *naiveNotify) shouldNotify(path string) bool { if err != nil { logrus.Infof("Error matching path %q: %v", path, err) } else if ignore { + logrus.Tracef("Ignoring event for path: %v", path) return false }