@@ -23,12 +23,12 @@ import (
2323 "os"
2424 "path"
2525 "path/filepath"
26- "sort"
2726 "strconv"
2827 "strings"
2928 "time"
3029
3130 "github.com/compose-spec/compose-go/v2/types"
31+ ccli "github.com/docker/cli/cli/command/container"
3232 pathutil "github.com/docker/compose/v2/internal/paths"
3333 "github.com/docker/compose/v2/internal/sync"
3434 "github.com/docker/compose/v2/pkg/api"
@@ -48,7 +48,7 @@ const quietPeriod = 500 * time.Millisecond
4848// fileEvent contains the Compose service and modified host system path.
4949type fileEvent struct {
5050 sync.PathMapping
51- Action types.WatchAction
51+ Trigger types.Trigger
5252}
5353
5454// getSyncImplementation returns an appropriate sync implementation for the
@@ -298,7 +298,7 @@ func maybeFileEvent(trigger types.Trigger, hostPath string, ignore watch.PathMat
298298 }
299299
300300 return & fileEvent {
301- Action : trigger . Action ,
301+ Trigger : trigger ,
302302 PathMapping : sync.PathMapping {
303303 HostPath : hostPath ,
304304 ContainerPath : containerPath ,
@@ -338,6 +338,9 @@ func loadDevelopmentConfig(service types.ServiceConfig, project *types.Project)
338338 if trigger .Action == types .WatchActionRebuild && service .Build == nil {
339339 return nil , fmt .Errorf ("service %s doesn't have a build section, can't apply 'rebuild' on watch" , service .Name )
340340 }
341+ if trigger .Action == types .WatchActionSyncExec && len (trigger .Exec .Command ) == 0 {
342+ return nil , fmt .Errorf ("can't watch with action 'sync+exec' on service %s wihtout a command" , service .Name )
343+ }
341344
342345 config .Watch [i ] = trigger
343346 }
@@ -352,24 +355,17 @@ func batchDebounceEvents(ctx context.Context, clock clockwork.Clock, delay time.
352355 out := make (chan []fileEvent )
353356 go func () {
354357 defer close (out )
355- seen := make (map [fileEvent ]time. Time )
358+ seen := make (map [sync. PathMapping ] fileEvent )
356359 flushEvents := func () {
357360 if len (seen ) == 0 {
358361 return
359362 }
360363 events := make ([]fileEvent , 0 , len (seen ))
361- for e := range seen {
364+ for _ , e := range seen {
362365 events = append (events , e )
363366 }
364- // sort batch by oldest -> newest
365- // (if an event is seen > 1 per batch, it gets the latest timestamp)
366- sort .SliceStable (events , func (i , j int ) bool {
367- x := events [i ]
368- y := events [j ]
369- return seen [x ].Before (seen [y ])
370- })
371367 out <- events
372- seen = make (map [fileEvent ]time. Time )
368+ seen = make (map [sync. PathMapping ] fileEvent )
373369 }
374370
375371 t := clock .NewTicker (delay )
@@ -386,7 +382,7 @@ func batchDebounceEvents(ctx context.Context, clock clockwork.Clock, delay time.
386382 flushEvents ()
387383 return
388384 }
389- seen [e ] = time . Now ()
385+ seen [e . PathMapping ] = e
390386 t .Reset (delay )
391387 }
392388 }
@@ -485,7 +481,7 @@ func (s *composeService) handleWatchBatch(ctx context.Context, project *types.Pr
485481 pathMappings := make ([]sync.PathMapping , len (batch ))
486482 restartService := false
487483 for i := range batch {
488- if batch [i ].Action == types .WatchActionRebuild {
484+ if batch [i ].Trigger . Action == types .WatchActionRebuild {
489485 options .LogTo .Log (api .WatchLogger , fmt .Sprintf ("Rebuilding service %q after changes were detected..." , serviceName ))
490486 // restrict the build to ONLY this service, not any of its dependencies
491487 options .Build .Services = []string {serviceName }
@@ -527,7 +523,7 @@ func (s *composeService) handleWatchBatch(ctx context.Context, project *types.Pr
527523 }
528524 return nil
529525 }
530- if batch [i ].Action == types .WatchActionSyncRestart {
526+ if batch [i ].Trigger . Action == types .WatchActionSyncRestart {
531527 restartService = true
532528 }
533529 pathMappings [i ] = batch [i ].PathMapping
@@ -554,9 +550,34 @@ func (s *composeService) handleWatchBatch(ctx context.Context, project *types.Pr
554550 options .LogTo .Log (
555551 api .WatchLogger ,
556552 fmt .Sprintf ("service %q restarted" , serviceName ))
557-
558553 }
559- return nil
554+ eg , ctx := errgroup .WithContext (ctx )
555+ for _ , b := range batch {
556+ if b .Trigger .Action == "sync+exec" {
557+ containers , err := s .getContainers (ctx , project .Name , oneOffExclude , false , serviceName )
558+ if err != nil {
559+ return err
560+ }
561+ x := b .Trigger .Exec
562+ for _ , c := range containers {
563+ eg .Go (func () error {
564+ exec := ccli .NewExecOptions ()
565+ exec .User = x .User
566+ exec .Privileged = x .Privileged
567+ exec .Command = x .Command
568+ exec .Workdir = x .WorkingDir
569+ for _ , v := range x .Environment .ToMapping ().Values () {
570+ err := exec .Env .Set (v )
571+ if err != nil {
572+ return err
573+ }
574+ }
575+ return ccli .RunExec (ctx , s .dockerCli , c .ID , exec )
576+ })
577+ }
578+ }
579+ }
580+ return eg .Wait ()
560581}
561582
562583// writeWatchSyncMessage prints out a message about the sync for the changed paths.
0 commit comments