From 68ab763674902cb651ff06bf72ea6800b3a360e0 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof Date: Mon, 17 Aug 2026 10:31:48 +0200 Subject: [PATCH] refactor: flatten watch trigger preparation and initial sync walk watch (cognitive complexity 52) inlined three passes over each service's triggers; they become prepareRebuildTriggers (validation + always-build marking), watchTriggerPaths (paths to monitor, bind-mount skips, initial sync) and initialSyncRequested (the DEPRECATED x-initialSync fallback). initialSyncFiles (34) delegates its WalkDir closure to initialSyncDirectory, keeping the Info() call order so error behavior on unreadable entries is unchanged. No behavior change: same validations, same warnings, same collected paths and sync mappings. Co-Authored-By: Claude Fable 5 Signed-off-by: Nicolas De Loof --- pkg/compose/watch.go | 200 ++++++++++++++++++++++++------------------- 1 file changed, 114 insertions(+), 86 deletions(-) diff --git a/pkg/compose/watch.go b/pkg/compose/watch.go index 94d518c19..e75538364 100644 --- a/pkg/compose/watch.go +++ b/pkg/compose/watch.go @@ -186,9 +186,6 @@ func (r watchRule) Matches(event watch.FileEvent) *sync.PathMapping { } } -// FIXME(ndeloof) complete migration to gocognit -// -//nolint:gocognit func (s *composeService) watch(ctx context.Context, project *types.Project, options api.WatchOptions) (func() error, error) { var err error if project, err = project.WithSelectedServices(options.Services); err != nil { @@ -218,47 +215,16 @@ func (s *composeService) watch(ctx context.Context, project *types.Project, opti continue } - for _, trigger := range config.Watch { - if trigger.Action == types.WatchActionRebuild { - if service.Build == nil { - return nil, fmt.Errorf("can't watch service %q with action %s without a build context", service.Name, types.WatchActionRebuild) - } - if options.Build == nil { - return nil, fmt.Errorf("--no-build is incompatible with watch action %s in service %s", types.WatchActionRebuild, service.Name) - } - // set the service to always be built - watch triggers `Up()` when it receives a rebuild event - service.PullPolicy = types.PullPolicyBuild - project.Services[serviceName] = service - } + service, err = prepareRebuildTriggers(project, serviceName, service, config, options) + if err != nil { + return nil, err } - for _, trigger := range config.Watch { - if isSync(trigger) && checkIfPathAlreadyBindMounted(trigger.Path, service.Volumes) { - logrus.Warnf("path '%s' also declared by a bind mount volume, this path won't be monitored!\n", trigger.Path) - continue - } else { - shouldInitialSync := trigger.InitialSync - - // Check legacy extension attribute for backward compatibility - if !shouldInitialSync { - var legacyInitialSync bool - success, err := trigger.Extensions.Get("x-initialSync", &legacyInitialSync) - if err == nil && success && legacyInitialSync { - shouldInitialSync = true - logrus.Warnf("x-initialSync is DEPRECATED, please use the official `initial_sync` attribute\n") - } - } - - if shouldInitialSync && isSync(trigger) { - // Need to check that initial files meant to be synced from the watch action are in the container - err := s.initialSync(ctx, project, service, trigger, syncer) - if err != nil { - return nil, err - } - } - } - paths = append(paths, trigger.Path) + triggerPaths, err := s.watchTriggerPaths(ctx, project, service, config, syncer) + if err != nil { + return nil, err } + paths = append(paths, triggerPaths...) serviceWatchRules, err := getWatchRules(config, service) if err != nil { @@ -295,6 +261,63 @@ func (s *composeService) watch(ctx context.Context, project *types.Project, opti }, nil } +// prepareRebuildTriggers validates rebuild watch actions and marks the +// service to always be built — watch triggers `Up()` when it receives a +// rebuild event. It returns the possibly-updated service config. +func prepareRebuildTriggers(project *types.Project, serviceName string, service types.ServiceConfig, config *types.DevelopConfig, options api.WatchOptions) (types.ServiceConfig, error) { + for _, trigger := range config.Watch { + if trigger.Action != types.WatchActionRebuild { + continue + } + if service.Build == nil { + return service, fmt.Errorf("can't watch service %q with action %s without a build context", service.Name, types.WatchActionRebuild) + } + if options.Build == nil { + return service, fmt.Errorf("--no-build is incompatible with watch action %s in service %s", types.WatchActionRebuild, service.Name) + } + service.PullPolicy = types.PullPolicyBuild + project.Services[serviceName] = service + } + return service, nil +} + +// watchTriggerPaths collects the paths to monitor for a service, skipping +// (with a warning) those already covered by a bind mount volume, and runs the +// initial sync for sync triggers requesting one. +func (s *composeService) watchTriggerPaths(ctx context.Context, project *types.Project, service types.ServiceConfig, config *types.DevelopConfig, syncer sync.Syncer) ([]string, error) { + var paths []string + for _, trigger := range config.Watch { + if isSync(trigger) && checkIfPathAlreadyBindMounted(trigger.Path, service.Volumes) { + logrus.Warnf("path '%s' also declared by a bind mount volume, this path won't be monitored!\n", trigger.Path) + continue + } + if initialSyncRequested(trigger) && isSync(trigger) { + // Need to check that initial files meant to be synced from the watch action are in the container + err := s.initialSync(ctx, project, service, trigger, syncer) + if err != nil { + return nil, err + } + } + paths = append(paths, trigger.Path) + } + return paths, nil +} + +// initialSyncRequested tells whether a sync trigger requests an initial sync, +// honoring the DEPRECATED x-initialSync extension attribute +func initialSyncRequested(trigger types.Trigger) bool { + if trigger.InitialSync { + return true + } + var legacyInitialSync bool + success, err := trigger.Extensions.Get("x-initialSync", &legacyInitialSync) + if err == nil && success && legacyInitialSync { + logrus.Warnf("x-initialSync is DEPRECATED, please use the official `initial_sync` attribute\n") + return true + } + return false +} + func getWatchRules(config *types.DevelopConfig, service types.ServiceConfig) ([]watchRule, error) { var rules []watchRule @@ -777,10 +800,6 @@ func (s *composeService) initialSync(ctx context.Context, project *types.Project } // Syncs files from develop.watch.path if they have been modified after the image has been created -// -// FIXME(ndeloof) complete migration to gocognit -// -//nolint:gocognit func (s *composeService) initialSyncFiles(ctx context.Context, project *types.Project, service types.ServiceConfig, trigger types.Trigger, ignore watch.PathMatcher) ([]*sync.PathMapping, error) { fi, err := os.Stat(trigger.Path) if err != nil { @@ -790,57 +809,66 @@ func (s *composeService) initialSyncFiles(ctx context.Context, project *types.Pr if err != nil { return nil, err } - var pathsToCopy []*sync.PathMapping switch mode := fi.Mode(); { case mode.IsDir(): // process directory - err = filepath.WalkDir(trigger.Path, func(path string, d fs.DirEntry, err error) error { - if err != nil { - // handle possible path err, just in case... - return err - } - if trigger.Path == path { - // walk starts at the root directory - return nil - } - if shouldIgnore(filepath.Base(path), ignore) || checkIfPathAlreadyBindMounted(path, service.Volumes) { - // By definition sync ignores bind mounted paths - if d.IsDir() { - // skip folder - return fs.SkipDir - } - return nil // skip file - } - info, err := d.Info() - if err != nil { - return err - } - if !d.IsDir() { - if info.ModTime().Before(timeImageCreated) { - // skip file if it was modified before image creation - return nil - } - rel, err := filepath.Rel(trigger.Path, path) - if err != nil { - return err - } - // only copy files (and not full directories) - pathsToCopy = append(pathsToCopy, &sync.PathMapping{ - HostPath: path, - ContainerPath: filepath.Join(trigger.Target, rel), - }) - } - return nil - }) + return initialSyncDirectory(trigger, service, ignore, timeImageCreated) case mode.IsRegular(): // process file if fi.ModTime().After(timeImageCreated) && !shouldIgnore(filepath.Base(trigger.Path), ignore) && !checkIfPathAlreadyBindMounted(trigger.Path, service.Volumes) { - pathsToCopy = append(pathsToCopy, &sync.PathMapping{ + return []*sync.PathMapping{{ HostPath: trigger.Path, ContainerPath: trigger.Target, - }) + }}, nil } } + return nil, nil +} + +// initialSyncDirectory collects the files of a watched directory which were +// modified after the image was created, skipping ignored and bind-mounted +// paths +func initialSyncDirectory(trigger types.Trigger, service types.ServiceConfig, ignore watch.PathMatcher, timeImageCreated time.Time) ([]*sync.PathMapping, error) { + var pathsToCopy []*sync.PathMapping + err := filepath.WalkDir(trigger.Path, func(path string, d fs.DirEntry, err error) error { + if err != nil { + // handle possible path err, just in case... + return err + } + if trigger.Path == path { + // walk starts at the root directory + return nil + } + if shouldIgnore(filepath.Base(path), ignore) || checkIfPathAlreadyBindMounted(path, service.Volumes) { + // By definition sync ignores bind mounted paths + if d.IsDir() { + // skip folder + return fs.SkipDir + } + return nil // skip file + } + info, err := d.Info() + if err != nil { + return err + } + if d.IsDir() { + return nil + } + if info.ModTime().Before(timeImageCreated) { + // skip file if it was modified before image creation + return nil + } + rel, err := filepath.Rel(trigger.Path, path) + if err != nil { + return err + } + // only copy files (and not full directories) + pathsToCopy = append(pathsToCopy, &sync.PathMapping{ + HostPath: path, + ContainerPath: filepath.Join(trigger.Target, rel), + }) + return nil + }) return pathsToCopy, err }