From 46a99ac963e659ec41ab134a8ea602b3212a35c5 Mon Sep 17 00:00:00 2001 From: Valentin Maerten Date: Fri, 3 Apr 2026 22:35:04 +0200 Subject: [PATCH 1/6] feat: add gitignore option to exclude ignored files from sources/generates When `gitignore: true` is set at the Taskfile or task level, files matching .gitignore rules are automatically excluded from sources and generates glob resolution. This prevents rebuilds triggered by changes to files that are in .gitignore (build artifacts, generated files, etc.). Uses go-git to load .gitignore patterns including nested .gitignore files, .git/info/exclude, and global gitignore configuration. --- go.mod | 16 +++ go.sum | 47 ++++++++ internal/fingerprint/gitignore.go | 60 ++++++++++ internal/fingerprint/gitignore_test.go | 127 ++++++++++++++++++++++ internal/fingerprint/glob.go | 7 +- internal/fingerprint/sources_checksum.go | 2 +- internal/fingerprint/sources_timestamp.go | 6 +- task_test.go | 49 +++++++++ taskfile/ast/task.go | 10 ++ taskfile/ast/taskfile.go | 13 ++- testdata/gitignore/.gitignore | 1 + testdata/gitignore/Taskfile.yml | 25 +++++ testdata/gitignore/source.txt | 1 + variables.go | 16 ++- watch.go | 2 +- website/src/public/schema.json | 10 ++ 16 files changed, 377 insertions(+), 15 deletions(-) create mode 100644 internal/fingerprint/gitignore.go create mode 100644 internal/fingerprint/gitignore_test.go create mode 100644 testdata/gitignore/.gitignore create mode 100644 testdata/gitignore/Taskfile.yml create mode 100644 testdata/gitignore/source.txt diff --git a/go.mod b/go.mod index 6e6861b4be..279ef5c0d3 100644 --- a/go.mod +++ b/go.mod @@ -43,9 +43,12 @@ require ( cloud.google.com/go/iam v1.5.3 // indirect cloud.google.com/go/monitoring v1.24.3 // indirect cloud.google.com/go/storage v1.60.0 // indirect + dario.cat/mergo v1.0.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/ProtonMail/go-crypto v1.1.6 // indirect github.com/atotto/clipboard v0.1.4 // indirect github.com/aws/aws-sdk-go-v2 v1.41.1 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 // indirect @@ -76,21 +79,30 @@ require ( github.com/charmbracelet/x/windows v0.2.2 // indirect github.com/clipperhouse/displaywidth v0.11.0 // indirect github.com/clipperhouse/uax29/v2 v2.7.0 // indirect + github.com/cloudflare/circl v1.6.3 // indirect github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 // indirect + github.com/cyphar/filepath-securejoin v0.4.1 // indirect github.com/dlclark/regexp2 v1.11.5 // indirect github.com/dustin/go-humanize v1.0.1 // indirect + github.com/emirpasic/gods v1.18.1 // indirect github.com/envoyproxy/go-control-plane/envoy v1.36.0 // indirect github.com/envoyproxy/protoc-gen-validate v1.3.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect + github.com/go-git/go-billy/v5 v5.8.0 // indirect + github.com/go-git/go-git/v5 v5.17.2 // indirect github.com/go-jose/go-jose/v4 v4.1.4 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect + github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/google/s2a-go v0.1.9 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.11 // indirect github.com/googleapis/gax-go/v2 v2.17.0 // indirect github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.70 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-version v1.8.0 // indirect + github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect + github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/klauspost/compress v1.18.4 // indirect github.com/klauspost/cpuid/v2 v2.2.10 // indirect github.com/klauspost/pgzip v1.2.6 // indirect @@ -101,15 +113,18 @@ require ( github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/pierrec/lz4/v4 v4.1.22 // indirect + github.com/pjbgf/sha1cd v0.3.2 // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect + github.com/skeema/knownhosts v1.3.1 // indirect github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/u-root/u-root v0.15.1-0.20251208185023-2f8c7e763cf8 // indirect github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 // indirect github.com/ulikunitz/xz v0.5.15 // indirect + github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/detectors/gcp v1.39.0 // indirect @@ -132,5 +147,6 @@ require ( google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 // indirect google.golang.org/grpc v1.79.3 // indirect google.golang.org/protobuf v1.36.11 // indirect + gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 00131950e7..fc74b31ac2 100644 --- a/go.sum +++ b/go.sum @@ -26,6 +26,8 @@ cloud.google.com/go/storage v1.60.0 h1:oBfZrSOCimggVNz9Y/bXY35uUcts7OViubeddTTVz cloud.google.com/go/storage v1.60.0/go.mod h1:q+5196hXfejkctrnx+VYU8RKQr/L3c0cBIlrjmiAKE0= cloud.google.com/go/trace v1.11.7 h1:kDNDX8JkaAG3R2nq1lIdkb7FCSi1rCmsEtKVsty7p+U= cloud.google.com/go/trace v1.11.7/go.mod h1:TNn9d5V3fQVf6s4SCveVMIBS2LJUqo73GACmq/Tky0s= +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 h1:sBEjpZlNHzK1voKq9695PJSX2o5NEXl7/OL3coiIY0c= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 h1:UnDZ/zFfG1JhH/DqxIZYU/1CUAlTUScoXD/LcM2Ykk8= @@ -38,6 +40,11 @@ github.com/Ladicle/tabwriter v1.0.0 h1:DZQqPvMumBDwVNElso13afjYLNp0Z7pHqHnu0r4t9 github.com/Ladicle/tabwriter v1.0.0/go.mod h1:c4MdCjxQyTbGuQO/gvqJ+IA/89UEwrsD6hUCW98dyp4= github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= github.com/Masterminds/semver/v3 v3.4.0/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.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw= +github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= github.com/alecthomas/chroma/v2 v2.23.1 h1:nv2AVZdTyClGbVQkIzlDm/rnhk1E9bU9nXwmZ/Vk/iY= @@ -110,10 +117,14 @@ github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSE github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0= github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= +github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= +github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 h1:6xNmx7iTtyBRev0+D/Tv1FZd4SCg8axKApyNyRsAt/w= github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI= 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= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -126,6 +137,8 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= 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/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA= github.com/envoyproxy/go-control-plane v0.14.0/go.mod h1:NcS5X47pLl/hfqxU70yPwL9ZMkUlwlKxtAohpi2wBEU= github.com/envoyproxy/go-control-plane/envoy v1.36.0 h1:yg/JjO5E7ubRyKX3m07GF3reDNEnfOboJ0QySbH736g= @@ -140,6 +153,12 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= +github.com/go-git/go-billy/v5 v5.8.0 h1:I8hjc3LbBlXTtVuFNJuwYuMiHvQJDq1AT6u4DwDzZG0= +github.com/go-git/go-billy/v5 v5.8.0/go.mod h1:RpvI/rw4Vr5QA+Z60c6d6LXH0rYJo0uD5SqfmrrheCY= +github.com/go-git/go-git/v5 v5.17.2 h1:B+nkdlxdYrvyFK4GPXVU8w1U+YkbsgciIR7f2sZJ104= +github.com/go-git/go-git/v5 v5.17.2/go.mod h1:pW/VmeqkanRFqR6AljLcs7EA7FbZaN5MQqO7oZADXpo= github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA= github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -153,6 +172,8 @@ 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.2.0 h1:xW7ek0o65FUSTbKcSNeg2Vyf/I7wYXFgLUznptvviBE= github.com/go-task/template v0.2.0/go.mod h1:dbdoUb6qKnHQi1y6o+IdIrs0J4o/SEhSTA6bbzZmdtc= +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/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= @@ -177,8 +198,12 @@ github.com/hashicorp/go-version v1.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bP github.com/hashicorp/go-version v1.8.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= 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= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= +github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c= github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= @@ -208,7 +233,10 @@ github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELU github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU= github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4= +github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -227,6 +255,9 @@ github.com/sebdah/goldie/v2 v2.8.0/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvK github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= 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.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8= +github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo= @@ -234,6 +265,7 @@ github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xI github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= @@ -244,6 +276,8 @@ github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 h1:pyC9PaHYZFgEKFdlp3G8 github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701/go.mod h1:P3a5rG4X7tI17Nn3aOIAYr5HbIMukwXG0urG0WuL8OA= github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY= github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= +github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= @@ -272,25 +306,36 @@ go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZY go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw= golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU= golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/api v0.267.0 h1:w+vfWPMPYeRs8qH1aYYsFX68jMls5acWl/jocfLomwE= @@ -309,6 +354,8 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 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= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= 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= diff --git a/internal/fingerprint/gitignore.go b/internal/fingerprint/gitignore.go new file mode 100644 index 0000000000..0b9ad2053d --- /dev/null +++ b/internal/fingerprint/gitignore.go @@ -0,0 +1,60 @@ +package fingerprint + +import ( + "path/filepath" + "strings" + + "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/plumbing/format/gitignore" +) + +// filterGitignored removes entries from the file map that match gitignore rules. +// Files are expected to be absolute paths. The dir parameter is used to find the git repository. +// Returns the input map unchanged if the directory is not inside a git repository. +func filterGitignored(files map[string]bool, dir string) map[string]bool { + repo, err := git.PlainOpenWithOptions(dir, &git.PlainOpenOptions{ + DetectDotGit: true, + }) + if err != nil { + return files + } + + wt, err := repo.Worktree() + if err != nil { + return files + } + + var allPatterns []gitignore.Pattern + + if ps, err := gitignore.LoadSystemPatterns(wt.Filesystem); err == nil { + allPatterns = append(allPatterns, ps...) + } + + if ps, err := gitignore.LoadGlobalPatterns(wt.Filesystem); err == nil { + allPatterns = append(allPatterns, ps...) + } + + if ps, err := gitignore.ReadPatterns(wt.Filesystem, nil); err == nil { + allPatterns = append(allPatterns, ps...) + } + + if len(allPatterns) == 0 { + return files + } + + matcher := gitignore.NewMatcher(allPatterns) + gitRoot := wt.Filesystem.Root() + + for path := range files { + relPath, err := filepath.Rel(gitRoot, path) + if err != nil { + continue + } + pathComponents := strings.Split(filepath.ToSlash(relPath), "/") + if matcher.Match(pathComponents, false) { + files[path] = false + } + } + + return files +} diff --git a/internal/fingerprint/gitignore_test.go b/internal/fingerprint/gitignore_test.go new file mode 100644 index 0000000000..8eb0dbfdbb --- /dev/null +++ b/internal/fingerprint/gitignore_test.go @@ -0,0 +1,127 @@ +package fingerprint + +import ( + "os" + "path/filepath" + "testing" + + "github.com/go-git/go-git/v5" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/go-task/task/v3/taskfile/ast" +) + +func initGitRepo(t *testing.T, dir string) { + t.Helper() + _, err := git.PlainInit(dir, false) + require.NoError(t, err) +} + +func TestGlobsWithGitignore(t *testing.T) { + t.Parallel() + + dir, err := os.MkdirTemp("", "task-gitignore-test-*") + require.NoError(t, err) + t.Cleanup(func() { os.RemoveAll(dir) }) + + initGitRepo(t, dir) + + // Create test files + require.NoError(t, os.WriteFile(filepath.Join(dir, "included.txt"), []byte("included"), 0o644)) + require.NoError(t, os.WriteFile(filepath.Join(dir, "ignored.log"), []byte("ignored"), 0o644)) + require.NoError(t, os.WriteFile(filepath.Join(dir, "also-included.txt"), []byte("also included"), 0o644)) + + // Create .gitignore + require.NoError(t, os.WriteFile(filepath.Join(dir, ".gitignore"), []byte("*.log\n"), 0o644)) + + globs := []*ast.Glob{ + {Glob: "./*"}, + } + + // Without gitignore - should include all files + filesWithout, err := Globs(dir, globs, false) + require.NoError(t, err) + + // With gitignore - should exclude .log files + filesWith, err := Globs(dir, globs, true) + require.NoError(t, err) + + // The .log file should be in the unfiltered list + hasLog := false + for _, f := range filesWithout { + if filepath.Base(f) == "ignored.log" { + hasLog = true + break + } + } + assert.True(t, hasLog, "ignored.log should be present without gitignore filter") + + // The .log file should NOT be in the filtered list + hasLog = false + for _, f := range filesWith { + if filepath.Base(f) == "ignored.log" { + hasLog = true + break + } + } + assert.False(t, hasLog, "ignored.log should be excluded with gitignore filter") + + // .txt files should still be present + txtCount := 0 + for _, f := range filesWith { + if filepath.Ext(f) == ".txt" { + txtCount++ + } + } + assert.Equal(t, 2, txtCount, "both .txt files should remain") +} + +func TestGlobsWithGitignoreDisabled(t *testing.T) { + t.Parallel() + + dir, err := os.MkdirTemp("", "task-gitignore-disabled-*") + require.NoError(t, err) + t.Cleanup(func() { os.RemoveAll(dir) }) + + initGitRepo(t, dir) + require.NoError(t, os.WriteFile(filepath.Join(dir, "file.txt"), []byte("content"), 0o644)) + require.NoError(t, os.WriteFile(filepath.Join(dir, "file.log"), []byte("content"), 0o644)) + require.NoError(t, os.WriteFile(filepath.Join(dir, ".gitignore"), []byte("*.log\n"), 0o644)) + + globs := []*ast.Glob{ + {Glob: "./*"}, + } + + // WithGitignore(false, ...) should not filter + files, err := Globs(dir, globs, false) + require.NoError(t, err) + + hasLog := false + for _, f := range files { + if filepath.Base(f) == "file.log" { + hasLog = true + break + } + } + assert.True(t, hasLog, "file.log should be present when gitignore is disabled") +} + +func TestGlobsWithGitignoreNoRepo(t *testing.T) { + t.Parallel() + + dir, err := os.MkdirTemp("", "task-gitignore-norepo-*") + require.NoError(t, err) + t.Cleanup(func() { os.RemoveAll(dir) }) + + require.NoError(t, os.WriteFile(filepath.Join(dir, "file.txt"), []byte("content"), 0o644)) + + globs := []*ast.Glob{ + {Glob: "./*"}, + } + + // Should not error and should return all files + files, err := Globs(dir, globs, true) + require.NoError(t, err) + assert.Len(t, files, 1) +} diff --git a/internal/fingerprint/glob.go b/internal/fingerprint/glob.go index fd3cafceaa..c87c9ec3b5 100644 --- a/internal/fingerprint/glob.go +++ b/internal/fingerprint/glob.go @@ -10,7 +10,7 @@ import ( "github.com/go-task/task/v3/taskfile/ast" ) -func Globs(dir string, globs []*ast.Glob) ([]string, error) { +func Globs(dir string, globs []*ast.Glob, gitignore bool) ([]string, error) { resultMap := make(map[string]bool) for _, g := range globs { matches, err := glob(dir, g.Glob) @@ -21,6 +21,11 @@ func Globs(dir string, globs []*ast.Glob) ([]string, error) { resultMap[match] = !g.Negate } } + + if gitignore { + resultMap = filterGitignored(resultMap, dir) + } + return collectKeys(resultMap), nil } diff --git a/internal/fingerprint/sources_checksum.go b/internal/fingerprint/sources_checksum.go index 16a98c1640..c9c0606b8f 100644 --- a/internal/fingerprint/sources_checksum.go +++ b/internal/fingerprint/sources_checksum.go @@ -88,7 +88,7 @@ func (*ChecksumChecker) Kind() string { } func (c *ChecksumChecker) checksum(t *ast.Task) (string, error) { - sources, err := Globs(t.Dir, t.Sources) + sources, err := Globs(t.Dir, t.Sources, t.IsGitignore()) if err != nil { return "", err } diff --git a/internal/fingerprint/sources_timestamp.go b/internal/fingerprint/sources_timestamp.go index b1a6f299d5..d413979211 100644 --- a/internal/fingerprint/sources_timestamp.go +++ b/internal/fingerprint/sources_timestamp.go @@ -28,11 +28,11 @@ func (checker *TimestampChecker) IsUpToDate(t *ast.Task) (bool, error) { return false, nil } - sources, err := Globs(t.Dir, t.Sources) + sources, err := Globs(t.Dir, t.Sources, t.IsGitignore()) if err != nil { return false, nil } - generates, err := Globs(t.Dir, t.Generates) + generates, err := Globs(t.Dir, t.Generates, t.IsGitignore()) if err != nil { return false, nil } @@ -90,7 +90,7 @@ func (checker *TimestampChecker) Kind() string { // Value implements the Checker Interface func (checker *TimestampChecker) Value(t *ast.Task) (any, error) { - sources, err := Globs(t.Dir, t.Sources) + sources, err := Globs(t.Dir, t.Sources, t.IsGitignore()) if err != nil { return time.Now(), err } diff --git a/task_test.go b/task_test.go index a09e496f8b..d4ab2d9a35 100644 --- a/task_test.go +++ b/task_test.go @@ -555,6 +555,55 @@ func TestStatusChecksum(t *testing.T) { // nolint:paralleltest // cannot run in } } +func TestGitignoreChecksum(t *testing.T) { //nolint:paralleltest // cannot run in parallel + const dir = "testdata/gitignore" + + // Clean up + _ = os.RemoveAll(filepathext.SmartJoin(dir, ".task")) + _ = os.Remove(filepathext.SmartJoin(dir, "generated.txt")) + + var buff bytes.Buffer + tempDir := task.TempDir{ + Remote: filepathext.SmartJoin(dir, ".task"), + Fingerprint: filepathext.SmartJoin(dir, ".task"), + } + e := task.NewExecutor( + task.WithDir(dir), + task.WithStdout(&buff), + task.WithStderr(&buff), + task.WithTempDir(tempDir), + ) + require.NoError(t, e.Setup()) + + // First run - should execute + require.NoError(t, e.Run(t.Context(), &task.Call{Task: "build"})) + + // Second run - should be up to date + buff.Reset() + require.NoError(t, e.Run(t.Context(), &task.Call{Task: "build"})) + assert.Equal(t, "task: Task \"build\" is up to date\n", buff.String()) + + // Modify the ignored file - should still be up to date + require.NoError(t, os.WriteFile(filepathext.SmartJoin(dir, "ignored.txt"), []byte("modified\n"), 0o644)) + buff.Reset() + require.NoError(t, e.Run(t.Context(), &task.Call{Task: "build"})) + assert.Equal(t, "task: Task \"build\" is up to date\n", buff.String()) + + // Modify the source file - should re-execute + require.NoError(t, os.WriteFile(filepathext.SmartJoin(dir, "source.txt"), []byte("modified source\n"), 0o644)) + buff.Reset() + require.NoError(t, e.Run(t.Context(), &task.Call{Task: "build"})) + assert.NotEqual(t, "task: Task \"build\" is up to date\n", buff.String()) + + // Restore source file + require.NoError(t, os.WriteFile(filepathext.SmartJoin(dir, "source.txt"), []byte("source content\n"), 0o644)) + require.NoError(t, os.WriteFile(filepathext.SmartJoin(dir, "ignored.txt"), []byte("ignored content\n"), 0o644)) + + // Clean up + _ = os.RemoveAll(filepathext.SmartJoin(dir, ".task")) + _ = os.Remove(filepathext.SmartJoin(dir, "generated.txt")) +} + func TestStatusVariables(t *testing.T) { t.Parallel() diff --git a/taskfile/ast/task.go b/taskfile/ast/task.go index 0e9893943e..30212ab613 100644 --- a/taskfile/ast/task.go +++ b/taskfile/ast/task.go @@ -38,6 +38,7 @@ type Task struct { Method string Prefix string `hash:"ignore"` IgnoreError bool + Gitignore *bool Run string Platforms []*Platform If string @@ -75,6 +76,12 @@ func (t *Task) IsSilent() bool { return t.Silent != nil && *t.Silent } +// IsGitignore returns true if the task has gitignore filtering explicitly enabled. +// Returns false if Gitignore is nil (not set) or explicitly set to false. +func (t *Task) IsGitignore() bool { + return t.Gitignore != nil && *t.Gitignore +} + // WildcardMatch will check if the given string matches the name of the Task and returns any wildcard values. func (t *Task) WildcardMatch(name string) (bool, []string) { names := append([]string{t.Task}, t.Aliases...) @@ -150,6 +157,7 @@ func (t *Task) UnmarshalYAML(node *yaml.Node) error { Method string Prefix string IgnoreError bool `yaml:"ignore_error"` + Gitignore *bool `yaml:"gitignore,omitempty"` Run string Platforms []*Platform If string @@ -190,6 +198,7 @@ func (t *Task) UnmarshalYAML(node *yaml.Node) error { t.Method = task.Method t.Prefix = task.Prefix t.IgnoreError = task.IgnoreError + t.Gitignore = deepcopy.Scalar(task.Gitignore) t.Run = task.Run t.Platforms = task.Platforms t.If = task.If @@ -233,6 +242,7 @@ func (t *Task) DeepCopy() *Task { Method: t.Method, Prefix: t.Prefix, IgnoreError: t.IgnoreError, + Gitignore: deepcopy.Scalar(t.Gitignore), Run: t.Run, IncludeVars: t.IncludeVars.DeepCopy(), IncludedTaskfileVars: t.IncludedTaskfileVars.DeepCopy(), diff --git a/taskfile/ast/taskfile.go b/taskfile/ast/taskfile.go index 4e3a3e4255..9216f39660 100644 --- a/taskfile/ast/taskfile.go +++ b/taskfile/ast/taskfile.go @@ -30,10 +30,11 @@ type Taskfile struct { Vars *Vars Env *Vars Tasks *Tasks - Silent bool - Dotenv []string - Run string - Interval time.Duration + Silent bool + Dotenv []string + Run string + Interval time.Duration + Gitignore bool } // Merge merges the second Taskfile into the first @@ -88,7 +89,8 @@ func (tf *Taskfile) UnmarshalYAML(node *yaml.Node) error { Silent bool Dotenv []string Run string - Interval time.Duration + Interval time.Duration + Gitignore bool } if err := node.Decode(&taskfile); err != nil { return errors.NewTaskfileDecodeError(err, node) @@ -106,6 +108,7 @@ func (tf *Taskfile) UnmarshalYAML(node *yaml.Node) error { tf.Dotenv = taskfile.Dotenv tf.Run = taskfile.Run tf.Interval = taskfile.Interval + tf.Gitignore = taskfile.Gitignore if tf.Includes == nil { tf.Includes = NewIncludes() } diff --git a/testdata/gitignore/.gitignore b/testdata/gitignore/.gitignore new file mode 100644 index 0000000000..f89d64dab6 --- /dev/null +++ b/testdata/gitignore/.gitignore @@ -0,0 +1 @@ +ignored.txt diff --git a/testdata/gitignore/Taskfile.yml b/testdata/gitignore/Taskfile.yml new file mode 100644 index 0000000000..c78e5a0efb --- /dev/null +++ b/testdata/gitignore/Taskfile.yml @@ -0,0 +1,25 @@ +version: '3' + +gitignore: true + +tasks: + build: + cmds: + - cp ./source.txt ./generated.txt + sources: + - ./*.txt + - exclude: ./generated.txt + generates: + - ./generated.txt + method: checksum + + build-no-gitignore: + gitignore: false + cmds: + - cp ./source.txt ./generated.txt + sources: + - ./*.txt + - exclude: ./generated.txt + generates: + - ./generated.txt + method: checksum diff --git a/testdata/gitignore/source.txt b/testdata/gitignore/source.txt new file mode 100644 index 0000000000..48763f0349 --- /dev/null +++ b/testdata/gitignore/source.txt @@ -0,0 +1 @@ +source content diff --git a/variables.go b/variables.go index c7c6cc8493..18d716a548 100644 --- a/variables.go +++ b/variables.go @@ -59,6 +59,7 @@ func (e *Executor) CompiledTaskForTaskList(call *Call) (*ast.Task, error) { Env: nil, Dotenv: origTask.Dotenv, Silent: deepcopy.Scalar(origTask.Silent), + Gitignore: deepcopy.Scalar(origTask.Gitignore), Interactive: origTask.Interactive, Internal: origTask.Internal, Method: origTask.Method, @@ -110,6 +111,11 @@ func (e *Executor) compiledTask(call *Call, evaluateShVars bool) (*ast.Task, err } } + gitignore := origTask.IsGitignore() + if origTask.Gitignore == nil { + gitignore = e.Taskfile.Gitignore + } + new := ast.Task{ Task: origTask.Task, Label: templater.Replace(origTask.Label, cache), @@ -126,6 +132,7 @@ func (e *Executor) compiledTask(call *Call, evaluateShVars bool) (*ast.Task, err Env: nil, Dotenv: templater.Replace(origTask.Dotenv, cache), Silent: deepcopy.Scalar(origTask.Silent), + Gitignore: &gitignore, Interactive: origTask.Interactive, Internal: origTask.Internal, Method: templater.Replace(origTask.Method, cache), @@ -219,7 +226,7 @@ func (e *Executor) compiledTask(call *Call, evaluateShVars bool) (*ast.Task, err continue } if cmd.For != nil { - list, keys, err := itemsFromFor(cmd.For, new.Dir, new.Sources, new.Generates, vars, origTask.Location, cache) + list, keys, err := itemsFromFor(cmd.For, new.Dir, new.Sources, new.Generates, gitignore, vars, origTask.Location, cache) if err != nil { return nil, err } @@ -268,7 +275,7 @@ func (e *Executor) compiledTask(call *Call, evaluateShVars bool) (*ast.Task, err continue } if dep.For != nil { - list, keys, err := itemsFromFor(dep.For, new.Dir, new.Sources, new.Generates, vars, origTask.Location, cache) + list, keys, err := itemsFromFor(dep.For, new.Dir, new.Sources, new.Generates, gitignore, vars, origTask.Location, cache) if err != nil { return nil, err } @@ -339,6 +346,7 @@ func itemsFromFor( dir string, sources []*ast.Glob, generates []*ast.Glob, + gitignore bool, vars *ast.Vars, location *ast.Location, cache *templater.Cache, @@ -361,7 +369,7 @@ func itemsFromFor( } // Get the list from the task sources if f.From == "sources" { - glist, err := fingerprint.Globs(dir, sources) + glist, err := fingerprint.Globs(dir, sources, gitignore) if err != nil { return nil, nil, err } @@ -375,7 +383,7 @@ func itemsFromFor( } // Get the list from the task generates if f.From == "generates" { - glist, err := fingerprint.Globs(dir, generates) + glist, err := fingerprint.Globs(dir, generates, gitignore) if err != nil { return nil, nil, err } diff --git a/watch.go b/watch.go index 8e7f7ccf7d..349dd57a69 100644 --- a/watch.go +++ b/watch.go @@ -205,7 +205,7 @@ func (e *Executor) collectSources(calls []*Call) ([]string, error) { var sources []string err := e.traverse(calls, func(task *ast.Task) error { - files, err := fingerprint.Globs(task.Dir, task.Sources) + files, err := fingerprint.Globs(task.Dir, task.Sources, task.IsGitignore()) if err != nil { return err } diff --git a/website/src/public/schema.json b/website/src/public/schema.json index 2210952d62..37751fa4fe 100644 --- a/website/src/public/schema.json +++ b/website/src/public/schema.json @@ -177,6 +177,11 @@ "enum": ["none", "checksum", "timestamp"], "default": "none" }, + "gitignore": { + "description": "When set to true, files matching .gitignore rules will be excluded from sources and generates glob resolution. Overrides the global gitignore setting.", + "type": "boolean", + "default": false + }, "prefix": { "description": "Defines a string to prefix the output of tasks running in parallel. Only used when the output mode is `prefixed`.", "type": "string" @@ -687,6 +692,11 @@ "enum": ["none", "checksum", "timestamp"], "default": "checksum" }, + "gitignore": { + "description": "When set to true, files matching .gitignore rules will be excluded from sources and generates glob resolution for all tasks. Can be overridden per task.", + "type": "boolean", + "default": false + }, "includes": { "description": "Imports tasks from the specified taskfiles. The tasks described in the given Taskfiles will be available with the informed namespace.", "type": "object", From 5f148e23e12df679a075406473b90ecced6b4aeb Mon Sep 17 00:00:00 2001 From: Valentin Maerten Date: Fri, 3 Apr 2026 22:54:41 +0200 Subject: [PATCH 2/6] refactor: replace go-git with sabhiram/go-gitignore for lighter dependency go-git pulled ~30 transitive dependencies and recursively walked the entire worktree on every Globs() call. Replace with sabhiram/go-gitignore (zero dependencies) and a simple walk from task dir up to rootDir to collect .gitignore files. Pass rootDir (Taskfile ROOT_DIR) through the checker chain to bound the search scope. --- go.mod | 17 +--- go.sum | 51 +----------- internal/fingerprint/gitignore.go | 94 ++++++++++++++--------- internal/fingerprint/gitignore_test.go | 45 +++++------ internal/fingerprint/glob.go | 4 +- internal/fingerprint/sources.go | 6 +- internal/fingerprint/sources_checksum.go | 6 +- internal/fingerprint/sources_timestamp.go | 10 ++- internal/fingerprint/task.go | 9 ++- status.go | 2 +- task.go | 1 + variables.go | 13 ++-- watch.go | 2 +- 13 files changed, 115 insertions(+), 145 deletions(-) diff --git a/go.mod b/go.mod index 279ef5c0d3..a9316af924 100644 --- a/go.mod +++ b/go.mod @@ -22,6 +22,7 @@ require ( github.com/joho/godotenv v1.5.1 github.com/mitchellh/hashstructure/v2 v2.0.2 github.com/puzpuzpuz/xsync/v4 v4.4.0 + github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 github.com/sajari/fuzzy v1.0.0 github.com/sebdah/goldie/v2 v2.8.0 github.com/spf13/pflag v1.0.10 @@ -43,12 +44,9 @@ require ( cloud.google.com/go/iam v1.5.3 // indirect cloud.google.com/go/monitoring v1.24.3 // indirect cloud.google.com/go/storage v1.60.0 // indirect - dario.cat/mergo v1.0.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 // indirect - github.com/Microsoft/go-winio v0.6.2 // indirect - github.com/ProtonMail/go-crypto v1.1.6 // indirect github.com/atotto/clipboard v0.1.4 // indirect github.com/aws/aws-sdk-go-v2 v1.41.1 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 // indirect @@ -79,30 +77,21 @@ require ( github.com/charmbracelet/x/windows v0.2.2 // indirect github.com/clipperhouse/displaywidth v0.11.0 // indirect github.com/clipperhouse/uax29/v2 v2.7.0 // indirect - github.com/cloudflare/circl v1.6.3 // indirect github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 // indirect - github.com/cyphar/filepath-securejoin v0.4.1 // indirect github.com/dlclark/regexp2 v1.11.5 // indirect github.com/dustin/go-humanize v1.0.1 // indirect - github.com/emirpasic/gods v1.18.1 // indirect github.com/envoyproxy/go-control-plane/envoy v1.36.0 // indirect github.com/envoyproxy/protoc-gen-validate v1.3.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect - github.com/go-git/go-billy/v5 v5.8.0 // indirect - github.com/go-git/go-git/v5 v5.17.2 // indirect github.com/go-jose/go-jose/v4 v4.1.4 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/google/s2a-go v0.1.9 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.11 // indirect github.com/googleapis/gax-go/v2 v2.17.0 // indirect github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.70 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-version v1.8.0 // indirect - github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect - github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/klauspost/compress v1.18.4 // indirect github.com/klauspost/cpuid/v2 v2.2.10 // indirect github.com/klauspost/pgzip v1.2.6 // indirect @@ -113,18 +102,15 @@ require ( github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/pierrec/lz4/v4 v4.1.22 // indirect - github.com/pjbgf/sha1cd v0.3.2 // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect - github.com/skeema/knownhosts v1.3.1 // indirect github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/u-root/u-root v0.15.1-0.20251208185023-2f8c7e763cf8 // indirect github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 // indirect github.com/ulikunitz/xz v0.5.15 // indirect - github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/detectors/gcp v1.39.0 // indirect @@ -147,6 +133,5 @@ require ( google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 // indirect google.golang.org/grpc v1.79.3 // indirect google.golang.org/protobuf v1.36.11 // indirect - gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index fc74b31ac2..e231960abb 100644 --- a/go.sum +++ b/go.sum @@ -26,8 +26,6 @@ cloud.google.com/go/storage v1.60.0 h1:oBfZrSOCimggVNz9Y/bXY35uUcts7OViubeddTTVz cloud.google.com/go/storage v1.60.0/go.mod h1:q+5196hXfejkctrnx+VYU8RKQr/L3c0cBIlrjmiAKE0= cloud.google.com/go/trace v1.11.7 h1:kDNDX8JkaAG3R2nq1lIdkb7FCSi1rCmsEtKVsty7p+U= cloud.google.com/go/trace v1.11.7/go.mod h1:TNn9d5V3fQVf6s4SCveVMIBS2LJUqo73GACmq/Tky0s= -dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= -dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 h1:sBEjpZlNHzK1voKq9695PJSX2o5NEXl7/OL3coiIY0c= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 h1:UnDZ/zFfG1JhH/DqxIZYU/1CUAlTUScoXD/LcM2Ykk8= @@ -40,11 +38,6 @@ github.com/Ladicle/tabwriter v1.0.0 h1:DZQqPvMumBDwVNElso13afjYLNp0Z7pHqHnu0r4t9 github.com/Ladicle/tabwriter v1.0.0/go.mod h1:c4MdCjxQyTbGuQO/gvqJ+IA/89UEwrsD6hUCW98dyp4= github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= github.com/Masterminds/semver/v3 v3.4.0/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.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= -github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= -github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw= -github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= github.com/alecthomas/chroma/v2 v2.23.1 h1:nv2AVZdTyClGbVQkIzlDm/rnhk1E9bU9nXwmZ/Vk/iY= @@ -117,14 +110,10 @@ github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSE github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0= github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= -github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= -github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 h1:6xNmx7iTtyBRev0+D/Tv1FZd4SCg8axKApyNyRsAt/w= github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI= 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= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -137,8 +126,6 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= 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/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA= github.com/envoyproxy/go-control-plane v0.14.0/go.mod h1:NcS5X47pLl/hfqxU70yPwL9ZMkUlwlKxtAohpi2wBEU= github.com/envoyproxy/go-control-plane/envoy v1.36.0 h1:yg/JjO5E7ubRyKX3m07GF3reDNEnfOboJ0QySbH736g= @@ -153,12 +140,6 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= -github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= -github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= -github.com/go-git/go-billy/v5 v5.8.0 h1:I8hjc3LbBlXTtVuFNJuwYuMiHvQJDq1AT6u4DwDzZG0= -github.com/go-git/go-billy/v5 v5.8.0/go.mod h1:RpvI/rw4Vr5QA+Z60c6d6LXH0rYJo0uD5SqfmrrheCY= -github.com/go-git/go-git/v5 v5.17.2 h1:B+nkdlxdYrvyFK4GPXVU8w1U+YkbsgciIR7f2sZJ104= -github.com/go-git/go-git/v5 v5.17.2/go.mod h1:pW/VmeqkanRFqR6AljLcs7EA7FbZaN5MQqO7oZADXpo= github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA= github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -172,8 +153,6 @@ 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.2.0 h1:xW7ek0o65FUSTbKcSNeg2Vyf/I7wYXFgLUznptvviBE= github.com/go-task/template v0.2.0/go.mod h1:dbdoUb6qKnHQi1y6o+IdIrs0J4o/SEhSTA6bbzZmdtc= -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/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= @@ -198,12 +177,8 @@ github.com/hashicorp/go-version v1.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bP github.com/hashicorp/go-version v1.8.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= 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= -github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= -github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c= github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= @@ -233,10 +208,7 @@ github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELU github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU= github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= -github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4= -github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -248,6 +220,8 @@ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 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/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 h1:OkMGxebDjyw0ULyrTYWeN0UNCCkmCWfjPnIA2W6oviI= +github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06/go.mod h1:+ePHsJ1keEjQtpvf9HHw0f4ZeJ0TLRsxhunSI2hYJSs= github.com/sajari/fuzzy v1.0.0 h1:+FmwVvJErsd0d0hAPlj4CxqxUtQY/fOoY0DwX4ykpRY= github.com/sajari/fuzzy v1.0.0/go.mod h1:OjYR6KxoWOe9+dOlXeiCJd4dIbED4Oo8wpS89o0pwOo= github.com/sebdah/goldie/v2 v2.8.0 h1:dZb9wR8q5++oplmEiJT+U/5KyotVD+HNGCAc5gNr8rc= @@ -255,9 +229,6 @@ github.com/sebdah/goldie/v2 v2.8.0/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvK github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= 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.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8= -github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo= @@ -265,9 +236,9 @@ github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xI github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/u-root/u-root v0.15.1-0.20251208185023-2f8c7e763cf8 h1:cq+DjLAjz3ZPwh0+G571O/jMH0c0DzReDPLjQGL2/BA= @@ -276,8 +247,6 @@ github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 h1:pyC9PaHYZFgEKFdlp3G8 github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701/go.mod h1:P3a5rG4X7tI17Nn3aOIAYr5HbIMukwXG0urG0WuL8OA= github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY= github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= -github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= -github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= @@ -306,36 +275,25 @@ go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZY go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw= golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM= -golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU= golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/api v0.267.0 h1:w+vfWPMPYeRs8qH1aYYsFX68jMls5acWl/jocfLomwE= @@ -354,10 +312,9 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 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= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= -gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= 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.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 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/moreinterp v0.0.0-20260120230322-19def062a997 h1:3bbJwtPFh98dJ6lxRdR3eLHTH1CmR3BcU6TriIMiXjE= diff --git a/internal/fingerprint/gitignore.go b/internal/fingerprint/gitignore.go index 0b9ad2053d..43b40671e0 100644 --- a/internal/fingerprint/gitignore.go +++ b/internal/fingerprint/gitignore.go @@ -1,58 +1,82 @@ package fingerprint import ( + "bufio" + "os" "path/filepath" "strings" - "github.com/go-git/go-git/v5" - "github.com/go-git/go-git/v5/plumbing/format/gitignore" + ignore "github.com/sabhiram/go-gitignore" ) -// filterGitignored removes entries from the file map that match gitignore rules. -// Files are expected to be absolute paths. The dir parameter is used to find the git repository. -// Returns the input map unchanged if the directory is not inside a git repository. -func filterGitignored(files map[string]bool, dir string) map[string]bool { - repo, err := git.PlainOpenWithOptions(dir, &git.PlainOpenOptions{ - DetectDotGit: true, - }) - if err != nil { - return files - } - - wt, err := repo.Worktree() - if err != nil { - return files - } +type gitignoreRule struct { + dir string + matcher *ignore.GitIgnore +} - var allPatterns []gitignore.Pattern +// loadGitignoreRules reads .gitignore files walking up from dir to rootDir. +func loadGitignoreRules(rootDir, dir string) []gitignoreRule { + rootDir, _ = filepath.Abs(rootDir) + dir, _ = filepath.Abs(dir) - if ps, err := gitignore.LoadSystemPatterns(wt.Filesystem); err == nil { - allPatterns = append(allPatterns, ps...) + var rules []gitignoreRule + current := dir + for { + lines := readGitignoreLines(filepath.Join(current, ".gitignore")) + if len(lines) > 0 { + rules = append(rules, gitignoreRule{ + dir: current, + matcher: ignore.CompileIgnoreLines(lines...), + }) + } + if current == rootDir { + break + } + parent := filepath.Dir(current) + if parent == current { + break + } + current = parent } - if ps, err := gitignore.LoadGlobalPatterns(wt.Filesystem); err == nil { - allPatterns = append(allPatterns, ps...) + return rules +} + +func readGitignoreLines(path string) []string { + f, err := os.Open(path) + if err != nil { + return nil } + defer f.Close() - if ps, err := gitignore.ReadPatterns(wt.Filesystem, nil); err == nil { - allPatterns = append(allPatterns, ps...) + var lines []string + scanner := bufio.NewScanner(f) + for scanner.Scan() { + line := scanner.Text() + if line != "" && !strings.HasPrefix(line, "#") { + lines = append(lines, line) + } } + return lines +} - if len(allPatterns) == 0 { +// filterGitignored removes entries from the file map that match gitignore rules. +func filterGitignored(files map[string]bool, rootDir, dir string) map[string]bool { + rules := loadGitignoreRules(rootDir, dir) + if len(rules) == 0 { return files } - matcher := gitignore.NewMatcher(allPatterns) - gitRoot := wt.Filesystem.Root() - for path := range files { - relPath, err := filepath.Rel(gitRoot, path) - if err != nil { - continue - } - pathComponents := strings.Split(filepath.ToSlash(relPath), "/") - if matcher.Match(pathComponents, false) { - files[path] = false + for _, rule := range rules { + relPath, err := filepath.Rel(rule.dir, path) + if err != nil || strings.HasPrefix(relPath, "..") { + continue + } + if rule.matcher.MatchesPath(filepath.ToSlash(relPath)) { + files[path] = false + break + } } } diff --git a/internal/fingerprint/gitignore_test.go b/internal/fingerprint/gitignore_test.go index 8eb0dbfdbb..2a09fdb655 100644 --- a/internal/fingerprint/gitignore_test.go +++ b/internal/fingerprint/gitignore_test.go @@ -5,7 +5,6 @@ import ( "path/filepath" "testing" - "github.com/go-git/go-git/v5" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -14,8 +13,7 @@ import ( func initGitRepo(t *testing.T, dir string) { t.Helper() - _, err := git.PlainInit(dir, false) - require.NoError(t, err) + require.NoError(t, os.MkdirAll(filepath.Join(dir, ".git"), 0o755)) } func TestGlobsWithGitignore(t *testing.T) { @@ -27,27 +25,21 @@ func TestGlobsWithGitignore(t *testing.T) { initGitRepo(t, dir) - // Create test files require.NoError(t, os.WriteFile(filepath.Join(dir, "included.txt"), []byte("included"), 0o644)) require.NoError(t, os.WriteFile(filepath.Join(dir, "ignored.log"), []byte("ignored"), 0o644)) require.NoError(t, os.WriteFile(filepath.Join(dir, "also-included.txt"), []byte("also included"), 0o644)) - - // Create .gitignore require.NoError(t, os.WriteFile(filepath.Join(dir, ".gitignore"), []byte("*.log\n"), 0o644)) globs := []*ast.Glob{ {Glob: "./*"}, } - // Without gitignore - should include all files - filesWithout, err := Globs(dir, globs, false) + filesWithout, err := Globs(dir, globs, false, dir) require.NoError(t, err) - // With gitignore - should exclude .log files - filesWith, err := Globs(dir, globs, true) + filesWith, err := Globs(dir, globs, true, dir) require.NoError(t, err) - // The .log file should be in the unfiltered list hasLog := false for _, f := range filesWithout { if filepath.Base(f) == "ignored.log" { @@ -57,7 +49,6 @@ func TestGlobsWithGitignore(t *testing.T) { } assert.True(t, hasLog, "ignored.log should be present without gitignore filter") - // The .log file should NOT be in the filtered list hasLog = false for _, f := range filesWith { if filepath.Base(f) == "ignored.log" { @@ -67,7 +58,6 @@ func TestGlobsWithGitignore(t *testing.T) { } assert.False(t, hasLog, "ignored.log should be excluded with gitignore filter") - // .txt files should still be present txtCount := 0 for _, f := range filesWith { if filepath.Ext(f) == ".txt" { @@ -77,34 +67,36 @@ func TestGlobsWithGitignore(t *testing.T) { assert.Equal(t, 2, txtCount, "both .txt files should remain") } -func TestGlobsWithGitignoreDisabled(t *testing.T) { +func TestGlobsWithGitignoreNested(t *testing.T) { t.Parallel() - dir, err := os.MkdirTemp("", "task-gitignore-disabled-*") + dir, err := os.MkdirTemp("", "task-gitignore-nested-*") require.NoError(t, err) t.Cleanup(func() { os.RemoveAll(dir) }) initGitRepo(t, dir) - require.NoError(t, os.WriteFile(filepath.Join(dir, "file.txt"), []byte("content"), 0o644)) - require.NoError(t, os.WriteFile(filepath.Join(dir, "file.log"), []byte("content"), 0o644)) + + subDir := filepath.Join(dir, "sub") + require.NoError(t, os.MkdirAll(subDir, 0o755)) + + require.NoError(t, os.WriteFile(filepath.Join(subDir, "keep.txt"), []byte("keep"), 0o644)) + require.NoError(t, os.WriteFile(filepath.Join(subDir, "build.out"), []byte("build"), 0o644)) + + // Root .gitignore ignores *.log require.NoError(t, os.WriteFile(filepath.Join(dir, ".gitignore"), []byte("*.log\n"), 0o644)) + // Nested .gitignore ignores *.out + require.NoError(t, os.WriteFile(filepath.Join(subDir, ".gitignore"), []byte("*.out\n"), 0o644)) globs := []*ast.Glob{ {Glob: "./*"}, } - // WithGitignore(false, ...) should not filter - files, err := Globs(dir, globs, false) + files, err := Globs(subDir, globs, true, dir) require.NoError(t, err) - hasLog := false for _, f := range files { - if filepath.Base(f) == "file.log" { - hasLog = true - break - } + assert.NotEqual(t, "build.out", filepath.Base(f), "build.out should be excluded by nested .gitignore") } - assert.True(t, hasLog, "file.log should be present when gitignore is disabled") } func TestGlobsWithGitignoreNoRepo(t *testing.T) { @@ -120,8 +112,7 @@ func TestGlobsWithGitignoreNoRepo(t *testing.T) { {Glob: "./*"}, } - // Should not error and should return all files - files, err := Globs(dir, globs, true) + files, err := Globs(dir, globs, true, dir) require.NoError(t, err) assert.Len(t, files, 1) } diff --git a/internal/fingerprint/glob.go b/internal/fingerprint/glob.go index c87c9ec3b5..f19da358b4 100644 --- a/internal/fingerprint/glob.go +++ b/internal/fingerprint/glob.go @@ -10,7 +10,7 @@ import ( "github.com/go-task/task/v3/taskfile/ast" ) -func Globs(dir string, globs []*ast.Glob, gitignore bool) ([]string, error) { +func Globs(dir string, globs []*ast.Glob, gitignore bool, rootDir string) ([]string, error) { resultMap := make(map[string]bool) for _, g := range globs { matches, err := glob(dir, g.Glob) @@ -23,7 +23,7 @@ func Globs(dir string, globs []*ast.Glob, gitignore bool) ([]string, error) { } if gitignore { - resultMap = filterGitignored(resultMap, dir) + resultMap = filterGitignored(resultMap, rootDir, dir) } return collectKeys(resultMap), nil diff --git a/internal/fingerprint/sources.go b/internal/fingerprint/sources.go index 34d3a04bee..54303a464d 100644 --- a/internal/fingerprint/sources.go +++ b/internal/fingerprint/sources.go @@ -2,12 +2,12 @@ package fingerprint import "fmt" -func NewSourcesChecker(method, tempDir string, dry bool) (SourcesCheckable, error) { +func NewSourcesChecker(method, tempDir string, dry bool, rootDir string) (SourcesCheckable, error) { switch method { case "timestamp": - return NewTimestampChecker(tempDir, dry), nil + return NewTimestampChecker(tempDir, dry, rootDir), nil case "checksum": - return NewChecksumChecker(tempDir, dry), nil + return NewChecksumChecker(tempDir, dry, rootDir), nil case "none": return NoneChecker{}, nil default: diff --git a/internal/fingerprint/sources_checksum.go b/internal/fingerprint/sources_checksum.go index c9c0606b8f..22d2d68eff 100644 --- a/internal/fingerprint/sources_checksum.go +++ b/internal/fingerprint/sources_checksum.go @@ -19,12 +19,14 @@ import ( type ChecksumChecker struct { tempDir string dry bool + rootDir string } -func NewChecksumChecker(tempDir string, dry bool) *ChecksumChecker { +func NewChecksumChecker(tempDir string, dry bool, rootDir string) *ChecksumChecker { return &ChecksumChecker{ tempDir: tempDir, dry: dry, + rootDir: rootDir, } } @@ -88,7 +90,7 @@ func (*ChecksumChecker) Kind() string { } func (c *ChecksumChecker) checksum(t *ast.Task) (string, error) { - sources, err := Globs(t.Dir, t.Sources, t.IsGitignore()) + sources, err := Globs(t.Dir, t.Sources, t.IsGitignore(), c.rootDir) if err != nil { return "", err } diff --git a/internal/fingerprint/sources_timestamp.go b/internal/fingerprint/sources_timestamp.go index d413979211..4d7e7ee334 100644 --- a/internal/fingerprint/sources_timestamp.go +++ b/internal/fingerprint/sources_timestamp.go @@ -13,12 +13,14 @@ import ( type TimestampChecker struct { tempDir string dry bool + rootDir string } -func NewTimestampChecker(tempDir string, dry bool) *TimestampChecker { +func NewTimestampChecker(tempDir string, dry bool, rootDir string) *TimestampChecker { return &TimestampChecker{ tempDir: tempDir, dry: dry, + rootDir: rootDir, } } @@ -28,11 +30,11 @@ func (checker *TimestampChecker) IsUpToDate(t *ast.Task) (bool, error) { return false, nil } - sources, err := Globs(t.Dir, t.Sources, t.IsGitignore()) + sources, err := Globs(t.Dir, t.Sources, t.IsGitignore(), checker.rootDir) if err != nil { return false, nil } - generates, err := Globs(t.Dir, t.Generates, t.IsGitignore()) + generates, err := Globs(t.Dir, t.Generates, t.IsGitignore(), checker.rootDir) if err != nil { return false, nil } @@ -90,7 +92,7 @@ func (checker *TimestampChecker) Kind() string { // Value implements the Checker Interface func (checker *TimestampChecker) Value(t *ast.Task) (any, error) { - sources, err := Globs(t.Dir, t.Sources, t.IsGitignore()) + sources, err := Globs(t.Dir, t.Sources, t.IsGitignore(), checker.rootDir) if err != nil { return time.Now(), err } diff --git a/internal/fingerprint/task.go b/internal/fingerprint/task.go index 2b48e114c9..ca8e18b9ab 100644 --- a/internal/fingerprint/task.go +++ b/internal/fingerprint/task.go @@ -13,6 +13,7 @@ type ( method string dry bool tempDir string + rootDir string logger *logger.Logger statusChecker StatusCheckable sourcesChecker SourcesCheckable @@ -37,6 +38,12 @@ func WithTempDir(tempDir string) CheckerOption { } } +func WithRootDir(rootDir string) CheckerOption { + return func(config *CheckerConfig) { + config.rootDir = rootDir + } +} + func WithLogger(logger *logger.Logger) CheckerOption { return func(config *CheckerConfig) { config.logger = logger @@ -86,7 +93,7 @@ func IsTaskUpToDate( // If no sources checker was given, set up the default one if config.sourcesChecker == nil { - config.sourcesChecker, err = NewSourcesChecker(config.method, config.tempDir, config.dry) + config.sourcesChecker, err = NewSourcesChecker(config.method, config.tempDir, config.dry, config.rootDir) if err != nil { return false, err } diff --git a/status.go b/status.go index ae40f5ba5f..8680d52704 100644 --- a/status.go +++ b/status.go @@ -46,7 +46,7 @@ func (e *Executor) statusOnError(t *ast.Task) error { if method == "" { method = e.Taskfile.Method } - checker, err := fingerprint.NewSourcesChecker(method, e.TempDir.Fingerprint, e.Dry) + checker, err := fingerprint.NewSourcesChecker(method, e.TempDir.Fingerprint, e.Dry, e.Dir) if err != nil { return err } diff --git a/task.go b/task.go index 54cda92762..fa2842e4f6 100644 --- a/task.go +++ b/task.go @@ -231,6 +231,7 @@ func (e *Executor) RunTask(ctx context.Context, call *Call) error { fingerprint.WithTempDir(e.TempDir.Fingerprint), fingerprint.WithDry(e.Dry), fingerprint.WithLogger(e.Logger), + fingerprint.WithRootDir(e.Dir), ) if err != nil { return err diff --git a/variables.go b/variables.go index 18d716a548..7f4054bf06 100644 --- a/variables.go +++ b/variables.go @@ -203,9 +203,9 @@ func (e *Executor) compiledTask(call *Call, evaluateShVars bool) (*ast.Task, err var checker fingerprint.SourcesCheckable if origTask.Method == "timestamp" { - checker = fingerprint.NewTimestampChecker(e.TempDir.Fingerprint, e.Dry) + checker = fingerprint.NewTimestampChecker(e.TempDir.Fingerprint, e.Dry, e.Dir) } else { - checker = fingerprint.NewChecksumChecker(e.TempDir.Fingerprint, e.Dry) + checker = fingerprint.NewChecksumChecker(e.TempDir.Fingerprint, e.Dry, e.Dir) } value, err := checker.Value(&new) @@ -226,7 +226,7 @@ func (e *Executor) compiledTask(call *Call, evaluateShVars bool) (*ast.Task, err continue } if cmd.For != nil { - list, keys, err := itemsFromFor(cmd.For, new.Dir, new.Sources, new.Generates, gitignore, vars, origTask.Location, cache) + list, keys, err := itemsFromFor(cmd.For, new.Dir, new.Sources, new.Generates, gitignore, e.Dir, vars, origTask.Location, cache) if err != nil { return nil, err } @@ -275,7 +275,7 @@ func (e *Executor) compiledTask(call *Call, evaluateShVars bool) (*ast.Task, err continue } if dep.For != nil { - list, keys, err := itemsFromFor(dep.For, new.Dir, new.Sources, new.Generates, gitignore, vars, origTask.Location, cache) + list, keys, err := itemsFromFor(dep.For, new.Dir, new.Sources, new.Generates, gitignore, e.Dir, vars, origTask.Location, cache) if err != nil { return nil, err } @@ -347,6 +347,7 @@ func itemsFromFor( sources []*ast.Glob, generates []*ast.Glob, gitignore bool, + rootDir string, vars *ast.Vars, location *ast.Location, cache *templater.Cache, @@ -369,7 +370,7 @@ func itemsFromFor( } // Get the list from the task sources if f.From == "sources" { - glist, err := fingerprint.Globs(dir, sources, gitignore) + glist, err := fingerprint.Globs(dir, sources, gitignore, rootDir) if err != nil { return nil, nil, err } @@ -383,7 +384,7 @@ func itemsFromFor( } // Get the list from the task generates if f.From == "generates" { - glist, err := fingerprint.Globs(dir, generates, gitignore) + glist, err := fingerprint.Globs(dir, generates, gitignore, rootDir) if err != nil { return nil, nil, err } diff --git a/watch.go b/watch.go index 349dd57a69..a246ae55c7 100644 --- a/watch.go +++ b/watch.go @@ -205,7 +205,7 @@ func (e *Executor) collectSources(calls []*Call) ([]string, error) { var sources []string err := e.traverse(calls, func(task *ast.Task) error { - files, err := fingerprint.Globs(task.Dir, task.Sources, task.IsGitignore()) + files, err := fingerprint.Globs(task.Dir, task.Sources, task.IsGitignore(), e.Dir) if err != nil { return err } From 32c79713f3e68f3bf3f4b71b67b645295ef98fe4 Mon Sep 17 00:00:00 2001 From: Valentin Maerten Date: Sat, 4 Apr 2026 10:29:45 +0200 Subject: [PATCH 3/6] wip --- internal/fingerprint/gitignore.go | 20 ++++++++++++++------ internal/fingerprint/gitignore_test.go | 8 ++++---- internal/fingerprint/glob.go | 4 ++-- internal/fingerprint/sources.go | 6 +++--- internal/fingerprint/sources_checksum.go | 6 ++---- internal/fingerprint/sources_timestamp.go | 10 ++++------ internal/fingerprint/task.go | 9 +-------- status.go | 2 +- task.go | 1 - variables.go | 13 ++++++------- watch.go | 2 +- 11 files changed, 38 insertions(+), 43 deletions(-) diff --git a/internal/fingerprint/gitignore.go b/internal/fingerprint/gitignore.go index 43b40671e0..62287f0d8a 100644 --- a/internal/fingerprint/gitignore.go +++ b/internal/fingerprint/gitignore.go @@ -14,13 +14,16 @@ type gitignoreRule struct { matcher *ignore.GitIgnore } -// loadGitignoreRules reads .gitignore files walking up from dir to rootDir. -func loadGitignoreRules(rootDir, dir string) []gitignoreRule { - rootDir, _ = filepath.Abs(rootDir) +// loadGitignoreRules walks up from dir collecting .gitignore files. +// Stops at the first .git (file or directory) found. +// Returns nil if no .git is found (not in a git repo). +func loadGitignoreRules(dir string) []gitignoreRule { dir, _ = filepath.Abs(dir) var rules []gitignoreRule + foundGit := false current := dir + for { lines := readGitignoreLines(filepath.Join(current, ".gitignore")) if len(lines) > 0 { @@ -29,7 +32,8 @@ func loadGitignoreRules(rootDir, dir string) []gitignoreRule { matcher: ignore.CompileIgnoreLines(lines...), }) } - if current == rootDir { + if _, err := os.Stat(filepath.Join(current, ".git")); err == nil { + foundGit = true break } parent := filepath.Dir(current) @@ -39,6 +43,10 @@ func loadGitignoreRules(rootDir, dir string) []gitignoreRule { current = parent } + if !foundGit { + return nil + } + return rules } @@ -61,8 +69,8 @@ func readGitignoreLines(path string) []string { } // filterGitignored removes entries from the file map that match gitignore rules. -func filterGitignored(files map[string]bool, rootDir, dir string) map[string]bool { - rules := loadGitignoreRules(rootDir, dir) +func filterGitignored(files map[string]bool, dir string) map[string]bool { + rules := loadGitignoreRules(dir) if len(rules) == 0 { return files } diff --git a/internal/fingerprint/gitignore_test.go b/internal/fingerprint/gitignore_test.go index 2a09fdb655..1c5118f884 100644 --- a/internal/fingerprint/gitignore_test.go +++ b/internal/fingerprint/gitignore_test.go @@ -34,10 +34,10 @@ func TestGlobsWithGitignore(t *testing.T) { {Glob: "./*"}, } - filesWithout, err := Globs(dir, globs, false, dir) + filesWithout, err := Globs(dir, globs, false) require.NoError(t, err) - filesWith, err := Globs(dir, globs, true, dir) + filesWith, err := Globs(dir, globs, true) require.NoError(t, err) hasLog := false @@ -91,7 +91,7 @@ func TestGlobsWithGitignoreNested(t *testing.T) { {Glob: "./*"}, } - files, err := Globs(subDir, globs, true, dir) + files, err := Globs(subDir, globs, true) require.NoError(t, err) for _, f := range files { @@ -112,7 +112,7 @@ func TestGlobsWithGitignoreNoRepo(t *testing.T) { {Glob: "./*"}, } - files, err := Globs(dir, globs, true, dir) + files, err := Globs(dir, globs, true) require.NoError(t, err) assert.Len(t, files, 1) } diff --git a/internal/fingerprint/glob.go b/internal/fingerprint/glob.go index f19da358b4..c87c9ec3b5 100644 --- a/internal/fingerprint/glob.go +++ b/internal/fingerprint/glob.go @@ -10,7 +10,7 @@ import ( "github.com/go-task/task/v3/taskfile/ast" ) -func Globs(dir string, globs []*ast.Glob, gitignore bool, rootDir string) ([]string, error) { +func Globs(dir string, globs []*ast.Glob, gitignore bool) ([]string, error) { resultMap := make(map[string]bool) for _, g := range globs { matches, err := glob(dir, g.Glob) @@ -23,7 +23,7 @@ func Globs(dir string, globs []*ast.Glob, gitignore bool, rootDir string) ([]str } if gitignore { - resultMap = filterGitignored(resultMap, rootDir, dir) + resultMap = filterGitignored(resultMap, dir) } return collectKeys(resultMap), nil diff --git a/internal/fingerprint/sources.go b/internal/fingerprint/sources.go index 54303a464d..34d3a04bee 100644 --- a/internal/fingerprint/sources.go +++ b/internal/fingerprint/sources.go @@ -2,12 +2,12 @@ package fingerprint import "fmt" -func NewSourcesChecker(method, tempDir string, dry bool, rootDir string) (SourcesCheckable, error) { +func NewSourcesChecker(method, tempDir string, dry bool) (SourcesCheckable, error) { switch method { case "timestamp": - return NewTimestampChecker(tempDir, dry, rootDir), nil + return NewTimestampChecker(tempDir, dry), nil case "checksum": - return NewChecksumChecker(tempDir, dry, rootDir), nil + return NewChecksumChecker(tempDir, dry), nil case "none": return NoneChecker{}, nil default: diff --git a/internal/fingerprint/sources_checksum.go b/internal/fingerprint/sources_checksum.go index 22d2d68eff..c9c0606b8f 100644 --- a/internal/fingerprint/sources_checksum.go +++ b/internal/fingerprint/sources_checksum.go @@ -19,14 +19,12 @@ import ( type ChecksumChecker struct { tempDir string dry bool - rootDir string } -func NewChecksumChecker(tempDir string, dry bool, rootDir string) *ChecksumChecker { +func NewChecksumChecker(tempDir string, dry bool) *ChecksumChecker { return &ChecksumChecker{ tempDir: tempDir, dry: dry, - rootDir: rootDir, } } @@ -90,7 +88,7 @@ func (*ChecksumChecker) Kind() string { } func (c *ChecksumChecker) checksum(t *ast.Task) (string, error) { - sources, err := Globs(t.Dir, t.Sources, t.IsGitignore(), c.rootDir) + sources, err := Globs(t.Dir, t.Sources, t.IsGitignore()) if err != nil { return "", err } diff --git a/internal/fingerprint/sources_timestamp.go b/internal/fingerprint/sources_timestamp.go index 4d7e7ee334..d413979211 100644 --- a/internal/fingerprint/sources_timestamp.go +++ b/internal/fingerprint/sources_timestamp.go @@ -13,14 +13,12 @@ import ( type TimestampChecker struct { tempDir string dry bool - rootDir string } -func NewTimestampChecker(tempDir string, dry bool, rootDir string) *TimestampChecker { +func NewTimestampChecker(tempDir string, dry bool) *TimestampChecker { return &TimestampChecker{ tempDir: tempDir, dry: dry, - rootDir: rootDir, } } @@ -30,11 +28,11 @@ func (checker *TimestampChecker) IsUpToDate(t *ast.Task) (bool, error) { return false, nil } - sources, err := Globs(t.Dir, t.Sources, t.IsGitignore(), checker.rootDir) + sources, err := Globs(t.Dir, t.Sources, t.IsGitignore()) if err != nil { return false, nil } - generates, err := Globs(t.Dir, t.Generates, t.IsGitignore(), checker.rootDir) + generates, err := Globs(t.Dir, t.Generates, t.IsGitignore()) if err != nil { return false, nil } @@ -92,7 +90,7 @@ func (checker *TimestampChecker) Kind() string { // Value implements the Checker Interface func (checker *TimestampChecker) Value(t *ast.Task) (any, error) { - sources, err := Globs(t.Dir, t.Sources, t.IsGitignore(), checker.rootDir) + sources, err := Globs(t.Dir, t.Sources, t.IsGitignore()) if err != nil { return time.Now(), err } diff --git a/internal/fingerprint/task.go b/internal/fingerprint/task.go index ca8e18b9ab..2b48e114c9 100644 --- a/internal/fingerprint/task.go +++ b/internal/fingerprint/task.go @@ -13,7 +13,6 @@ type ( method string dry bool tempDir string - rootDir string logger *logger.Logger statusChecker StatusCheckable sourcesChecker SourcesCheckable @@ -38,12 +37,6 @@ func WithTempDir(tempDir string) CheckerOption { } } -func WithRootDir(rootDir string) CheckerOption { - return func(config *CheckerConfig) { - config.rootDir = rootDir - } -} - func WithLogger(logger *logger.Logger) CheckerOption { return func(config *CheckerConfig) { config.logger = logger @@ -93,7 +86,7 @@ func IsTaskUpToDate( // If no sources checker was given, set up the default one if config.sourcesChecker == nil { - config.sourcesChecker, err = NewSourcesChecker(config.method, config.tempDir, config.dry, config.rootDir) + config.sourcesChecker, err = NewSourcesChecker(config.method, config.tempDir, config.dry) if err != nil { return false, err } diff --git a/status.go b/status.go index 8680d52704..ae40f5ba5f 100644 --- a/status.go +++ b/status.go @@ -46,7 +46,7 @@ func (e *Executor) statusOnError(t *ast.Task) error { if method == "" { method = e.Taskfile.Method } - checker, err := fingerprint.NewSourcesChecker(method, e.TempDir.Fingerprint, e.Dry, e.Dir) + checker, err := fingerprint.NewSourcesChecker(method, e.TempDir.Fingerprint, e.Dry) if err != nil { return err } diff --git a/task.go b/task.go index fa2842e4f6..54cda92762 100644 --- a/task.go +++ b/task.go @@ -231,7 +231,6 @@ func (e *Executor) RunTask(ctx context.Context, call *Call) error { fingerprint.WithTempDir(e.TempDir.Fingerprint), fingerprint.WithDry(e.Dry), fingerprint.WithLogger(e.Logger), - fingerprint.WithRootDir(e.Dir), ) if err != nil { return err diff --git a/variables.go b/variables.go index 7f4054bf06..18d716a548 100644 --- a/variables.go +++ b/variables.go @@ -203,9 +203,9 @@ func (e *Executor) compiledTask(call *Call, evaluateShVars bool) (*ast.Task, err var checker fingerprint.SourcesCheckable if origTask.Method == "timestamp" { - checker = fingerprint.NewTimestampChecker(e.TempDir.Fingerprint, e.Dry, e.Dir) + checker = fingerprint.NewTimestampChecker(e.TempDir.Fingerprint, e.Dry) } else { - checker = fingerprint.NewChecksumChecker(e.TempDir.Fingerprint, e.Dry, e.Dir) + checker = fingerprint.NewChecksumChecker(e.TempDir.Fingerprint, e.Dry) } value, err := checker.Value(&new) @@ -226,7 +226,7 @@ func (e *Executor) compiledTask(call *Call, evaluateShVars bool) (*ast.Task, err continue } if cmd.For != nil { - list, keys, err := itemsFromFor(cmd.For, new.Dir, new.Sources, new.Generates, gitignore, e.Dir, vars, origTask.Location, cache) + list, keys, err := itemsFromFor(cmd.For, new.Dir, new.Sources, new.Generates, gitignore, vars, origTask.Location, cache) if err != nil { return nil, err } @@ -275,7 +275,7 @@ func (e *Executor) compiledTask(call *Call, evaluateShVars bool) (*ast.Task, err continue } if dep.For != nil { - list, keys, err := itemsFromFor(dep.For, new.Dir, new.Sources, new.Generates, gitignore, e.Dir, vars, origTask.Location, cache) + list, keys, err := itemsFromFor(dep.For, new.Dir, new.Sources, new.Generates, gitignore, vars, origTask.Location, cache) if err != nil { return nil, err } @@ -347,7 +347,6 @@ func itemsFromFor( sources []*ast.Glob, generates []*ast.Glob, gitignore bool, - rootDir string, vars *ast.Vars, location *ast.Location, cache *templater.Cache, @@ -370,7 +369,7 @@ func itemsFromFor( } // Get the list from the task sources if f.From == "sources" { - glist, err := fingerprint.Globs(dir, sources, gitignore, rootDir) + glist, err := fingerprint.Globs(dir, sources, gitignore) if err != nil { return nil, nil, err } @@ -384,7 +383,7 @@ func itemsFromFor( } // Get the list from the task generates if f.From == "generates" { - glist, err := fingerprint.Globs(dir, generates, gitignore, rootDir) + glist, err := fingerprint.Globs(dir, generates, gitignore) if err != nil { return nil, nil, err } diff --git a/watch.go b/watch.go index a246ae55c7..349dd57a69 100644 --- a/watch.go +++ b/watch.go @@ -205,7 +205,7 @@ func (e *Executor) collectSources(calls []*Call) ([]string, error) { var sources []string err := e.traverse(calls, func(task *ast.Task) error { - files, err := fingerprint.Globs(task.Dir, task.Sources, task.IsGitignore(), e.Dir) + files, err := fingerprint.Globs(task.Dir, task.Sources, task.IsGitignore()) if err != nil { return err } From c95f8dea1e4ff61f65cec7a4dc69d780cebb8580 Mon Sep 17 00:00:00 2001 From: Valentin Maerten Date: Sat, 4 Apr 2026 10:44:35 +0200 Subject: [PATCH 4/6] refactor: remove rootDir param, auto-detect .git as boundary Walk up from task dir to find .git instead of threading rootDir through Globs, checkers, and itemsFromFor. Gitignore rules are discarded if no .git is found, matching ripgrep's require_git behavior. This keeps the Globs signature clean and makes the future .taskignore integration straightforward (loaded at setup like .taskrc, separate boundary). --- internal/fingerprint/gitignore_test.go | 16 +++------- taskfile/ast/task.go | 2 +- taskfile/ast/taskfile.go | 44 +++++++++++++------------- 3 files changed, 28 insertions(+), 34 deletions(-) diff --git a/internal/fingerprint/gitignore_test.go b/internal/fingerprint/gitignore_test.go index 1c5118f884..edef18b1af 100644 --- a/internal/fingerprint/gitignore_test.go +++ b/internal/fingerprint/gitignore_test.go @@ -19,10 +19,7 @@ func initGitRepo(t *testing.T, dir string) { func TestGlobsWithGitignore(t *testing.T) { t.Parallel() - dir, err := os.MkdirTemp("", "task-gitignore-test-*") - require.NoError(t, err) - t.Cleanup(func() { os.RemoveAll(dir) }) - + dir := t.TempDir() initGitRepo(t, dir) require.NoError(t, os.WriteFile(filepath.Join(dir, "included.txt"), []byte("included"), 0o644)) @@ -70,10 +67,7 @@ func TestGlobsWithGitignore(t *testing.T) { func TestGlobsWithGitignoreNested(t *testing.T) { t.Parallel() - dir, err := os.MkdirTemp("", "task-gitignore-nested-*") - require.NoError(t, err) - t.Cleanup(func() { os.RemoveAll(dir) }) - + dir := t.TempDir() initGitRepo(t, dir) subDir := filepath.Join(dir, "sub") @@ -82,9 +76,7 @@ func TestGlobsWithGitignoreNested(t *testing.T) { require.NoError(t, os.WriteFile(filepath.Join(subDir, "keep.txt"), []byte("keep"), 0o644)) require.NoError(t, os.WriteFile(filepath.Join(subDir, "build.out"), []byte("build"), 0o644)) - // Root .gitignore ignores *.log require.NoError(t, os.WriteFile(filepath.Join(dir, ".gitignore"), []byte("*.log\n"), 0o644)) - // Nested .gitignore ignores *.out require.NoError(t, os.WriteFile(filepath.Join(subDir, ".gitignore"), []byte("*.out\n"), 0o644)) globs := []*ast.Glob{ @@ -102,7 +94,9 @@ func TestGlobsWithGitignoreNested(t *testing.T) { func TestGlobsWithGitignoreNoRepo(t *testing.T) { t.Parallel() - dir, err := os.MkdirTemp("", "task-gitignore-norepo-*") + // Cannot use t.TempDir() here because it creates a dir inside the + // go-task repo which has a .git parent, defeating the "no repo" test. + dir, err := os.MkdirTemp("", "task-gitignore-norepo-*") //nolint:usetesting require.NoError(t, err) t.Cleanup(func() { os.RemoveAll(dir) }) diff --git a/taskfile/ast/task.go b/taskfile/ast/task.go index 30212ab613..f7f0f31870 100644 --- a/taskfile/ast/task.go +++ b/taskfile/ast/task.go @@ -156,7 +156,7 @@ func (t *Task) UnmarshalYAML(node *yaml.Node) error { Internal bool Method string Prefix string - IgnoreError bool `yaml:"ignore_error"` + IgnoreError bool `yaml:"ignore_error"` Gitignore *bool `yaml:"gitignore,omitempty"` Run string Platforms []*Platform diff --git a/taskfile/ast/taskfile.go b/taskfile/ast/taskfile.go index 9216f39660..125b2670e0 100644 --- a/taskfile/ast/taskfile.go +++ b/taskfile/ast/taskfile.go @@ -20,16 +20,16 @@ var ErrIncludedTaskfilesCantHaveDotenvs = errors.New("task: Included Taskfiles c // Taskfile is the abstract syntax tree for a Taskfile type Taskfile struct { - Location string - Version *semver.Version - Output Output - Method string - Includes *Includes - Set []string - Shopt []string - Vars *Vars - Env *Vars - Tasks *Tasks + Location string + Version *semver.Version + Output Output + Method string + Includes *Includes + Set []string + Shopt []string + Vars *Vars + Env *Vars + Tasks *Tasks Silent bool Dotenv []string Run string @@ -77,18 +77,18 @@ func (tf *Taskfile) UnmarshalYAML(node *yaml.Node) error { switch node.Kind { case yaml.MappingNode: var taskfile struct { - Version *semver.Version - Output Output - Method string - Includes *Includes - Set []string - Shopt []string - Vars *Vars - Env *Vars - Tasks *Tasks - Silent bool - Dotenv []string - Run string + Version *semver.Version + Output Output + Method string + Includes *Includes + Set []string + Shopt []string + Vars *Vars + Env *Vars + Tasks *Tasks + Silent bool + Dotenv []string + Run string Interval time.Duration Gitignore bool } From 498798b11869576d3de36cede5df956415611816 Mon Sep 17 00:00:00 2001 From: Valentin Maerten Date: Sat, 4 Apr 2026 13:05:01 +0200 Subject: [PATCH 5/6] rename: gitignore -> use_gitignore for clarity Rename the Taskfile/task option from `gitignore` to `use_gitignore` to avoid ambiguity (could be read as "ignore git" vs "use .gitignore"). Consistent with Biome's `useIgnoreFile` naming convention. --- internal/fingerprint/glob.go | 4 ++-- internal/fingerprint/sources_checksum.go | 2 +- internal/fingerprint/sources_timestamp.go | 6 +++--- taskfile/ast/task.go | 16 ++++++++-------- taskfile/ast/taskfile.go | 6 +++--- testdata/gitignore/Taskfile.yml | 6 +++--- variables.go | 10 +++++----- watch.go | 2 +- website/src/public/schema.json | 4 ++-- 9 files changed, 28 insertions(+), 28 deletions(-) diff --git a/internal/fingerprint/glob.go b/internal/fingerprint/glob.go index c87c9ec3b5..930a54a3b1 100644 --- a/internal/fingerprint/glob.go +++ b/internal/fingerprint/glob.go @@ -10,7 +10,7 @@ import ( "github.com/go-task/task/v3/taskfile/ast" ) -func Globs(dir string, globs []*ast.Glob, gitignore bool) ([]string, error) { +func Globs(dir string, globs []*ast.Glob, useGitignore bool) ([]string, error) { resultMap := make(map[string]bool) for _, g := range globs { matches, err := glob(dir, g.Glob) @@ -22,7 +22,7 @@ func Globs(dir string, globs []*ast.Glob, gitignore bool) ([]string, error) { } } - if gitignore { + if useGitignore { resultMap = filterGitignored(resultMap, dir) } diff --git a/internal/fingerprint/sources_checksum.go b/internal/fingerprint/sources_checksum.go index c9c0606b8f..e30e8744a6 100644 --- a/internal/fingerprint/sources_checksum.go +++ b/internal/fingerprint/sources_checksum.go @@ -88,7 +88,7 @@ func (*ChecksumChecker) Kind() string { } func (c *ChecksumChecker) checksum(t *ast.Task) (string, error) { - sources, err := Globs(t.Dir, t.Sources, t.IsGitignore()) + sources, err := Globs(t.Dir, t.Sources, t.ShouldUseGitignore()) if err != nil { return "", err } diff --git a/internal/fingerprint/sources_timestamp.go b/internal/fingerprint/sources_timestamp.go index d413979211..e2c27575de 100644 --- a/internal/fingerprint/sources_timestamp.go +++ b/internal/fingerprint/sources_timestamp.go @@ -28,11 +28,11 @@ func (checker *TimestampChecker) IsUpToDate(t *ast.Task) (bool, error) { return false, nil } - sources, err := Globs(t.Dir, t.Sources, t.IsGitignore()) + sources, err := Globs(t.Dir, t.Sources, t.ShouldUseGitignore()) if err != nil { return false, nil } - generates, err := Globs(t.Dir, t.Generates, t.IsGitignore()) + generates, err := Globs(t.Dir, t.Generates, t.ShouldUseGitignore()) if err != nil { return false, nil } @@ -90,7 +90,7 @@ func (checker *TimestampChecker) Kind() string { // Value implements the Checker Interface func (checker *TimestampChecker) Value(t *ast.Task) (any, error) { - sources, err := Globs(t.Dir, t.Sources, t.IsGitignore()) + sources, err := Globs(t.Dir, t.Sources, t.ShouldUseGitignore()) if err != nil { return time.Now(), err } diff --git a/taskfile/ast/task.go b/taskfile/ast/task.go index f7f0f31870..bff0ff596d 100644 --- a/taskfile/ast/task.go +++ b/taskfile/ast/task.go @@ -38,7 +38,7 @@ type Task struct { Method string Prefix string `hash:"ignore"` IgnoreError bool - Gitignore *bool + UseGitignore *bool Run string Platforms []*Platform If string @@ -76,10 +76,10 @@ func (t *Task) IsSilent() bool { return t.Silent != nil && *t.Silent } -// IsGitignore returns true if the task has gitignore filtering explicitly enabled. -// Returns false if Gitignore is nil (not set) or explicitly set to false. -func (t *Task) IsGitignore() bool { - return t.Gitignore != nil && *t.Gitignore +// ShouldUseGitignore returns true if the task has gitignore filtering explicitly enabled. +// Returns false if UseGitignore is nil (not set) or explicitly set to false. +func (t *Task) ShouldUseGitignore() bool { + return t.UseGitignore != nil && *t.UseGitignore } // WildcardMatch will check if the given string matches the name of the Task and returns any wildcard values. @@ -157,7 +157,7 @@ func (t *Task) UnmarshalYAML(node *yaml.Node) error { Method string Prefix string IgnoreError bool `yaml:"ignore_error"` - Gitignore *bool `yaml:"gitignore,omitempty"` + UseGitignore *bool `yaml:"use_gitignore,omitempty"` Run string Platforms []*Platform If string @@ -198,7 +198,7 @@ func (t *Task) UnmarshalYAML(node *yaml.Node) error { t.Method = task.Method t.Prefix = task.Prefix t.IgnoreError = task.IgnoreError - t.Gitignore = deepcopy.Scalar(task.Gitignore) + t.UseGitignore = deepcopy.Scalar(task.UseGitignore) t.Run = task.Run t.Platforms = task.Platforms t.If = task.If @@ -242,7 +242,7 @@ func (t *Task) DeepCopy() *Task { Method: t.Method, Prefix: t.Prefix, IgnoreError: t.IgnoreError, - Gitignore: deepcopy.Scalar(t.Gitignore), + UseGitignore: deepcopy.Scalar(t.UseGitignore), Run: t.Run, IncludeVars: t.IncludeVars.DeepCopy(), IncludedTaskfileVars: t.IncludedTaskfileVars.DeepCopy(), diff --git a/taskfile/ast/taskfile.go b/taskfile/ast/taskfile.go index 125b2670e0..30dcbc8a07 100644 --- a/taskfile/ast/taskfile.go +++ b/taskfile/ast/taskfile.go @@ -34,7 +34,7 @@ type Taskfile struct { Dotenv []string Run string Interval time.Duration - Gitignore bool + UseGitignore bool `yaml:"use_gitignore"` } // Merge merges the second Taskfile into the first @@ -90,7 +90,7 @@ func (tf *Taskfile) UnmarshalYAML(node *yaml.Node) error { Dotenv []string Run string Interval time.Duration - Gitignore bool + UseGitignore bool `yaml:"use_gitignore"` } if err := node.Decode(&taskfile); err != nil { return errors.NewTaskfileDecodeError(err, node) @@ -108,7 +108,7 @@ func (tf *Taskfile) UnmarshalYAML(node *yaml.Node) error { tf.Dotenv = taskfile.Dotenv tf.Run = taskfile.Run tf.Interval = taskfile.Interval - tf.Gitignore = taskfile.Gitignore + tf.UseGitignore = taskfile.UseGitignore if tf.Includes == nil { tf.Includes = NewIncludes() } diff --git a/testdata/gitignore/Taskfile.yml b/testdata/gitignore/Taskfile.yml index c78e5a0efb..93452867c6 100644 --- a/testdata/gitignore/Taskfile.yml +++ b/testdata/gitignore/Taskfile.yml @@ -1,6 +1,6 @@ version: '3' -gitignore: true +use_gitignore: true tasks: build: @@ -13,8 +13,8 @@ tasks: - ./generated.txt method: checksum - build-no-gitignore: - gitignore: false + build-no-use_gitignore: + use_gitignore: false cmds: - cp ./source.txt ./generated.txt sources: diff --git a/variables.go b/variables.go index 18d716a548..2fbe1f9bb3 100644 --- a/variables.go +++ b/variables.go @@ -59,7 +59,7 @@ func (e *Executor) CompiledTaskForTaskList(call *Call) (*ast.Task, error) { Env: nil, Dotenv: origTask.Dotenv, Silent: deepcopy.Scalar(origTask.Silent), - Gitignore: deepcopy.Scalar(origTask.Gitignore), + UseGitignore: deepcopy.Scalar(origTask.UseGitignore), Interactive: origTask.Interactive, Internal: origTask.Internal, Method: origTask.Method, @@ -111,9 +111,9 @@ func (e *Executor) compiledTask(call *Call, evaluateShVars bool) (*ast.Task, err } } - gitignore := origTask.IsGitignore() - if origTask.Gitignore == nil { - gitignore = e.Taskfile.Gitignore + gitignore := origTask.ShouldUseGitignore() + if origTask.UseGitignore == nil { + gitignore = e.Taskfile.UseGitignore } new := ast.Task{ @@ -132,7 +132,7 @@ func (e *Executor) compiledTask(call *Call, evaluateShVars bool) (*ast.Task, err Env: nil, Dotenv: templater.Replace(origTask.Dotenv, cache), Silent: deepcopy.Scalar(origTask.Silent), - Gitignore: &gitignore, + UseGitignore: &gitignore, Interactive: origTask.Interactive, Internal: origTask.Internal, Method: templater.Replace(origTask.Method, cache), diff --git a/watch.go b/watch.go index 349dd57a69..1a0dbee0c7 100644 --- a/watch.go +++ b/watch.go @@ -205,7 +205,7 @@ func (e *Executor) collectSources(calls []*Call) ([]string, error) { var sources []string err := e.traverse(calls, func(task *ast.Task) error { - files, err := fingerprint.Globs(task.Dir, task.Sources, task.IsGitignore()) + files, err := fingerprint.Globs(task.Dir, task.Sources, task.ShouldUseGitignore()) if err != nil { return err } diff --git a/website/src/public/schema.json b/website/src/public/schema.json index 37751fa4fe..61f7a843b7 100644 --- a/website/src/public/schema.json +++ b/website/src/public/schema.json @@ -177,7 +177,7 @@ "enum": ["none", "checksum", "timestamp"], "default": "none" }, - "gitignore": { + "use_gitignore": { "description": "When set to true, files matching .gitignore rules will be excluded from sources and generates glob resolution. Overrides the global gitignore setting.", "type": "boolean", "default": false @@ -692,7 +692,7 @@ "enum": ["none", "checksum", "timestamp"], "default": "checksum" }, - "gitignore": { + "use_gitignore": { "description": "When set to true, files matching .gitignore rules will be excluded from sources and generates glob resolution for all tasks. Can be overridden per task.", "type": "boolean", "default": false From 0ebad8807bf746872cccf13cf965946ef07cb8aa Mon Sep 17 00:00:00 2001 From: Valentin Maerten Date: Sat, 4 Apr 2026 13:14:43 +0200 Subject: [PATCH 6/6] style: fix formatting --- taskfile/ast/task.go | 6 ++--- taskfile/ast/taskfile.go | 54 ++++++++++++++++++++-------------------- variables.go | 4 +-- 3 files changed, 32 insertions(+), 32 deletions(-) diff --git a/taskfile/ast/task.go b/taskfile/ast/task.go index bff0ff596d..c9d2c20779 100644 --- a/taskfile/ast/task.go +++ b/taskfile/ast/task.go @@ -38,7 +38,7 @@ type Task struct { Method string Prefix string `hash:"ignore"` IgnoreError bool - UseGitignore *bool + UseGitignore *bool Run string Platforms []*Platform If string @@ -157,7 +157,7 @@ func (t *Task) UnmarshalYAML(node *yaml.Node) error { Method string Prefix string IgnoreError bool `yaml:"ignore_error"` - UseGitignore *bool `yaml:"use_gitignore,omitempty"` + UseGitignore *bool `yaml:"use_gitignore,omitempty"` Run string Platforms []*Platform If string @@ -242,7 +242,7 @@ func (t *Task) DeepCopy() *Task { Method: t.Method, Prefix: t.Prefix, IgnoreError: t.IgnoreError, - UseGitignore: deepcopy.Scalar(t.UseGitignore), + UseGitignore: deepcopy.Scalar(t.UseGitignore), Run: t.Run, IncludeVars: t.IncludeVars.DeepCopy(), IncludedTaskfileVars: t.IncludedTaskfileVars.DeepCopy(), diff --git a/taskfile/ast/taskfile.go b/taskfile/ast/taskfile.go index 30dcbc8a07..9de0f90255 100644 --- a/taskfile/ast/taskfile.go +++ b/taskfile/ast/taskfile.go @@ -20,20 +20,20 @@ var ErrIncludedTaskfilesCantHaveDotenvs = errors.New("task: Included Taskfiles c // Taskfile is the abstract syntax tree for a Taskfile type Taskfile struct { - Location string - Version *semver.Version - Output Output - Method string - Includes *Includes - Set []string - Shopt []string - Vars *Vars - Env *Vars - Tasks *Tasks - Silent bool - Dotenv []string - Run string - Interval time.Duration + Location string + Version *semver.Version + Output Output + Method string + Includes *Includes + Set []string + Shopt []string + Vars *Vars + Env *Vars + Tasks *Tasks + Silent bool + Dotenv []string + Run string + Interval time.Duration UseGitignore bool `yaml:"use_gitignore"` } @@ -77,19 +77,19 @@ func (tf *Taskfile) UnmarshalYAML(node *yaml.Node) error { switch node.Kind { case yaml.MappingNode: var taskfile struct { - Version *semver.Version - Output Output - Method string - Includes *Includes - Set []string - Shopt []string - Vars *Vars - Env *Vars - Tasks *Tasks - Silent bool - Dotenv []string - Run string - Interval time.Duration + Version *semver.Version + Output Output + Method string + Includes *Includes + Set []string + Shopt []string + Vars *Vars + Env *Vars + Tasks *Tasks + Silent bool + Dotenv []string + Run string + Interval time.Duration UseGitignore bool `yaml:"use_gitignore"` } if err := node.Decode(&taskfile); err != nil { diff --git a/variables.go b/variables.go index 2fbe1f9bb3..dad3ca9219 100644 --- a/variables.go +++ b/variables.go @@ -59,7 +59,7 @@ func (e *Executor) CompiledTaskForTaskList(call *Call) (*ast.Task, error) { Env: nil, Dotenv: origTask.Dotenv, Silent: deepcopy.Scalar(origTask.Silent), - UseGitignore: deepcopy.Scalar(origTask.UseGitignore), + UseGitignore: deepcopy.Scalar(origTask.UseGitignore), Interactive: origTask.Interactive, Internal: origTask.Internal, Method: origTask.Method, @@ -132,7 +132,7 @@ func (e *Executor) compiledTask(call *Call, evaluateShVars bool) (*ast.Task, err Env: nil, Dotenv: templater.Replace(origTask.Dotenv, cache), Silent: deepcopy.Scalar(origTask.Silent), - UseGitignore: &gitignore, + UseGitignore: &gitignore, Interactive: origTask.Interactive, Internal: origTask.Internal, Method: templater.Replace(origTask.Method, cache),