diff --git a/cmd/task/task.go b/cmd/task/task.go index 5a3893b8..abee37dd 100644 --- a/cmd/task/task.go +++ b/cmd/task/task.go @@ -73,6 +73,7 @@ func main() { entrypoint string output taskfile.Output color bool + interval string ) pflag.BoolVar(&versionFlag, "version", false, "show Task version") @@ -96,6 +97,7 @@ func main() { pflag.StringVar(&output.Group.End, "output-group-end", "", "message template to print after a task's grouped output") pflag.BoolVarP(&color, "color", "c", true, "colored output. Enabled by default. Set flag to false or use NO_COLOR=1 to disable") pflag.IntVarP(&concurrency, "concurrency", "C", 0, "limit number tasks to run concurrently") + pflag.StringVarP(&interval, "interval", "I", "5s", "interval to watch for changes") pflag.Parse() if versionFlag { @@ -151,6 +153,7 @@ func main() { Parallel: parallel, Color: color, Concurrency: concurrency, + Interval: interval, Stdin: os.Stdin, Stdout: os.Stdout, @@ -206,6 +209,10 @@ func main() { e.InterceptInterruptSignals() } + if e.Interval == "" { + e.Interval = strings.TrimSpace(interval) + } + ctx := context.Background() if status { diff --git a/task.go b/task.go index 59cde1f1..0a075161 100644 --- a/task.go +++ b/task.go @@ -41,6 +41,7 @@ type Executor struct { Parallel bool Color bool Concurrency int + Interval string Stdin io.Reader Stdout io.Writer diff --git a/task_test.go b/task_test.go index 9602a6c0..b66d8d48 100644 --- a/task_test.go +++ b/task_test.go @@ -10,6 +10,7 @@ import ( "runtime" "strings" "testing" + "time" "github.com/stretchr/testify/assert" @@ -1480,3 +1481,64 @@ func TestEvaluateSymlinksInPaths(t *testing.T) { err = os.RemoveAll(dir + "/.task") assert.NoError(t, err) } + +func TestFileWatcherInterval(t *testing.T) { + const dir = "testdata/watcher_interval" + expectedOutput := strings.TrimSpace(` +task: Started watching for tasks: default +task: [default] echo "Hello, World!" +Hello, World! +task: [default] echo "Hello, World!" +Hello, World! + `) + + var buff bytes.Buffer + e := &task.Executor{ + Dir: dir, + Stdout: &buff, + Stderr: &buff, + Watch: true, + } + + assert.NoError(t, e.Setup()) + buff.Reset() + + err := os.MkdirAll(dir+"/src", 0755) + assert.NoError(t, err) + + err = os.WriteFile(dir+"/src/a", []byte("test"), 0644) + if err != nil { + t.Fatal(err) + } + + ctx := context.Background() + ctx, cancel := context.WithCancel(ctx) + + go func(ctx context.Context) { + for { + select { + case <-ctx.Done(): + return + default: + err := e.Run(ctx, taskfile.Call{Task: "default"}) + if err != nil { + return + } + } + } + }(ctx) + + time.Sleep(10 * time.Millisecond) + err = os.WriteFile(dir+"/src/a", []byte("test updated"), 0644) + if err != nil { + t.Fatal(err) + } + time.Sleep(70 * time.Millisecond) + cancel() + assert.Equal(t, expectedOutput, strings.TrimSpace(buff.String())) + buff.Reset() + err = os.RemoveAll(dir + "/.task") + assert.NoError(t, err) + err = os.RemoveAll(dir + "/src") + assert.NoError(t, err) +} diff --git a/taskfile/taskfile.go b/taskfile/taskfile.go index ff7b1bbf..90f274ed 100644 --- a/taskfile/taskfile.go +++ b/taskfile/taskfile.go @@ -18,6 +18,7 @@ type Taskfile struct { Silent bool Dotenv []string Run string + Interval string } // UnmarshalYAML implements yaml.Unmarshaler interface @@ -34,10 +35,13 @@ func (tf *Taskfile) UnmarshalYAML(unmarshal func(interface{}) error) error { Silent bool Dotenv []string Run string + Interval string } + if err := unmarshal(&taskfile); err != nil { return err } + tf.Version = taskfile.Version tf.Expansions = taskfile.Expansions tf.Output = taskfile.Output @@ -49,6 +53,8 @@ func (tf *Taskfile) UnmarshalYAML(unmarshal func(interface{}) error) error { tf.Silent = taskfile.Silent tf.Dotenv = taskfile.Dotenv tf.Run = taskfile.Run + tf.Interval = taskfile.Interval + if tf.Expansions <= 0 { tf.Expansions = 2 } diff --git a/testdata/watcher_interval/.gitignore b/testdata/watcher_interval/.gitignore new file mode 100644 index 00000000..71bcfc6c --- /dev/null +++ b/testdata/watcher_interval/.gitignore @@ -0,0 +1 @@ +src/* diff --git a/testdata/watcher_interval/Taskfile.yaml b/testdata/watcher_interval/Taskfile.yaml new file mode 100644 index 00000000..2a1e16b8 --- /dev/null +++ b/testdata/watcher_interval/Taskfile.yaml @@ -0,0 +1,16 @@ +# https://taskfile.dev + +version: '3' + +vars: + GREETING: Hello, World! + +interval: "30ms" + +tasks: + default: + sources: + - "src/*" + cmds: + - echo "{{.GREETING}}" + silent: false diff --git a/watch.go b/watch.go index b93e2d30..60ac6aae 100644 --- a/watch.go +++ b/watch.go @@ -16,7 +16,7 @@ import ( "github.com/radovskyb/watcher" ) -const watchInterval = 5 * time.Second +const defaultWatchInterval = 5 * time.Second // watchTasks start watching the given tasks func (e *Executor) watchTasks(calls ...taskfile.Call) error { @@ -24,6 +24,7 @@ func (e *Executor) watchTasks(calls ...taskfile.Call) error { for i, c := range calls { tasks[i] = c.Task } + e.Logger.Errf(logger.Green, "task: Started watching for tasks: %s", strings.Join(tasks, ", ")) ctx, cancel := context.WithCancel(context.Background()) @@ -36,6 +37,26 @@ func (e *Executor) watchTasks(calls ...taskfile.Call) error { }() } + var watchIntervalString string + + if e.Interval != "" { + watchIntervalString = e.Interval + } else if e.Taskfile.Interval != "" { + watchIntervalString = e.Taskfile.Interval + } + + watchInterval := defaultWatchInterval + + if watchIntervalString != "" { + var err error + watchInterval, err = parseWatchInterval(watchIntervalString) + if err != nil { + e.Logger.Errf(logger.Red, "%v", err) + } + } + + e.Logger.VerboseOutf(logger.Green, "task: Watching for changes every %v", watchInterval) + w := watcher.New() defer w.Close() w.SetMaxEvents(1) @@ -163,3 +184,11 @@ func (e *Executor) registerWatchedFiles(w *watcher.Watcher, calls ...taskfile.Ca func shouldIgnoreFile(path string) bool { return strings.Contains(path, "/.git") || strings.Contains(path, "/.task") || strings.Contains(path, "/node_modules") } + +func parseWatchInterval(watchInterval string) (time.Duration, error) { + v, err := time.ParseDuration(watchInterval) + if err != nil { + return 0, fmt.Errorf(`task: Could not parse watch interval "%s": %v`, watchInterval, err) + } + return v, nil +}