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
48 changes: 34 additions & 14 deletions pkg/compose/watch.go
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,11 @@ func getWatchRules(config *types.DevelopConfig, service types.ServiceConfig) ([]
return nil, err
}

dockerFileIgnore, err := dockerFileIgnoreMatcher(service)
if err != nil {
return nil, err
}

for _, trigger := range config.Watch {
ignore, err := watch.NewDockerPatternMatcher(trigger.Path, trigger.Ignore)
if err != nil {
Expand All @@ -352,15 +357,22 @@ func getWatchRules(config *types.DevelopConfig, service types.ServiceConfig) ([]
}
}

ignores := []watch.PathMatcher{
dockerIgnores,
watch.EphemeralPathMatcher(),
dotGitIgnore,
ignore,
}
// Copy actions only: rebuild on the same tree must still fire.
switch trigger.Action {
case types.WatchActionSync, types.WatchActionSyncRestart, types.WatchActionSyncExec:
ignores = append(ignores, dockerFileIgnore)
}

rules = append(rules, watchRule{
Trigger: trigger,
include: include,
ignore: watch.NewCompositeMatcher(
dockerIgnores,
watch.EphemeralPathMatcher(),
dotGitIgnore,
ignore,
),
ignore: watch.NewCompositeMatcher(ignores...),
service: service.Name,
})
}
Expand Down Expand Up @@ -760,6 +772,21 @@ func (s *composeService) pruneDanglingImagesOnRebuild(ctx context.Context, proje
}
}

// **/anchored so the matcher hits both initialSync's basenames and the
// watch loop's absolute host paths.
func dockerFileIgnoreMatcher(service types.ServiceConfig) (watch.PathMatcher, error) {
names := append([]string{"Dockerfile"}, cli.DefaultFileNames...)
names = append(names, cli.DefaultOverrideFileNames...)
if service.Build != nil && service.Build.Dockerfile != "" {
names = append(names, filepath.Base(service.Build.Dockerfile))
}
patterns := make([]string, len(names))
for i, name := range names {
patterns[i] = "**/" + name
}
return watch.NewDockerPatternMatcher("/", patterns)
}

// Walks develop.watch.path and checks which files should be copied inside the container
// ignores develop.watch.ignore, Dockerfile, compose files, bind mounted paths and .git
func (s *composeService) initialSync(ctx context.Context, service types.ServiceConfig, trigger types.Trigger, syncer sync.Syncer) error {
Expand All @@ -778,14 +805,7 @@ func (s *composeService) initialSync(ctx context.Context, service types.ServiceC
return err
}

// also exclude override compose files and any custom-named Dockerfile
dockerFilePatterns := append([]string{"Dockerfile"}, cli.DefaultFileNames...)
dockerFilePatterns = append(dockerFilePatterns, cli.DefaultOverrideFileNames...)
if service.Build != nil && service.Build.Dockerfile != "" {
dockerFilePatterns = append(dockerFilePatterns, filepath.Base(service.Build.Dockerfile))
}

dockerFileIgnore, err := watch.NewDockerPatternMatcher("/", dockerFilePatterns)
dockerFileIgnore, err := dockerFileIgnoreMatcher(service)
if err != nil {
return err
}
Expand Down
61 changes: 61 additions & 0 deletions pkg/compose/watch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,67 @@ func TestInitialSync_ExcludesNestedCustomNamedDockerfile(t *testing.T) {
}})
}

// getWatchRules historically never excluded Dockerfile/compose files (see
// #14117). The continuous loop matches absolute host paths, so the same
// basename-only matcher initialSync used would miss them.
func TestGetWatchRules_ExcludesDockerfileAndComposeFilesFromSync(t *testing.T) {
rules, err := getWatchRules(&types.DevelopConfig{
Watch: []types.Trigger{{
Path: "/proj",
Action: types.WatchActionSync,
Target: "/app",
}},
}, types.ServiceConfig{Name: "svc"})
assert.NilError(t, err)
assert.Equal(t, 1, len(rules))

for _, name := range []string{"Dockerfile", "compose.yaml", "docker-compose.yml", "compose.override.yml"} {
assert.Assert(t, rules[0].Matches(watch.NewFileEvent("/proj/"+name)) == nil, name)
}

got := rules[0].Matches(watch.NewFileEvent("/proj/app.go"))
assert.DeepEqual(t, got, &sync.PathMapping{
HostPath: "/proj/app.go",
ContainerPath: "/app/app.go",
})
}

func TestGetWatchRules_ExcludesCustomNamedDockerfileFromSync(t *testing.T) {
rules, err := getWatchRules(&types.DevelopConfig{
Watch: []types.Trigger{{
Path: "/proj",
Action: types.WatchActionSync,
Target: "/app",
}},
}, types.ServiceConfig{
Name: "svc",
Build: &types.BuildConfig{Context: t.TempDir(), Dockerfile: "docker/Dockerfile.prod"},
})
assert.NilError(t, err)
assert.Assert(t, rules[0].Matches(watch.NewFileEvent("/proj/docker/Dockerfile.prod")) == nil)
assert.Assert(t, rules[0].Matches(watch.NewFileEvent("/proj/app.go")) != nil)
}

func TestGetWatchRules_CopyActionsExcludeDockerfile(t *testing.T) {
rules, err := getWatchRules(&types.DevelopConfig{
Watch: []types.Trigger{
{Path: "/proj", Action: types.WatchActionSync, Target: "/app"},
{Path: "/proj", Action: types.WatchActionRebuild},
{Path: "/proj", Action: types.WatchActionSyncExec, Target: "/app"},
},
}, types.ServiceConfig{
Name: "svc",
Build: &types.BuildConfig{Context: t.TempDir()},
})
assert.NilError(t, err)
assert.Equal(t, 3, len(rules))

event := watch.NewFileEvent("/proj/Dockerfile")
assert.Assert(t, rules[0].Matches(event) == nil)
assert.Assert(t, rules[1].Matches(event) != nil)
assert.Assert(t, rules[2].Matches(event) == nil)
}

// TestPruneDanglingImagesOnRebuild verifies the post-rebuild prune only
// removes superseded dangling images: a dangling image whose ID matches one
// of the freshly built images must be spared. The lookup used to probe the
Expand Down