diff --git a/Taskfile.yml b/Taskfile.yml index e8ce66f1..ee9bcf9a 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -18,6 +18,11 @@ tasks: - task: lint - task: test + run: + desc: Runs Task + cmds: + - go run ./cmd/task {{.CLI_ARGS}} + install: desc: Installs Task aliases: [i] @@ -104,6 +109,12 @@ tasks: cmds: - go test ./... + test:watch: + desc: Runs test suite with watch tests included + deps: [sleepit:build] + cmds: + - go test ./... -tags 'watch' + test:all: desc: Runs test suite with signals and watch tests included deps: [sleepit:build] diff --git a/executor.go b/executor.go index 6b8d991f..c7d5f64c 100644 --- a/executor.go +++ b/executor.go @@ -7,6 +7,7 @@ import ( "sync" "time" + "github.com/puzpuzpuz/xsync/v3" "github.com/sajari/fuzzy" "github.com/go-task/task/v3/internal/logger" @@ -65,6 +66,7 @@ type ( mkdirMutexMap map[string]*sync.Mutex executionHashes map[string]context.Context executionHashesMutex sync.Mutex + watchedDirs *xsync.MapOf[string, bool] } TempDir struct { Remote string @@ -77,7 +79,6 @@ type ( func NewExecutor(opts ...ExecutorOption) *Executor { e := &Executor{ Timeout: time.Second * 10, - Interval: time.Second * 5, Stdin: os.Stdin, Stdout: os.Stdout, Stderr: os.Stderr, @@ -254,8 +255,8 @@ func ExecutorWithConcurrency(concurrency int) ExecutorOption { } } -// ExecutorWithInterval sets the interval at which the [Executor] will check for -// changes when watching tasks. +// ExecutorWithInterval sets the interval at which the [Executor] will wait for +// duplicated events before running a task. func ExecutorWithInterval(interval time.Duration) ExecutorOption { return func(e *Executor) { e.Interval = interval diff --git a/go.mod b/go.mod index 3699d7d0..bc5fda63 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/dominikbraun/graph v0.23.0 github.com/elliotchance/orderedmap/v3 v3.1.0 github.com/fatih/color v1.18.0 + github.com/fsnotify/fsnotify v1.8.0 github.com/go-git/go-billy/v5 v5.6.2 github.com/go-git/go-git/v5 v5.14.0 github.com/go-task/slim-sprig/v3 v3.0.0 @@ -19,7 +20,7 @@ require ( github.com/mattn/go-zglob v0.0.6 github.com/mitchellh/hashstructure/v2 v2.0.2 github.com/otiai10/copy v1.14.1 - github.com/radovskyb/watcher v1.0.7 + github.com/puzpuzpuz/xsync/v3 v3.5.1 github.com/sajari/fuzzy v1.0.0 github.com/spf13/pflag v1.0.6 github.com/stretchr/testify v1.10.0 @@ -45,7 +46,6 @@ require ( github.com/klauspost/cpuid/v2 v2.2.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/muesli/cancelreader v0.2.2 // indirect github.com/otiai10/mint v1.6.3 // indirect github.com/pjbgf/sha1cd v0.3.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect @@ -54,9 +54,7 @@ require ( github.com/stretchr/objx v0.5.2 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect golang.org/x/crypto v0.35.0 // indirect - golang.org/x/mod v0.22.0 // indirect golang.org/x/net v0.35.0 // indirect golang.org/x/sys v0.31.0 // indirect - golang.org/x/tools v0.27.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect ) diff --git a/go.sum b/go.sum index 9a8afcda..c9898452 100644 --- a/go.sum +++ b/go.sum @@ -5,8 +5,6 @@ github.com/Ladicle/tabwriter v1.0.0/go.mod h1:c4MdCjxQyTbGuQO/gvqJ+IA/89UEwrsD6h github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4= github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= -github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= -github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/ProtonMail/go-crypto v1.1.5 h1:eoAQfK2dwL+tFSFpr7TbOaPNUbPiJj4fLYwwGE1FQO4= @@ -23,14 +21,10 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPd github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/chainguard-dev/git-urls v1.0.2 h1:pSpT7ifrpc5X55n4aTTm7FFUE+ZQHKiqpiwNkJrVcKQ= github.com/chainguard-dev/git-urls v1.0.2/go.mod h1:rbGgj10OS7UgZlbzdUQIQpT0k/D4+An04HJY7Ol+Y/o= -github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= -github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= github.com/cloudflare/circl v1.6.0 h1:cr5JKic4HI+LkINy2lg3W2jF8sHCVTBncJr5gIIq7qk= github.com/cloudflare/circl v1.6.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= -github.com/creack/pty v1.1.23 h1:4M6+isWdcStXEf15G/RbrMPOQj1dZ7HPZCGwE4kOeP0= -github.com/creack/pty v1.1.23/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= -github.com/cyphar/filepath-securejoin v0.3.6 h1:4d9N5ykBnSp5Xn2JkhocYDkOpURL/18CYMpo6xB9uWM= -github.com/cyphar/filepath-securejoin v0.3.6/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= +github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= +github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -40,14 +34,16 @@ github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yA github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dominikbraun/graph v0.23.0 h1:TdZB4pPqCLFxYhdyMFb1TBdFxp8XLcJfTTBQucVPgCo= github.com/dominikbraun/graph v0.23.0/go.mod h1:yOjYyogZLY1LSG9E33JWZJiq5k83Qy2C6POAuiViluc= -github.com/elazarl/goproxy v1.4.0 h1:4GyuSbFa+s26+3rmYNSuUVsx+HgPrV1bk1jXI0l9wjM= -github.com/elazarl/goproxy v1.4.0/go.mod h1:X/5W/t+gzDyLfHW4DrMdpjqYjpXsURlBt9lpBDxZZZQ= +github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o= +github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= github.com/elliotchance/orderedmap/v3 v3.1.0 h1:j4DJ5ObEmMBt/lcwIecKcoRxIQUEnw0L804lXYDt/pg= github.com/elliotchance/orderedmap/v3 v3.1.0/go.mod h1:G+Hc2RwaZvJMcS4JpGCOyViCnGeKf0bTYCGTO4uhjSo= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= +github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= @@ -56,8 +52,6 @@ github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UN github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= -github.com/go-git/go-git/v5 v5.13.2 h1:7O7xvsK7K+rZPKW6AQR1YyNhfywkv7B8/FsP3ki6Zv0= -github.com/go-git/go-git/v5 v5.13.2/go.mod h1:hWdW5P4YZRjmpGHwRH2v3zkWcNl6HeXaXQEMGb3NJ9A= github.com/go-git/go-git/v5 v5.14.0 h1:/MD3lCrGjCen5WfEAzKg00MJJffKhC8gzS80ycmCi60= github.com/go-git/go-git/v5 v5.14.0/go.mod h1:Z5Xhoia5PcWA3NF8vRLURn9E5FRhSl7dGj9ItW3Wk5k= github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= @@ -66,12 +60,10 @@ github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1v github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/go-task/template v0.1.0 h1:ym/r2G937RZA1bsgiWedNnY9e5kxDT+3YcoAnuIetTE= github.com/go-task/template v0.1.0/go.mod h1:RgwRaZK+kni/hJJ7/AaOE2lPQFPbAdji/DyhC6pxo4k= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= @@ -98,8 +90,6 @@ github.com/mattn/go-zglob v0.0.6 h1:mP8RnmCgho4oaUYDIDn6GNxYk+qJGUs8fJLn+twYj2A= github.com/mattn/go-zglob v0.0.6/go.mod h1:MxxjyoXXnMxfIpxTK2GAkw1w8glPsQILx3N5wrKakiY= github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= -github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= -github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= github.com/otiai10/copy v1.14.1 h1:5/7E6qsUMBaH5AnQ0sSLzzTg1oTECmcCmT6lvF45Na8= @@ -112,17 +102,15 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/radovskyb/watcher v1.0.7 h1:AYePLih6dpmS32vlHfhCeli8127LzkIgwJGcwwe8tUE= -github.com/radovskyb/watcher v1.0.7/go.mod h1:78okwvY5wPdzcb1UYnip1pvrZNIVEIh/Cm+ZuvsUYIg= -github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= -github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg= +github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/sajari/fuzzy v1.0.0 h1:+FmwVvJErsd0d0hAPlj4CxqxUtQY/fOoY0DwX4ykpRY= github.com/sajari/fuzzy v1.0.0/go.mod h1:OjYR6KxoWOe9+dOlXeiCJd4dIbED4Oo8wpS89o0pwOo= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/skeema/knownhosts v1.3.0 h1:AM+y0rI04VksttfwjkSTNQorvGqmwATnvnAHpSgc0LY= -github.com/skeema/knownhosts v1.3.0/go.mod h1:sPINvnADmT/qYH1kfv+ePMmOBTH6Tbl7b5LvTDjFK7M= github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8= github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY= github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= @@ -141,22 +129,13 @@ github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= -golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs= golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= -golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= -golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= -golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= -golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= -golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -168,22 +147,15 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= -golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= -golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= -golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= -golang.org/x/tools v0.27.0/go.mod h1:sUi0ZgbwW9ZPAq26Ekut+weQPR5eIM6GQLQ1Yjm1H0Q= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= @@ -194,7 +166,5 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -mvdan.cc/sh/v3 v3.10.0 h1:v9z7N1DLZ7owyLM/SXZQkBSXcwr2IGMm2LY2pmhVXj4= -mvdan.cc/sh/v3 v3.10.0/go.mod h1:z/mSSVyLFGZzqb3ZIKojjyqIx/xbmz/UHdCSv9HmqXY= mvdan.cc/sh/v3 v3.11.0 h1:q5h+XMDRfUGUedCqFFsjoFjrhwf2Mvtt1rkMvVz0blw= mvdan.cc/sh/v3 v3.11.0/go.mod h1:LRM+1NjoYCzuq/WZ6y44x14YNAI0NK7FLPeQSaFagGg= diff --git a/internal/fingerprint/glob.go b/internal/fingerprint/glob.go index f564eff2..d9e70135 100644 --- a/internal/fingerprint/glob.go +++ b/internal/fingerprint/glob.go @@ -12,28 +12,20 @@ import ( ) func Globs(dir string, globs []*ast.Glob) ([]string, error) { - fileMap := make(map[string]bool) + resultMap := make(map[string]bool) for _, g := range globs { - matches, err := Glob(dir, g.Glob) + matches, err := glob(dir, g.Glob) if err != nil { continue } for _, match := range matches { - fileMap[match] = !g.Negate + resultMap[match] = !g.Negate } } - files := make([]string, 0) - for file, includePath := range fileMap { - if includePath { - files = append(files, file) - } - } - sort.Strings(files) - return files, nil + return collectKeys(resultMap), nil } -func Glob(dir string, g string) ([]string, error) { - files := make([]string, 0) +func glob(dir string, g string) ([]string, error) { g = filepathext.SmartJoin(dir, g) g, err := execext.Expand(g) @@ -46,6 +38,8 @@ func Glob(dir string, g string) ([]string, error) { return nil, err } + results := make(map[string]bool, len(fs)) + for _, f := range fs { info, err := os.Stat(f) if err != nil { @@ -54,7 +48,18 @@ func Glob(dir string, g string) ([]string, error) { if info.IsDir() { continue } - files = append(files, f) + results[f] = true } - return files, nil + return collectKeys(results), nil +} + +func collectKeys(m map[string]bool) []string { + keys := make([]string, 0, len(m)) + for k, v := range m { + if v { + keys = append(keys, k) + } + } + sort.Strings(keys) + return keys } diff --git a/internal/fingerprint/sources_checksum.go b/internal/fingerprint/sources_checksum.go index 91cb6e38..16a98c16 100644 --- a/internal/fingerprint/sources_checksum.go +++ b/internal/fingerprint/sources_checksum.go @@ -56,7 +56,7 @@ func (checker *ChecksumChecker) IsUpToDate(t *ast.Task) (bool, error) { if g.Negate { continue } - generates, err := Glob(t.Dir, g.Glob) + generates, err := glob(t.Dir, g.Glob) if os.IsNotExist(err) { return false, nil } diff --git a/internal/fsnotifyext/fsnotify_dedup.go b/internal/fsnotifyext/fsnotify_dedup.go new file mode 100644 index 00000000..ef9fa9cb --- /dev/null +++ b/internal/fsnotifyext/fsnotify_dedup.go @@ -0,0 +1,56 @@ +package fsnotifyext + +import ( + "math" + "sync" + "time" + + "github.com/fsnotify/fsnotify" +) + +type Deduper struct { + w *fsnotify.Watcher + waitTime time.Duration + mutex sync.Mutex +} + +func NewDeduper(w *fsnotify.Watcher, waitTime time.Duration) *Deduper { + return &Deduper{ + w: w, + waitTime: waitTime, + } +} + +func (d *Deduper) GetChan() chan fsnotify.Event { + channel := make(chan fsnotify.Event) + timers := make(map[string]*time.Timer) + + go func() { + for { + event, ok := <-d.w.Events + switch { + case !ok: + return + case event.Op == fsnotify.Chmod: + continue + } + + d.mutex.Lock() + timer, ok := timers[event.String()] + d.mutex.Unlock() + + if !ok { + timer = time.AfterFunc(math.MaxInt64, func() { channel <- event }) + timer.Stop() + + d.mutex.Lock() + timers[event.String()] = timer + d.mutex.Unlock() + } + + timer.Reset(d.waitTime) + } + }() + + return channel +} diff --git a/testdata/watcher_interval/.gitignore b/testdata/watch/.gitignore similarity index 100% rename from testdata/watcher_interval/.gitignore rename to testdata/watch/.gitignore diff --git a/testdata/watch/Taskfile.yaml b/testdata/watch/Taskfile.yaml new file mode 100644 index 00000000..813ff45d --- /dev/null +++ b/testdata/watch/Taskfile.yaml @@ -0,0 +1,10 @@ +# https://taskfile.dev + +version: '3' + +tasks: + default: + sources: + - "src/*" + cmds: + - echo "Task running!" diff --git a/testdata/watcher_interval/Taskfile.yaml b/testdata/watcher_interval/Taskfile.yaml deleted file mode 100644 index 110eff27..00000000 --- a/testdata/watcher_interval/Taskfile.yaml +++ /dev/null @@ -1,16 +0,0 @@ -# https://taskfile.dev - -version: '3' - -vars: - GREETING: Hello, World! - -interval: "500ms" - -tasks: - default: - sources: - - "src/*" - cmds: - - echo "{{.GREETING}}" - silent: false diff --git a/watch.go b/watch.go index 3b290b8b..76bab9ae 100644 --- a/watch.go +++ b/watch.go @@ -6,18 +6,22 @@ import ( "os" "os/signal" "path/filepath" + "slices" "strings" "syscall" "time" - "github.com/radovskyb/watcher" + "github.com/fsnotify/fsnotify" + "github.com/puzpuzpuz/xsync/v3" "github.com/go-task/task/v3/errors" + "github.com/go-task/task/v3/internal/filepathext" "github.com/go-task/task/v3/internal/fingerprint" + "github.com/go-task/task/v3/internal/fsnotifyext" "github.com/go-task/task/v3/internal/logger" ) -const defaultWatchInterval = 5 * time.Second +const defaultWaitTime = 100 * time.Millisecond // watchTasks start watching the given tasks func (e *Executor) watchTasks(calls ...*Call) error { @@ -32,34 +36,48 @@ func (e *Executor) watchTasks(calls ...*Call) error { for _, c := range calls { c := c go func() { - if err := e.RunTask(ctx, c); err != nil && !isContextError(err) { + err := e.RunTask(ctx, c) + if err == nil { + e.Logger.Errf(logger.Green, "task: task \"%s\" finished running\n", c.Task) + } else if !isContextError(err) { e.Logger.Errf(logger.Red, "%v\n", err) } }() } - var watchInterval time.Duration + var waitTime time.Duration switch { case e.Interval != 0: - watchInterval = e.Interval + waitTime = e.Interval case e.Taskfile.Interval != 0: - watchInterval = e.Taskfile.Interval + waitTime = e.Taskfile.Interval default: - watchInterval = defaultWatchInterval + waitTime = defaultWaitTime } - e.Logger.VerboseOutf(logger.Green, "task: Watching for changes every %v\n", watchInterval) - - w := watcher.New() + w, err := fsnotify.NewWatcher() + if err != nil { + cancel() + return err + } defer w.Close() - w.SetMaxEvents(1) + + deduper := fsnotifyext.NewDeduper(w, waitTime) + eventsChan := deduper.GetChan() closeOnInterrupt(w) go func() { for { select { - case event := <-w.Event: + case event, ok := <-eventsChan: + switch { + case !ok: + cancel() + return + case event.Op == fsnotify.Chmod: + continue + } e.Logger.VerboseErrf(logger.Magenta, "task: received watch event: %v\n", event) cancel() @@ -70,35 +88,58 @@ func (e *Executor) watchTasks(calls ...*Call) error { for _, c := range calls { c := c go func() { - if err := e.RunTask(ctx, c); err != nil && !isContextError(err) { + t, err := e.GetTask(c) + if err != nil { + e.Logger.Errf(logger.Red, "%v\n", err) + return + } + baseDir := filepathext.SmartJoin(e.Dir, t.Dir) + files, err := fingerprint.Globs(baseDir, t.Sources) + if err != nil { + e.Logger.Errf(logger.Red, "%v\n", err) + return + } + if !event.Has(fsnotify.Remove) && !slices.Contains(files, event.Name) { + relPath, _ := filepath.Rel(baseDir, event.Name) + e.Logger.VerboseErrf(logger.Magenta, "task: skipped for file not in sources: %s\n", relPath) + return + } + err = e.RunTask(ctx, c) + if err == nil { + e.Logger.Errf(logger.Green, "task: task \"%s\" finished running\n", c.Task) + } else if !isContextError(err) { e.Logger.Errf(logger.Red, "%v\n", err) } }() } - case err := <-w.Error: - switch err { - case watcher.ErrWatchedFileDeleted: + case err, ok := <-w.Errors: + switch { + case !ok: + cancel() + return default: e.Logger.Errf(logger.Red, "%v\n", err) } - case <-w.Closed: - cancel() - return } } }() + e.watchedDirs = xsync.NewMapOf[string, bool]() + go func() { - // re-register every 5 seconds because we can have new files, but this process is expensive to run + // NOTE(@andreynering): New files can be created in directories + // that were previously empty, so we need to check for new dirs + // from time to time. for { - if err := e.registerWatchedFiles(w, calls...); err != nil { + if err := e.registerWatchedDirs(w, calls...); err != nil { e.Logger.Errf(logger.Red, "%v\n", err) } - time.Sleep(watchInterval) + time.Sleep(5 * time.Second) } }() - return w.Start(watchInterval) + <-make(chan struct{}) + return nil } func isContextError(err error) bool { @@ -106,73 +147,66 @@ func isContextError(err error) bool { err = taskRunErr.Err } - return err == context.Canceled || err == context.DeadlineExceeded + return errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) } -func closeOnInterrupt(w *watcher.Watcher) { +func closeOnInterrupt(w *fsnotify.Watcher) { ch := make(chan os.Signal, 1) signal.Notify(ch, os.Interrupt, syscall.SIGTERM) go func() { <-ch w.Close() + os.Exit(0) }() } -func (e *Executor) registerWatchedFiles(w *watcher.Watcher, calls ...*Call) error { - watchedFiles := w.WatchedFiles() - - var registerTaskFiles func(*Call) error - registerTaskFiles = func(c *Call) error { +func (e *Executor) registerWatchedDirs(w *fsnotify.Watcher, calls ...*Call) error { + var registerTaskDirs func(*Call) error + registerTaskDirs = func(c *Call) error { task, err := e.CompiledTask(c) if err != nil { return err } for _, d := range task.Deps { - if err := registerTaskFiles(&Call{Task: d.Task, Vars: d.Vars}); err != nil { + if err := registerTaskDirs(&Call{Task: d.Task, Vars: d.Vars}); err != nil { return err } } for _, c := range task.Cmds { if c.Task != "" { - if err := registerTaskFiles(&Call{Task: c.Task, Vars: c.Vars}); err != nil { + if err := registerTaskDirs(&Call{Task: c.Task, Vars: c.Vars}); err != nil { return err } } } - globs, err := fingerprint.Globs(task.Dir, task.Sources) + files, err := fingerprint.Globs(task.Dir, task.Sources) if err != nil { return err } - for _, s := range globs { - files, err := fingerprint.Glob(task.Dir, s) - if err != nil { - return fmt.Errorf("task: %s: %w", s, err) + for _, f := range files { + d := filepath.Dir(f) + if isSet, ok := e.watchedDirs.Load(d); ok && isSet { + continue } - for _, f := range files { - absFile, err := filepath.Abs(f) - if err != nil { - return err - } - if ShouldIgnoreFile(absFile) { - continue - } - if _, ok := watchedFiles[absFile]; ok { - continue - } - if err := w.Add(absFile); err != nil { - return err - } - e.Logger.VerboseOutf(logger.Green, "task: watching new file: %v\n", absFile) + if ShouldIgnoreFile(d) { + continue } + if err := w.Add(d); err != nil { + return err + } + e.watchedDirs.Store(d, true) + relPath, _ := filepath.Rel(e.Dir, d) + w.Events <- fsnotify.Event{Name: f, Op: fsnotify.Create} + e.Logger.VerboseOutf(logger.Green, "task: watching new dir: %v\n", relPath) } return nil } for _, c := range calls { - if err := registerTaskFiles(c); err != nil { + if err := registerTaskDirs(c); err != nil { return err } } diff --git a/watch_test.go b/watch_test.go index be15ff2d..447c910a 100644 --- a/watch_test.go +++ b/watch_test.go @@ -17,71 +17,72 @@ import ( "github.com/go-task/task/v3" "github.com/go-task/task/v3/internal/filepathext" - "github.com/go-task/task/v3/taskfile/ast" ) -func TestFileWatcherInterval(t *testing.T) { - const dir = "testdata/watcher_interval" +func TestFileWatch(t *testing.T) { + t.Parallel() + + const dir = "testdata/watch" + _ = os.RemoveAll(filepathext.SmartJoin(dir, ".task")) + _ = os.RemoveAll(filepathext.SmartJoin(dir, "src")) + expectedOutput := strings.TrimSpace(` task: Started watching for tasks: default -task: [default] echo "Hello, World!" -Hello, World! -task: [default] echo "Hello, World!" -Hello, World! +task: [default] echo "Task running!" +Task running! +task: task "default" finished running +task: Task "default" is up to date +task: task "default" finished running `) var buff bytes.Buffer - e := &task.NewExecutor( - task.WithDir(dir), - task.WithStdout(&buff), - task.WithStderr(&buff), - task.WithWatch(true), + e := task.NewExecutor( + task.ExecutorWithDir(dir), + task.ExecutorWithStdout(&buff), + task.ExecutorWithStderr(&buff), + task.ExecutorWithWatch(true), ) require.NoError(t, e.Setup()) buff.Reset() - err := os.MkdirAll(filepathext.SmartJoin(dir, "src"), 0755) + dirPath := filepathext.SmartJoin(dir, "src") + filePath := filepathext.SmartJoin(dirPath, "a") + + err := os.MkdirAll(dirPath, 0755) require.NoError(t, err) - err = os.WriteFile(filepathext.SmartJoin(dir, "src/a"), []byte("test"), 0644) - if err != nil { - t.Fatal(err) - } + err = os.WriteFile(filePath, []byte("test"), 0644) + require.NoError(t, err) - ctx := context.Background() - ctx, cancel := context.WithCancel(ctx) + ctx, cancel := context.WithCancel(context.Background()) - go func(ctx context.Context) { + go func() { for { select { case <-ctx.Done(): return default: - err := e.Run(ctx, &ast.Call{Task: "default"}) + err := e.Run(ctx, &task.Call{Task: "default"}) if err != nil { - return + panic(err) } } } - }(ctx) + }() time.Sleep(10 * time.Millisecond) - err = os.WriteFile(filepathext.SmartJoin(dir, "src/a"), []byte("test updated"), 0644) - if err != nil { - t.Fatal(err) - } - time.Sleep(700 * time.Millisecond) + err = os.WriteFile(filePath, []byte("test updated"), 0644) + require.NoError(t, err) + + time.Sleep(150 * time.Millisecond) cancel() assert.Equal(t, expectedOutput, strings.TrimSpace(buff.String())) - buff.Reset() - err = os.RemoveAll(filepathext.SmartJoin(dir, ".task")) - require.NoError(t, err) - err = os.RemoveAll(filepathext.SmartJoin(dir, "src")) - require.NoError(t, err) } func TestShouldIgnoreFile(t *testing.T) { + t.Parallel() + tt := []struct { path string expect bool diff --git a/website/docs/usage.mdx b/website/docs/usage.mdx index 0bfa5086..f9738596 100644 --- a/website/docs/usage.mdx +++ b/website/docs/usage.mdx @@ -2285,9 +2285,11 @@ With the flags `--watch` or `-w` task will watch for file changes and run the task again. This requires the `sources` attribute to be given, so task knows which files to watch. -The default watch interval is 5 seconds, but it's possible to change it by -either setting `interval: '500ms'` in the root of the Taskfile or by passing it -as an argument like `--interval=500ms`. +The default watch interval is 100 milliseconds, but it's possible to change it +by either setting `interval: '500ms'` in the root of the Taskfile or by passing +it as an argument like `--interval=500ms`. +This interval is the time Task will wait for duplicated events. It will only run +the task again once, even if multiple changes happen within the interval. Also, it's possible to set `watch: true` in a given task and it'll automatically run in watch mode: diff --git a/website/static/schema.json b/website/static/schema.json index a44df7a8..49af497b 100644 --- a/website/static/schema.json +++ b/website/static/schema.json @@ -733,7 +733,7 @@ "$ref": "#/definitions/run" }, "interval": { - "description": "Sets a different watch interval when using `--watch`, the default being 5 seconds. This string should be a valid Go duration: https://pkg.go.dev/time#ParseDuration.", + "description": "Sets a different watch interval when using `--watch`, the default being 100 milliseconds. This string should be a valid Go duration: https://pkg.go.dev/time#ParseDuration.", "type": "string", "pattern": "^[0-9]+(?:m|s|ms)$" }