|  | @@ -17,7 +17,7 @@ package compose
 | 
	
		
			
				|  |  |  import (
 | 
	
		
			
				|  |  |  	"context"
 | 
	
		
			
				|  |  |  	"fmt"
 | 
	
		
			
				|  |  | -	"log"
 | 
	
		
			
				|  |  | +	"path/filepath"
 | 
	
		
			
				|  |  |  	"strings"
 | 
	
		
			
				|  |  |  	"time"
 | 
	
		
			
				|  |  |  
 | 
	
	
		
			
				|  | @@ -32,56 +32,29 @@ import (
 | 
	
		
			
				|  |  |  )
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  type DevelopmentConfig struct {
 | 
	
		
			
				|  |  | +	Sync     map[string]string `json:"sync,omitempty"`
 | 
	
		
			
				|  |  | +	Excludes []string          `json:"excludes,omitempty"`
 | 
	
		
			
				|  |  |  }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  const quietPeriod = 2 * time.Second
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  func (s *composeService) Watch(ctx context.Context, project *types.Project, services []string, options api.WatchOptions) error {
 | 
	
		
			
				|  |  | -	fmt.Fprintln(s.stderr(), "not implemented yet")
 | 
	
		
			
				|  |  | +	needRebuild := make(chan string)
 | 
	
		
			
				|  |  | +	needSync := make(chan api.CopyOptions, 5)
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  	eg, ctx := errgroup.WithContext(ctx)
 | 
	
		
			
				|  |  | -	needRefresh := make(chan string)
 | 
	
		
			
				|  |  |  	eg.Go(func() error {
 | 
	
		
			
				|  |  |  		clock := clockwork.NewRealClock()
 | 
	
		
			
				|  |  | -		debounce(ctx, clock, quietPeriod, needRefresh, func(services []string) {
 | 
	
		
			
				|  |  | -			fmt.Fprintf(s.stderr(), "Updating %s after changes were detected\n", strings.Join(services, ", "))
 | 
	
		
			
				|  |  | -			imageIds, err := s.build(ctx, project, api.BuildOptions{
 | 
	
		
			
				|  |  | -				Services: services,
 | 
	
		
			
				|  |  | -			})
 | 
	
		
			
				|  |  | -			if err != nil {
 | 
	
		
			
				|  |  | -				fmt.Fprintf(s.stderr(), "Build failed")
 | 
	
		
			
				|  |  | -			}
 | 
	
		
			
				|  |  | -			for i, service := range project.Services {
 | 
	
		
			
				|  |  | -				if id, ok := imageIds[service.Name]; ok {
 | 
	
		
			
				|  |  | -					service.Image = id
 | 
	
		
			
				|  |  | -				}
 | 
	
		
			
				|  |  | -				project.Services[i] = service
 | 
	
		
			
				|  |  | -			}
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -			err = s.Up(ctx, project, api.UpOptions{
 | 
	
		
			
				|  |  | -				Create: api.CreateOptions{
 | 
	
		
			
				|  |  | -					Services: services,
 | 
	
		
			
				|  |  | -					Inherit:  true,
 | 
	
		
			
				|  |  | -				},
 | 
	
		
			
				|  |  | -				Start: api.StartOptions{
 | 
	
		
			
				|  |  | -					Services: services,
 | 
	
		
			
				|  |  | -					Project:  project,
 | 
	
		
			
				|  |  | -				},
 | 
	
		
			
				|  |  | -			})
 | 
	
		
			
				|  |  | -			if err != nil {
 | 
	
		
			
				|  |  | -				fmt.Fprintf(s.stderr(), "Application failed to start after update")
 | 
	
		
			
				|  |  | -			}
 | 
	
		
			
				|  |  | -		})
 | 
	
		
			
				|  |  | +		debounce(ctx, clock, quietPeriod, needRebuild, s.makeRebuildFn(ctx, project))
 | 
	
		
			
				|  |  |  		return nil
 | 
	
		
			
				|  |  |  	})
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +	eg.Go(s.makeSyncFn(ctx, project, needSync))
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |  	err := project.WithServices(services, func(service types.ServiceConfig) error {
 | 
	
		
			
				|  |  | -		var config DevelopmentConfig
 | 
	
		
			
				|  |  | -		if y, ok := service.Extensions["x-develop"]; ok {
 | 
	
		
			
				|  |  | -			err := mapstructure.Decode(y, &config)
 | 
	
		
			
				|  |  | -			if err != nil {
 | 
	
		
			
				|  |  | -				return err
 | 
	
		
			
				|  |  | -			}
 | 
	
		
			
				|  |  | +		config, err := loadDevelopmentConfig(service, project)
 | 
	
		
			
				|  |  | +		if err != nil {
 | 
	
		
			
				|  |  | +			return err
 | 
	
		
			
				|  |  |  		}
 | 
	
		
			
				|  |  |  		if service.Build == nil {
 | 
	
		
			
				|  |  |  			return errors.New("can't watch a service without a build section")
 | 
	
	
		
			
				|  | @@ -98,7 +71,7 @@ func (s *composeService) Watch(ctx context.Context, project *types.Project, serv
 | 
	
		
			
				|  |  |  			return err
 | 
	
		
			
				|  |  |  		}
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -		fmt.Println("watching " + context)
 | 
	
		
			
				|  |  | +		fmt.Fprintf(s.stderr(), "watching %s\n", context)
 | 
	
		
			
				|  |  |  		err = watcher.Start()
 | 
	
		
			
				|  |  |  		if err != nil {
 | 
	
		
			
				|  |  |  			return err
 | 
	
	
		
			
				|  | @@ -106,13 +79,32 @@ func (s *composeService) Watch(ctx context.Context, project *types.Project, serv
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  		eg.Go(func() error {
 | 
	
		
			
				|  |  |  			defer watcher.Close() //nolint:errcheck
 | 
	
		
			
				|  |  | +		WATCH:
 | 
	
		
			
				|  |  |  			for {
 | 
	
		
			
				|  |  |  				select {
 | 
	
		
			
				|  |  |  				case <-ctx.Done():
 | 
	
		
			
				|  |  |  					return nil
 | 
	
		
			
				|  |  |  				case event := <-watcher.Events():
 | 
	
		
			
				|  |  | -					log.Println("fs event :", event.Path())
 | 
	
		
			
				|  |  | -					needRefresh <- service.Name
 | 
	
		
			
				|  |  | +					fmt.Fprintf(s.stderr(), "change detected on %s\n", event.Path())
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +					for src, dest := range config.Sync {
 | 
	
		
			
				|  |  | +						path := filepath.Clean(event.Path())
 | 
	
		
			
				|  |  | +						src = filepath.Clean(src)
 | 
	
		
			
				|  |  | +						if watch.IsChild(path, src) {
 | 
	
		
			
				|  |  | +							rel, err := filepath.Rel(src, path)
 | 
	
		
			
				|  |  | +							if err != nil {
 | 
	
		
			
				|  |  | +								return err
 | 
	
		
			
				|  |  | +							}
 | 
	
		
			
				|  |  | +							dest = filepath.Join(dest, rel)
 | 
	
		
			
				|  |  | +							needSync <- api.CopyOptions{
 | 
	
		
			
				|  |  | +								Source:      path,
 | 
	
		
			
				|  |  | +								Destination: fmt.Sprintf("%s:%s", service.Name, dest),
 | 
	
		
			
				|  |  | +							}
 | 
	
		
			
				|  |  | +							continue WATCH
 | 
	
		
			
				|  |  | +						}
 | 
	
		
			
				|  |  | +					}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +					needRebuild <- service.Name
 | 
	
		
			
				|  |  |  				case err := <-watcher.Errors():
 | 
	
		
			
				|  |  |  					return err
 | 
	
		
			
				|  |  |  				}
 | 
	
	
		
			
				|  | @@ -127,6 +119,73 @@ func (s *composeService) Watch(ctx context.Context, project *types.Project, serv
 | 
	
		
			
				|  |  |  	return eg.Wait()
 | 
	
		
			
				|  |  |  }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +func loadDevelopmentConfig(service types.ServiceConfig, project *types.Project) (DevelopmentConfig, error) {
 | 
	
		
			
				|  |  | +	var config DevelopmentConfig
 | 
	
		
			
				|  |  | +	if y, ok := service.Extensions["x-develop"]; ok {
 | 
	
		
			
				|  |  | +		err := mapstructure.Decode(y, &config)
 | 
	
		
			
				|  |  | +		if err != nil {
 | 
	
		
			
				|  |  | +			return DevelopmentConfig{}, err
 | 
	
		
			
				|  |  | +		}
 | 
	
		
			
				|  |  | +		for src, dest := range config.Sync {
 | 
	
		
			
				|  |  | +			if !filepath.IsAbs(src) {
 | 
	
		
			
				|  |  | +				delete(config.Sync, src)
 | 
	
		
			
				|  |  | +				src = filepath.Join(project.WorkingDir, src)
 | 
	
		
			
				|  |  | +				config.Sync[src] = dest
 | 
	
		
			
				|  |  | +			}
 | 
	
		
			
				|  |  | +		}
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +	return config, nil
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +func (s *composeService) makeRebuildFn(ctx context.Context, project *types.Project) func(services []string) {
 | 
	
		
			
				|  |  | +	return func(services []string) {
 | 
	
		
			
				|  |  | +		fmt.Fprintf(s.stderr(), "Updating %s after changes were detected\n", strings.Join(services, ", "))
 | 
	
		
			
				|  |  | +		imageIds, err := s.build(ctx, project, api.BuildOptions{
 | 
	
		
			
				|  |  | +			Services: services,
 | 
	
		
			
				|  |  | +		})
 | 
	
		
			
				|  |  | +		if err != nil {
 | 
	
		
			
				|  |  | +			fmt.Fprintf(s.stderr(), "Build failed")
 | 
	
		
			
				|  |  | +		}
 | 
	
		
			
				|  |  | +		for i, service := range project.Services {
 | 
	
		
			
				|  |  | +			if id, ok := imageIds[service.Name]; ok {
 | 
	
		
			
				|  |  | +				service.Image = id
 | 
	
		
			
				|  |  | +			}
 | 
	
		
			
				|  |  | +			project.Services[i] = service
 | 
	
		
			
				|  |  | +		}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +		err = s.Up(ctx, project, api.UpOptions{
 | 
	
		
			
				|  |  | +			Create: api.CreateOptions{
 | 
	
		
			
				|  |  | +				Services: services,
 | 
	
		
			
				|  |  | +				Inherit:  true,
 | 
	
		
			
				|  |  | +			},
 | 
	
		
			
				|  |  | +			Start: api.StartOptions{
 | 
	
		
			
				|  |  | +				Services: services,
 | 
	
		
			
				|  |  | +				Project:  project,
 | 
	
		
			
				|  |  | +			},
 | 
	
		
			
				|  |  | +		})
 | 
	
		
			
				|  |  | +		if err != nil {
 | 
	
		
			
				|  |  | +			fmt.Fprintf(s.stderr(), "Application failed to start after update")
 | 
	
		
			
				|  |  | +		}
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +func (s *composeService) makeSyncFn(ctx context.Context, project *types.Project, needSync chan api.CopyOptions) func() error {
 | 
	
		
			
				|  |  | +	return func() error {
 | 
	
		
			
				|  |  | +		for {
 | 
	
		
			
				|  |  | +			select {
 | 
	
		
			
				|  |  | +			case <-ctx.Done():
 | 
	
		
			
				|  |  | +				return nil
 | 
	
		
			
				|  |  | +			case opt := <-needSync:
 | 
	
		
			
				|  |  | +				err := s.Copy(ctx, project.Name, opt)
 | 
	
		
			
				|  |  | +				if err != nil {
 | 
	
		
			
				|  |  | +					return err
 | 
	
		
			
				|  |  | +				}
 | 
	
		
			
				|  |  | +				fmt.Fprintf(s.stderr(), "%s updated\n", opt.Source)
 | 
	
		
			
				|  |  | +			}
 | 
	
		
			
				|  |  | +		}
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |  func debounce(ctx context.Context, clock clockwork.Clock, delay time.Duration, input chan string, fn func(services []string)) {
 | 
	
		
			
				|  |  |  	services := utils.Set[string]{}
 | 
	
		
			
				|  |  |  	t := clock.AfterFunc(delay, func() {
 |