on main help sign in
perf: replace regex-based gitignore with glob-based matching (#2199)
d7e2252Austin Cherry
go.mod+5−2
@@ -38,6 +38,7 @@ require (
38 github.com/denisbrodbeck/machineid v1.0.1
38 github.com/denisbrodbeck/machineid v1.0.1
39 github.com/disintegration/imaging v1.6.2
39 github.com/disintegration/imaging v1.6.2
40 github.com/dustin/go-humanize v1.0.1
40 github.com/dustin/go-humanize v1.0.1
41+ github.com/go-git/go-git/v5 v5.16.5
41 github.com/google/uuid v1.6.0
42 github.com/google/uuid v1.6.0
42 github.com/invopop/jsonschema v0.13.0
43 github.com/invopop/jsonschema v0.13.0
43 github.com/joho/godotenv v1.5.1
44 github.com/joho/godotenv v1.5.1
@@ -53,7 +54,6 @@ require (
53 github.com/pressly/goose/v3 v3.26.0
54 github.com/pressly/goose/v3 v3.26.0
54 github.com/qjebbs/go-jsons v1.0.0-alpha.4
55 github.com/qjebbs/go-jsons v1.0.0-alpha.4
55 github.com/rivo/uniseg v0.4.7
56 github.com/rivo/uniseg v0.4.7
56− github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06
57 github.com/sahilm/fuzzy v0.1.1
57 github.com/sahilm/fuzzy v0.1.1
58 github.com/sourcegraph/jsonrpc2 v0.2.1
58 github.com/sourcegraph/jsonrpc2 v0.2.1
59 github.com/spf13/cobra v1.10.2
59 github.com/spf13/cobra v1.10.2
@@ -110,6 +110,8 @@ require (
110 github.com/ebitengine/purego v0.10.0-alpha.3.0.20260102153238-200df6041cff // indirect
110 github.com/ebitengine/purego v0.10.0-alpha.3.0.20260102153238-200df6041cff // indirect
111 github.com/felixge/httpsnoop v1.0.4 // indirect
111 github.com/felixge/httpsnoop v1.0.4 // indirect
112 github.com/fsnotify/fsnotify v1.9.0 // indirect
112 github.com/fsnotify/fsnotify v1.9.0 // indirect
113+ github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
114+ github.com/go-git/go-billy/v5 v5.6.2 // indirect
113 github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e // indirect
115 github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e // indirect
114 github.com/go-logfmt/logfmt v0.6.0 // indirect
116 github.com/go-logfmt/logfmt v0.6.0 // indirect
115 github.com/go-logr/logr v1.4.3 // indirect
117 github.com/go-logr/logr v1.4.3 // indirect
@@ -127,6 +129,7 @@ require (
127 github.com/gorilla/websocket v1.5.3 // indirect
129 github.com/gorilla/websocket v1.5.3 // indirect
128 github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
130 github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
129 github.com/inconshreveable/mousetrap v1.1.0 // indirect
131 github.com/inconshreveable/mousetrap v1.1.0 // indirect
132+ github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
130 github.com/kaptinlin/go-i18n v0.2.3 // indirect
133 github.com/kaptinlin/go-i18n v0.2.3 // indirect
131 github.com/kaptinlin/jsonpointer v0.4.9 // indirect
134 github.com/kaptinlin/jsonpointer v0.4.9 // indirect
132 github.com/kaptinlin/jsonschema v0.6.10 // indirect
135 github.com/kaptinlin/jsonschema v0.6.10 // indirect
@@ -149,7 +152,6 @@ require (
149 github.com/pierrec/lz4/v4 v4.1.22 // indirect
152 github.com/pierrec/lz4/v4 v4.1.22 // indirect
150 github.com/pmezard/go-difflib v1.0.0 // indirect
153 github.com/pmezard/go-difflib v1.0.0 // indirect
151 github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
154 github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
152− github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
153 github.com/sethvargo/go-retry v0.3.0 // indirect
155 github.com/sethvargo/go-retry v0.3.0 // indirect
154 github.com/spf13/pflag v1.0.9 // indirect
156 github.com/spf13/pflag v1.0.9 // indirect
155 github.com/tetratelabs/wazero v1.11.0 // indirect
157 github.com/tetratelabs/wazero v1.11.0 // indirect
@@ -185,6 +187,7 @@ require (
185 google.golang.org/protobuf v1.36.10 // indirect
187 google.golang.org/protobuf v1.36.10 // indirect
186 gopkg.in/dnaeon/go-vcr.v4 v4.0.6-0.20251110073552-01de4eb40290 // indirect
188 gopkg.in/dnaeon/go-vcr.v4 v4.0.6-0.20251110073552-01de4eb40290 // indirect
187 gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
189 gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
190+ gopkg.in/warnings.v0 v0.1.2 // indirect
188 modernc.org/libc v1.67.6 // indirect
191 modernc.org/libc v1.67.6 // indirect
189 modernc.org/mathutil v1.7.1 // indirect
192 modernc.org/mathutil v1.7.1 // indirect
190 modernc.org/memory v1.11.0 // indirect
193 modernc.org/memory v1.11.0 // indirect
go.sum+12−4
@@ -164,6 +164,12 @@ github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSw
164github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
164github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
165github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
165github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
166github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
166github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
167+github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
168+github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
169+github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM=
170+github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU=
171+github.com/go-git/go-git/v5 v5.16.5 h1:mdkuqblwr57kVfXri5TTH+nMFLNUxIj9Z7F5ykFbw5s=
172+github.com/go-git/go-git/v5 v5.16.5/go.mod h1:QOMLpNf1qxuSY4StA/ArOdfFR2TrKEjJiye2kel2m+M=
167github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e h1:Lf/gRkoycfOBPa42vU2bbgPurFong6zXeFtPoxholzU=
173github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e h1:Lf/gRkoycfOBPa42vU2bbgPurFong6zXeFtPoxholzU=
168github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e/go.mod h1:uNVvRXArCGbZ508SxYYTC5v1JWoz2voff5pm25jU1Ok=
174github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e/go.mod h1:uNVvRXArCGbZ508SxYYTC5v1JWoz2voff5pm25jU1Ok=
169github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
175github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
@@ -215,6 +221,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2
215github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
221github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
216github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E=
222github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E=
217github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=
223github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=
224+github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
225+github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
218github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
226github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
219github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
227github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
220github.com/jordanella/go-ansi-paintbrush v0.0.0-20240728195301-b7ad996ecf3d h1:on25kP+Sx7sxUMRQiA8gdcToAGet4DK/EIA30mXre+4=
228github.com/jordanella/go-ansi-paintbrush v0.0.0-20240728195301-b7ad996ecf3d h1:on25kP+Sx7sxUMRQiA8gdcToAGet4DK/EIA30mXre+4=
@@ -286,6 +294,8 @@ github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFu
286github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
294github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
287github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
295github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
288github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
296github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
297+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
298+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
289github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
299github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
290github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
300github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
291github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
301github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -303,8 +313,6 @@ github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUc
303github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
313github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
304github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
314github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
305github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
315github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
306−github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 h1:OkMGxebDjyw0ULyrTYWeN0UNCCkmCWfjPnIA2W6oviI=
307−github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06/go.mod h1:+ePHsJ1keEjQtpvf9HHw0f4ZeJ0TLRsxhunSI2hYJSs=
308github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA=
316github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA=
309github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
317github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
310github.com/sebdah/goldie/v2 v2.5.3 h1:9ES/mNN+HNUbNWpVAlrzuZ7jE+Nrczbj8uFRjM7624Y=
318github.com/sebdah/goldie/v2 v2.5.3 h1:9ES/mNN+HNUbNWpVAlrzuZ7jE+Nrczbj8uFRjM7624Y=
@@ -324,7 +332,6 @@ github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An
324github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
332github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
325github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
333github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
326github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
334github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
327−github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
328github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
335github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
329github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
336github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
330github.com/tetratelabs/wazero v1.11.0 h1:+gKemEuKCTevU4d7ZTzlsvgd1uaToIDtlQlmNbwqYhA=
337github.com/tetratelabs/wazero v1.11.0 h1:+gKemEuKCTevU4d7ZTzlsvgd1uaToIDtlQlmNbwqYhA=
@@ -502,10 +509,11 @@ gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST
502gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
509gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
503gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
510gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
504gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
511gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
512+gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
513+gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
505gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
514gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
506gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
515gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
507gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
516gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
508−gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
509gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
517gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
510gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
518gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
511modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
519modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
internal/fsext/fileutil.go+20−11
@@ -70,10 +70,16 @@ func NewFastGlobWalker(searchPath string) *FastGlobWalker {
70 }
70 }
71}
71}
72
72
73−// ShouldSkip checks if a path should be skipped based on hierarchical gitignore,
73+// ShouldSkip checks if a file path should be skipped based on hierarchical gitignore,
74−// crushignore, and hidden file rules
74+// crushignore, and hidden file rules.
75func (w *FastGlobWalker) ShouldSkip(path string) bool {
75func (w *FastGlobWalker) ShouldSkip(path string) bool {
76− return w.directoryLister.shouldIgnore(path, nil)
76+ return w.directoryLister.shouldIgnore(path, nil, false)
77+}
78+
79+// ShouldSkipDir checks if a directory path should be skipped based on hierarchical
80+// gitignore, crushignore, and hidden file rules.
81+func (w *FastGlobWalker) ShouldSkipDir(path string) bool {
82+ return w.directoryLister.shouldIgnore(path, nil, true)
77}
83}
78
84
79func GlobWithDoubleStar(pattern, searchPath string, limit int) ([]string, bool, error) {
85func GlobWithDoubleStar(pattern, searchPath string, limit int) ([]string, bool, error) {
@@ -93,14 +99,15 @@ func GlobWithDoubleStar(pattern, searchPath string, limit int) ([]string, bool,
93 return nil // Skip files we can't access
99 return nil // Skip files we can't access
94 }
100 }
95
101
96− if d.IsDir() {
102+ isDir := d.IsDir()
103+ if isDir {
97− if walker.ShouldSkip(path) {
104+ if walker.ShouldSkipDir(path) {
98 return filepath.SkipDir
105 return filepath.SkipDir
99 }
106 }
100− }
107+ } else {
101−
102− if walker.ShouldSkip(path) {
108+ if walker.ShouldSkip(path) {
103− return nil
109+ return nil
110+ }
104 }
111 }
105
112
106 relPath, err := filepath.Rel(searchPath, path)
113 relPath, err := filepath.Rel(searchPath, path)
@@ -145,10 +152,12 @@ func GlobWithDoubleStar(pattern, searchPath string, limit int) ([]string, bool,
145}
152}
146
153
147// ShouldExcludeFile checks if a file should be excluded from processing
154// ShouldExcludeFile checks if a file should be excluded from processing
148−// based on common patterns and ignore rules
155+// based on common patterns and ignore rules.
149func ShouldExcludeFile(rootPath, filePath string) bool {
156func ShouldExcludeFile(rootPath, filePath string) bool {
157+ info, err := os.Stat(filePath)
158+ isDir := err == nil && info.IsDir()
150 return NewDirectoryLister(rootPath).
159 return NewDirectoryLister(rootPath).
151− shouldIgnore(filePath, nil)
160+ shouldIgnore(filePath, nil, isDir)
152}
161}
153
162
154func PrettyPath(path string) string {
163func PrettyPath(path string) string {
internal/fsext/ignore_test.go+3−3
@@ -21,9 +21,9 @@ func TestCrushIgnore(t *testing.T) {
21 require.NoError(t, os.WriteFile(".crushignore", []byte("*.log\n"), 0o644))
21 require.NoError(t, os.WriteFile(".crushignore", []byte("*.log\n"), 0o644))
22
22
23 dl := NewDirectoryLister(tempDir)
23 dl := NewDirectoryLister(tempDir)
24− require.True(t, dl.shouldIgnore("test2.log", nil), ".log files should be ignored")
24+ require.True(t, dl.shouldIgnore("test2.log", nil, false), ".log files should be ignored")
25− require.False(t, dl.shouldIgnore("test1.txt", nil), ".txt files should not be ignored")
25+ require.False(t, dl.shouldIgnore("test1.txt", nil, false), ".txt files should not be ignored")
26− require.True(t, dl.shouldIgnore("test3.tmp", nil), ".tmp files should be ignored by common patterns")
26+ require.True(t, dl.shouldIgnore("test3.tmp", nil, false), ".tmp files should be ignored by common patterns")
27}
27}
28
28
29func TestShouldExcludeFile(t *testing.T) {
29func TestShouldExcludeFile(t *testing.T) {
internal/fsext/ls.go+155−121
@@ -12,29 +12,45 @@ import (
12 "github.com/charlievieth/fastwalk"
12 "github.com/charlievieth/fastwalk"
13 "github.com/charmbracelet/crush/internal/csync"
13 "github.com/charmbracelet/crush/internal/csync"
14 "github.com/charmbracelet/crush/internal/home"
14 "github.com/charmbracelet/crush/internal/home"
15− ignore "github.com/sabhiram/go-gitignore"
15+ "github.com/go-git/go-git/v5/plumbing/format/gitignore"
16)
16)
17
17
18+// fastIgnoreDirs is a set of directory names that are always ignored.
19+// This provides O(1) lookup for common cases to avoid expensive pattern matching.
20+var fastIgnoreDirs = map[string]bool{
21+ ".git": true,
22+ ".svn": true,
23+ ".hg": true,
24+ ".bzr": true,
25+ ".vscode": true,
26+ ".idea": true,
27+ "node_modules": true,
28+ "__pycache__": true,
29+ ".pytest_cache": true,
30+ ".cache": true,
31+ ".tmp": true,
32+ ".Trash": true,
33+ ".Spotlight-V100": true,
34+ ".fseventsd": true,
35+ ".crush": true,
36+ "OrbStack": true,
37+ ".local": true,
38+ ".share": true,
39+}
40+
18−// commonIgnorePatterns contains commonly ignored files and directories
41+// commonIgnorePatterns contains commonly ignored files and directories.
42+// Note: Exact directory names that are in fastIgnoreDirs are handled there for O(1) lookup.
43+// This list contains wildcard patterns and file-specific patterns.
19−var commonIgnorePatterns = sync.OnceValue(func() ignore.IgnoreParser {
44+var commonIgnorePatterns = sync.OnceValue(func() []gitignore.Pattern {
20− return ignore.CompileIgnoreLines(
45+ patterns := []string{
21− // Version control
22− ".git",
23− ".svn",
24− ".hg",
25− ".bzr",
26−
27− // IDE and editor files
46+ // IDE and editor files (wildcards)
28− ".vscode",
29− ".idea",
30 "*.swp",
47 "*.swp",
31 "*.swo",
48 "*.swo",
32 "*~",
49 "*~",
33 ".DS_Store",
50 ".DS_Store",
34 "Thumbs.db",
51 "Thumbs.db",
35
52
36− // Build artifacts and dependencies
53+ // Build artifacts (non-fastIgnoreDirs)
37− "node_modules",
38 "target",
54 "target",
39 "build",
55 "build",
40 "dist",
56 "dist",
@@ -47,84 +63,147 @@ var commonIgnorePatterns = sync.OnceValue(func() ignore.IgnoreParser {
47 "*.dll",
63 "*.dll",
48 "*.exe",
64 "*.exe",
49
65
50− // Logs and temporary files
66+ // Logs and temporary files (wildcards)
51 "*.log",
67 "*.log",
52 "*.tmp",
68 "*.tmp",
53 "*.temp",
69 "*.temp",
54− ".cache",
55− ".tmp",
56
70
57− // Language-specific
71+ // Language-specific (wildcards and non-fastIgnoreDirs)
58− "__pycache__",
59 "*.pyc",
72 "*.pyc",
60 "*.pyo",
73 "*.pyo",
61− ".pytest_cache",
62 "vendor",
74 "vendor",
63 "Cargo.lock",
75 "Cargo.lock",
64 "package-lock.json",
76 "package-lock.json",
65 "yarn.lock",
77 "yarn.lock",
66 "pnpm-lock.yaml",
78 "pnpm-lock.yaml",
67−
79+ }
68− // OS generated files
80+ return parsePatterns(patterns, nil)
69− ".Trash",
70− ".Spotlight-V100",
71− ".fseventsd",
72−
73− // Crush
74− ".crush",
75−
76− // macOS stuff
77− "OrbStack",
78− ".local",
79− ".share",
80− )
81})
81})
82
82
83−var homeIgnore = sync.OnceValue(func() ignore.IgnoreParser {
83+var homeIgnorePatterns = sync.OnceValue(func() []gitignore.Pattern {
84− home := home.Dir()
84+ homeDir := home.Dir()
85 var lines []string
85 var lines []string
86 for _, name := range []string{
86 for _, name := range []string{
87− filepath.Join(home, ".gitignore"),
87+ filepath.Join(homeDir, ".gitignore"),
88− filepath.Join(home, ".config", "git", "ignore"),
88+ filepath.Join(homeDir, ".config", "git", "ignore"),
89− filepath.Join(home, ".config", "crush", "ignore"),
89+ filepath.Join(homeDir, ".config", "crush", "ignore"),
90 } {
90 } {
91 if bts, err := os.ReadFile(name); err == nil {
91 if bts, err := os.ReadFile(name); err == nil {
92 lines = append(lines, strings.Split(string(bts), "\n")...)
92 lines = append(lines, strings.Split(string(bts), "\n")...)
93 }
93 }
94 }
94 }
95− return ignore.CompileIgnoreLines(lines...)
95+ return parsePatterns(lines, nil)
96})
96})
97
97
98+// parsePatterns parses gitignore pattern strings into Pattern objects.
99+// domain is the path components where the patterns are defined (nil for global).
100+func parsePatterns(lines []string, domain []string) []gitignore.Pattern {
101+ var patterns []gitignore.Pattern
102+ for _, line := range lines {
103+ line = strings.TrimSpace(line)
104+ if line == "" || strings.HasPrefix(line, "#") {
105+ continue
106+ }
107+ patterns = append(patterns, gitignore.ParsePattern(line, domain))
108+ }
109+ return patterns
110+}
111+
98type directoryLister struct {
112type directoryLister struct {
99− ignores *csync.Map[string, ignore.IgnoreParser]
113+ // dirPatterns caches parsed patterns from .gitignore/.crushignore for each directory.
114+ // This avoids re-reading files when building combined matchers.
115+ dirPatterns *csync.Map[string, []gitignore.Pattern]
116+ // combinedMatchers caches a combined matcher for each directory that includes
117+ // all ancestor patterns. This allows O(1) matching per file.
118+ combinedMatchers *csync.Map[string, gitignore.Matcher]
100− rootPath string
119+ rootPath string
101}
120}
102
121
103func NewDirectoryLister(rootPath string) *directoryLister {
122func NewDirectoryLister(rootPath string) *directoryLister {
104− dl := &directoryLister{
123+ return &directoryLister{
105− rootPath: rootPath,
124+ rootPath: rootPath,
106− ignores: csync.NewMap[string, ignore.IgnoreParser](),
125+ dirPatterns: csync.NewMap[string, []gitignore.Pattern](),
126+ combinedMatchers: csync.NewMap[string, gitignore.Matcher](),
107 }
127 }
108− dl.getIgnore(rootPath)
109− return dl
110}
128}
111
129
112−// git checks, in order:
130+// pathToComponents splits a path into its components for gitignore matching.
113−// - ./.gitignore, ../.gitignore, etc, until repo root
131+func pathToComponents(path string) []string {
114−// ~/.config/git/ignore
132+ path = filepath.ToSlash(path)
115−// ~/.gitignore
133+ if path == "" || path == "." {
116−//
134+ return nil
117−// This will do the following:
135+ }
118−// - the given ignorePatterns
136+ return strings.Split(path, "/")
137+}
138+
139+// getDirPatterns returns the parsed patterns for a specific directory's
140+// .gitignore and .crushignore files. Results are cached.
141+func (dl *directoryLister) getDirPatterns(dir string) []gitignore.Pattern {
142+ return dl.dirPatterns.GetOrSet(dir, func() []gitignore.Pattern {
143+ var allPatterns []gitignore.Pattern
144+
145+ relPath, _ := filepath.Rel(dl.rootPath, dir)
146+ var domain []string
147+ if relPath != "" && relPath != "." {
148+ domain = pathToComponents(relPath)
149+ }
150+
151+ for _, ignoreFile := range []string{".gitignore", ".crushignore"} {
152+ ignPath := filepath.Join(dir, ignoreFile)
153+ if content, err := os.ReadFile(ignPath); err == nil {
154+ lines := strings.Split(string(content), "\n")
155+ allPatterns = append(allPatterns, parsePatterns(lines, domain)...)
156+ }
157+ }
158+ return allPatterns
159+ })
160+}
161+
162+// getCombinedMatcher returns a matcher that combines all gitignore patterns
163+// from the root to the given directory, plus common patterns and home patterns.
164+// Results are cached per directory, and we reuse parent directory matchers.
165+func (dl *directoryLister) getCombinedMatcher(dir string) gitignore.Matcher {
166+ return dl.combinedMatchers.GetOrSet(dir, func() gitignore.Matcher {
167+ var allPatterns []gitignore.Pattern
168+
169+ // Add common patterns first (lowest priority).
119−// - [commonIgnorePatterns]
170+ allPatterns = append(allPatterns, commonIgnorePatterns()...)
120−// - ./.gitignore, ../.gitignore, etc, until dl.rootPath
171+
121−// - ./.crushignore, ../.crushignore, etc, until dl.rootPath
172+ // Add home ignore patterns.
122−// ~/.config/git/ignore
173+ allPatterns = append(allPatterns, homeIgnorePatterns()...)
123−// ~/.gitignore
174+
124−// ~/.config/crush/ignore
175+ // Collect patterns from root to this directory.
176+ relDir, _ := filepath.Rel(dl.rootPath, dir)
177+ var pathParts []string
178+ if relDir != "" && relDir != "." {
179+ pathParts = pathToComponents(relDir)
180+ }
181+
182+ // Add patterns from each directory from root to current.
183+ currentPath := dl.rootPath
184+ allPatterns = append(allPatterns, dl.getDirPatterns(currentPath)...)
185+
186+ for _, part := range pathParts {
187+ currentPath = filepath.Join(currentPath, part)
188+ allPatterns = append(allPatterns, dl.getDirPatterns(currentPath)...)
189+ }
190+
191+ return gitignore.NewMatcher(allPatterns)
192+ })
193+}
194+
195+// shouldIgnore checks if a path should be ignored based on gitignore rules.
196+// This uses a combined matcher that includes all ancestor patterns for O(1) matching.
125−func (dl *directoryLister) shouldIgnore(path string, ignorePatterns []string) bool {
197+func (dl *directoryLister) shouldIgnore(path string, ignorePatterns []string, isDir bool) bool {
198+ base := filepath.Base(path)
199+
200+ // Fast path: O(1) lookup for commonly ignored directories.
201+ if isDir && fastIgnoreDirs[base] {
202+ return true
203+ }
204+
205+ // Check explicit ignore patterns.
126 if len(ignorePatterns) > 0 {
206 if len(ignorePatterns) > 0 {
127− base := filepath.Base(path)
128 for _, pattern := range ignorePatterns {
207 for _, pattern := range ignorePatterns {
129 if matched, err := filepath.Match(pattern, base); err == nil && matched {
208 if matched, err := filepath.Match(pattern, base); err == nil && matched {
130 return true
209 return true
@@ -132,8 +211,7 @@ func (dl *directoryLister) shouldIgnore(path string, ignorePatterns []string) bo
132 }
211 }
133 }
212 }
134
213
135− // Don't apply gitignore rules to the root directory itself
214+ // Don't apply gitignore rules to the root directory itself.
136− // In gitignore semantics, patterns don't apply to the repo root
137 if path == dl.rootPath {
215 if path == dl.rootPath {
138 return false
216 return false
139 }
217 }
@@ -143,69 +221,24 @@ func (dl *directoryLister) shouldIgnore(path string, ignorePatterns []string) bo
143 relPath = path
221 relPath = path
144 }
222 }
145
223
146− if commonIgnorePatterns().MatchesPath(relPath) {
224+ pathComponents := pathToComponents(relPath)
147− slog.Debug("Ignoring common pattern", "path", relPath)
225+ if len(pathComponents) == 0 {
148− return true
226+ return false
149 }
227 }
150
228
229+ // Get the combined matcher for the parent directory.
151 parentDir := filepath.Dir(path)
230 parentDir := filepath.Dir(path)
152− ignoreParser := dl.getIgnore(parentDir)
231+ matcher := dl.getCombinedMatcher(parentDir)
153− if ignoreParser.MatchesPath(relPath) {
154− slog.Debug("Ignoring dir pattern", "path", relPath, "dir", parentDir)
155− return true
156− }
157
232
158− // For directories, also check with trailing slash (gitignore convention)
233+ if matcher.Match(pathComponents, isDir) {
159− if ignoreParser.MatchesPath(relPath + "/") {
160− slog.Debug("Ignoring dir pattern with slash", "path", relPath+"/", "dir", parentDir)
234+ slog.Debug("Ignoring path", "path", relPath)
161 return true
235 return true
162 }
236 }
163
237
164− if dl.checkParentIgnores(relPath) {
165− return true
166− }
167−
168− if homeIgnore().MatchesPath(relPath) {
169− slog.Debug("Ignoring home dir pattern", "path", relPath)
170− return true
171− }
172−
173− return false
174−}
175−
176−func (dl *directoryLister) checkParentIgnores(path string) bool {
177− parent := filepath.Dir(filepath.Dir(path))
178− for parent != "." && path != "." {
179− if dl.getIgnore(parent).MatchesPath(path) {
180− slog.Debug("Ignoring parent dir pattern", "path", path, "dir", parent)
181− return true
182− }
183− if parent == dl.rootPath {
184− break
185− }
186− parent = filepath.Dir(parent)
187− }
188 return false
238 return false
189}
239}
190
240
191−func (dl *directoryLister) getIgnore(path string) ignore.IgnoreParser {
192− return dl.ignores.GetOrSet(path, func() ignore.IgnoreParser {
193− var lines []string
194− for _, ign := range []string{".crushignore", ".gitignore"} {
195− name := filepath.Join(path, ign)
196− if content, err := os.ReadFile(name); err == nil {
197− lines = append(lines, strings.Split(string(content), "\n")...)
198− }
199− }
200− if len(lines) == 0 {
201− // Return a no-op parser to avoid nil checks
202− return ignore.CompileIgnoreLines()
203− }
204− return ignore.CompileIgnoreLines(lines...)
205− })
206−}
207−
208−// ListDirectory lists files and directories in the specified path,
241+// ListDirectory lists files and directories in the specified path.
209func ListDirectory(initialPath string, ignorePatterns []string, depth, limit int) ([]string, bool, error) {
242func ListDirectory(initialPath string, ignorePatterns []string, depth, limit int) ([]string, bool, error) {
210 found := csync.NewSlice[string]()
243 found := csync.NewSlice[string]()
211 dl := NewDirectoryLister(initialPath)
244 dl := NewDirectoryLister(initialPath)
@@ -224,15 +257,16 @@ func ListDirectory(initialPath string, ignorePatterns []string, depth, limit int
224 return nil // Skip files we don't have permission to access
257 return nil // Skip files we don't have permission to access
225 }
258 }
226
259
260+ isDir := d.IsDir()
227− if dl.shouldIgnore(path, ignorePatterns) {
261+ if dl.shouldIgnore(path, ignorePatterns, isDir) {
228− if d.IsDir() {
262+ if isDir {
229 return filepath.SkipDir
263 return filepath.SkipDir
230 }
264 }
231 return nil
265 return nil
232 }
266 }
233
267
234 if path != initialPath {
268 if path != initialPath {
235− if d.IsDir() {
269+ if isDir {
236 path = path + string(filepath.Separator)
270 path = path + string(filepath.Separator)
237 }
271 }
238 found.Append(path)
272 found.Append(path)
internal/fsext/ls_test.go+3−2
@@ -31,11 +31,12 @@ func TestListDirectory(t *testing.T) {
31 files, truncated, err := ListDirectory(tmp, nil, -1, -1)
31 files, truncated, err := ListDirectory(tmp, nil, -1, -1)
32 require.NoError(t, err)
32 require.NoError(t, err)
33 require.False(t, truncated)
33 require.False(t, truncated)
34+ // The .gitignore has ".*" pattern which ignores hidden files anywhere
35+ // (like real git does), so subdir/.another is ignored.
34− require.Len(t, files, 4)
36+ require.Len(t, files, 3)
35 require.ElementsMatch(t, []string{
37 require.ElementsMatch(t, []string{
36 "regular.txt",
38 "regular.txt",
37 "subdir",
39 "subdir",
38− "subdir/.another",
39 "subdir/file.go",
40 "subdir/file.go",
40 }, relPaths(t, files, tmp))
41 }, relPaths(t, files, tmp))
41 })
42 })