From acd4997459848f7eddb0a729d9794196e7a451d5 Mon Sep 17 00:00:00 2001 From: Brian McGee Date: Wed, 3 Jan 2024 10:37:33 +0000 Subject: [PATCH] feat: support .gitignore files By default, any files which match `.gitignore` files anywhere within the tree root will be ignored. This behaviour can be disabled by providing the `--no-gitignore` flag. Signed-off-by: Brian McGee --- go.mod | 10 ++++-- go.sum | 34 ++++++++++++++++---- gomod2nix.toml | 28 ++++++++++++++--- internal/cache/cache.go | 28 +++++++++++++++-- internal/cli/cli.go | 1 + internal/cli/format.go | 2 +- internal/cli/format_test.go | 63 +++++++++++++++++++++++++++++++++++++ internal/format/glob.go | 2 +- internal/git/ignore.go | 38 ++++++++++++++++++++++ internal/test/temp.go | 7 +++++ 10 files changed, 195 insertions(+), 18 deletions(-) create mode 100644 internal/git/ignore.go diff --git a/go.mod b/go.mod index 8263bc4..f2f6020 100644 --- a/go.mod +++ b/go.mod @@ -7,8 +7,9 @@ require ( github.com/adrg/xdg v0.4.0 github.com/alecthomas/kong v0.8.1 github.com/charmbracelet/log v0.3.1 + github.com/go-git/go-billy/v5 v5.5.0 + github.com/go-git/go-git/v5 v5.11.0 github.com/gobwas/glob v0.2.3 - github.com/juju/errors v1.0.0 github.com/otiai10/copy v1.14.0 github.com/stretchr/testify v1.8.4 github.com/vmihailenco/msgpack/v5 v5.4.1 @@ -19,8 +20,11 @@ require ( require ( github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/charmbracelet/lipgloss v0.9.1 // indirect + github.com/cyphar/filepath-securejoin v0.2.4 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-logfmt/logfmt v0.6.0 // indirect + github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-isatty v0.0.18 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect @@ -30,6 +34,8 @@ require ( github.com/rivo/uniseg v0.2.0 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect - golang.org/x/sys v0.13.0 // indirect + golang.org/x/net v0.19.0 // indirect + golang.org/x/sys v0.15.0 // 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 b9000cb..e8b38e7 100644 --- a/go.sum +++ b/go.sum @@ -14,19 +14,29 @@ github.com/charmbracelet/lipgloss v0.9.1 h1:PNyd3jvaJbg4jRHKWXnCj1akQm4rh8dbEzN1 github.com/charmbracelet/lipgloss v0.9.1/go.mod h1:1mPmG4cxScwUQALAAnacHaigiiHB9Pmr+v1VEawJl6I= github.com/charmbracelet/log v0.3.1 h1:TjuY4OBNbxmHWSwO3tosgqs5I3biyY8sQPny/eCMTYw= github.com/charmbracelet/log v0.3.1/go.mod h1:OR4E1hutLsax3ZKpXbgUqPtTjQfrh1pG3zwHGWuuq8g= +github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= +github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +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.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU= +github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow= +github.com/go-git/go-git/v5 v5.11.0 h1:XIZc1p+8YzypNr34itUfSvYJcv+eYdTnTvOZ2vD3cA4= +github.com/go-git/go-git/v5 v5.11.0/go.mod h1:6GFcX2P3NM7FPBfpePbpLd21XxsgdAt+lKqXmCUiUCY= github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= -github.com/juju/errors v1.0.0 h1:yiq7kjCLll1BiaRuNY53MGI0+EQ3rF6GB+wvboZDefM= -github.com/juju/errors v1.0.0/go.mod h1:B5x9thDqx0wIMH3+aLIMP9HjItInYWObRovoCFM5Qe8= -github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +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/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= @@ -40,15 +50,21 @@ github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= +github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= +github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= github.com/otiai10/copy v1.14.0 h1:dCI/t1iTdYGtkvCuBG2BgR6KZa83PTclw4U5n2wAllU= github.com/otiai10/copy v1.14.0/go.mod h1:ECfuL02W+/FkTWZWgQqXPWZgW9oeKCSQ5qVfSc4qc4w= github.com/otiai10/mint v1.5.1 h1:XaPLeE+9vGbuyEHem1JNk3bYc7KKqyI/na0/mLd/Kks= github.com/otiai10/mint v1.5.1/go.mod h1:MJm72SBthJjz8qhefc4z1PYEieWmy8Bku7CjcAqyUSM= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= +github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= @@ -61,15 +77,21 @@ go.etcd.io/bbolt v1.3.8 h1:xs88BrvEv273UsB79e0hcVrlUWmS0a8upikMFhSyAtA= go.etcd.io/bbolt v1.3.8/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw= golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= +golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= +golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= -golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/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.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= diff --git a/gomod2nix.toml b/gomod2nix.toml index db754d7..9870d73 100644 --- a/gomod2nix.toml +++ b/gomod2nix.toml @@ -19,18 +19,30 @@ schema = 3 [mod."github.com/charmbracelet/log"] version = "v0.3.1" hash = "sha256-Er60POPID2eNrRZnBHxoI4yHn0mIKnXYftGKSslbXx0=" + [mod."github.com/cyphar/filepath-securejoin"] + version = "v0.2.4" + hash = "sha256-heCD0xMxlwnHCHcRBgTjVexHOLyWI2zRW3E8NFKoLzk=" [mod."github.com/davecgh/go-spew"] version = "v1.1.1" hash = "sha256-nhzSUrE1fCkN0+RL04N4h8jWmRFPPPWbCuDc7Ss0akI=" + [mod."github.com/go-git/gcfg"] + version = "v1.5.1-0.20230307220236-3a3c6141e376" + hash = "sha256-f4k0gSYuo0/q3WOoTxl2eFaj7WZpdz29ih6CKc8Ude8=" + [mod."github.com/go-git/go-billy/v5"] + version = "v5.5.0" + hash = "sha256-4XUoD2bOCMCdu83egb/y8kY/Fm0s1rWgPMtiahh38OQ=" + [mod."github.com/go-git/go-git/v5"] + version = "v5.11.0" + hash = "sha256-2yUM/FlV+nYxacVynJCnDZeMub4Iu8JL2WBHmlnwOkE=" [mod."github.com/go-logfmt/logfmt"] version = "v0.6.0" hash = "sha256-RtIG2qARd5sT10WQ7F3LR8YJhS8exs+KiuUiVf75bWg=" [mod."github.com/gobwas/glob"] version = "v0.2.3" hash = "sha256-hYHMUdwxVkMOjSKjR7UWO0D0juHdI4wL8JEy5plu/Jc=" - [mod."github.com/juju/errors"] - version = "v1.0.0" - hash = "sha256-9uZ0wNf44ilzLsvXqOsmFUpNOBFAVadj6+ZH8+QMDMk=" + [mod."github.com/jbenet/go-context"] + version = "v0.0.0-20150711004518-d14ea06fba99" + hash = "sha256-VANNCWNNpARH/ILQV9sCQsBWgyL2iFT+4AHZREpxIWE=" [mod."github.com/lucasb-eyer/go-colorful"] version = "v1.2.0" hash = "sha256-Gg9dDJFCTaHrKHRR1SrJgZ8fWieJkybljybkI9x0gyE=" @@ -70,12 +82,18 @@ schema = 3 [mod."golang.org/x/exp"] version = "v0.0.0-20231006140011-7918f672742d" hash = "sha256-2SO1etTQ6UCUhADR5sgvDEDLHcj77pJKCIa/8mGDbAo=" + [mod."golang.org/x/net"] + version = "v0.19.0" + hash = "sha256-3M5rKEvJx4cO/q+06cGjR5sxF5JpnUWY0+fQttrWdT4=" [mod."golang.org/x/sync"] version = "v0.5.0" hash = "sha256-EAKeODSsct5HhXPmpWJfulKSCkuUu6kkDttnjyZMNcI=" [mod."golang.org/x/sys"] - version = "v0.13.0" - hash = "sha256-/+RDZ0a0oEfJ0k304VqpJpdrl2ZXa3yFlOxy4mjW7w0=" + version = "v0.15.0" + hash = "sha256-n7TlABF6179RzGq3gctPDKDPRtDfnwPdjNCMm8ps2KY=" + [mod."gopkg.in/warnings.v0"] + version = "v0.1.2" + hash = "sha256-ATVL9yEmgYbkJ1DkltDGRn/auGAjqGOfjQyBYyUo8s8=" [mod."gopkg.in/yaml.v3"] version = "v3.0.1" hash = "sha256-FqL9TKYJ0XkNwJFnq9j0VvJ5ZUU1RvH/52h/f5bkYAU=" diff --git a/internal/cache/cache.go b/internal/cache/cache.go index 120496f..435c90d 100644 --- a/internal/cache/cache.go +++ b/internal/cache/cache.go @@ -10,6 +10,8 @@ import ( "path/filepath" "time" + "git.numtide.com/numtide/treefmt/internal/git" + "git.numtide.com/numtide/treefmt/internal/format" "github.com/charmbracelet/log" @@ -172,7 +174,18 @@ func putEntry(bucket *bolt.Bucket, path string, entry *Entry) error { // ChangeSet is used to walk a filesystem, starting at root, and outputting any new or changed paths using pathsCh. // It determines if a path is new or has changed by comparing against cache entries. -func ChangeSet(ctx context.Context, root string, pathsCh chan<- string) error { +func ChangeSet(ctx context.Context, root string, gitignore bool, pathsCh chan<- string) error { + l := log.WithPrefix("cache") + + cwd, err := os.Getwd() + if err != nil { + return fmt.Errorf("%w: failed to get current working directory", err) + } + + if err := git.LoadIgnorePatterns(); err != nil { + return err + } + var tx *bolt.Tx var bucket *bolt.Bucket var processed int @@ -221,8 +234,17 @@ func ChangeSet(ctx context.Context, root string, pathsCh chan<- string) error { return nil } - // pass on the path - pathsCh <- path + relPath, err := filepath.Rel(cwd, path) + if err != nil { + return fmt.Errorf("%w: failed to determine relative path", err) + } + + if gitignore && git.Ignore(relPath) { + l.Debugf("git ignoring '%v'", path) + } else { + // pass on the path + pathsCh <- relPath + } // close the current tx if we have reached the batch size processed += 1 diff --git a/internal/cli/cli.go b/internal/cli/cli.go index 8733cbe..ae7cfa3 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -14,6 +14,7 @@ type Options struct { ConfigFile string `type:"existingfile" default:"./treefmt.toml"` FailOnChange bool `help:"Exit with error if any changes were made. Useful for CI"` Formatters []string `help:"Specify formatters to apply. Defaults to all formatters"` + NoGitignore bool `help:"Ignore .gitignore files within the tree root" default:"false"` TreeRoot string `type:"existingdir" default:"."` Verbosity int `name:"verbose" short:"v" type:"counter" default:"0" env:"LOG_LEVEL" help:"Set the verbosity of logs e.g. -vv"` diff --git a/internal/cli/format.go b/internal/cli/format.go index 9483a5b..675640a 100644 --- a/internal/cli/format.go +++ b/internal/cli/format.go @@ -191,7 +191,7 @@ func (f *Format) Run() error { eg.Go(func() error { defer close(pathsCh) - return cache.ChangeSet(ctx, Cli.TreeRoot, pathsCh) + return cache.ChangeSet(ctx, Cli.TreeRoot, !Cli.NoGitignore, pathsCh) }) // listen for shutdown and call cancel if required diff --git a/internal/cli/format_test.go b/internal/cli/format_test.go index 4773007..d5ee632 100644 --- a/internal/cli/format_test.go +++ b/internal/cli/format_test.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "os/exec" + "path" "testing" "git.numtide.com/numtide/treefmt/internal/test" @@ -343,3 +344,65 @@ func TestBustCacheOnFormatterChange(t *testing.T) { as.NoError(err) as.Contains(string(out), "0 files changed") } + +func TestGitignore(t *testing.T) { + as := require.New(t) + + // capture current cwd, so we can replace it after the test is finished + cwd, err := os.Getwd() + as.NoError(err) + + t.Cleanup(func() { + // return to the previous working directory + as.NoError(os.Chdir(cwd)) + }) + + tempDir := test.TempExamples(t) + configPath := tempDir + "/treefmt.toml" + + // test without any excludes + config := format.Config{ + Formatters: map[string]*format.Formatter{ + "echo": { + Command: "echo", + Includes: []string{"*"}, + }, + }, + } + + // without any ignores + test.WriteConfig(t, configPath, config) + out, err := cmd(t, "-C", tempDir) + as.NoError(err) + as.Contains(string(out), fmt.Sprintf("%d files changed", 29)) + + // ignore elm directory from root + test.WriteGitignore(t, tempDir, []string{"elm"}) + out, err = cmd(t, "-C", tempDir, "-c") + as.NoError(err) + as.Contains(string(out), fmt.Sprintf("%d files changed", 28)) + + // ignore a specific file from root + test.WriteGitignore(t, tempDir, []string{"elm", "go/main.go"}) + out, err = cmd(t, "-C", tempDir, "-c") + as.NoError(err) + as.Contains(string(out), fmt.Sprintf("%d files changed", 27)) + + // Ignore a haskell file using a nested gitignore + // we add 1 file (.gitignore) and ignore 1 file so the count remains the same + test.WriteGitignore(t, path.Join(tempDir, "haskell"), []string{"Nested"}) + out, err = cmd(t, "-C", tempDir, "-c") + as.NoError(err) + as.Contains(string(out), fmt.Sprintf("%d files changed", 27)) + + // Ignore a pattern from root + test.WriteGitignore(t, tempDir, []string{"elm", "go/main.go", "**/*.js"}) + out, err = cmd(t, "-C", tempDir, "-c") + as.NoError(err) + as.Contains(string(out), fmt.Sprintf("%d files changed", 26)) + + // Ignore .gitignore files + out, err = cmd(t, "-C", tempDir, "-c", "--no-gitignore") + as.NoError(err) + as.Contains(string(out), fmt.Sprintf("%d files changed", 31)) +} diff --git a/internal/format/glob.go b/internal/format/glob.go index 947ec9f..c0e8192 100644 --- a/internal/format/glob.go +++ b/internal/format/glob.go @@ -11,7 +11,7 @@ func CompileGlobs(patterns []string) ([]glob.Glob, error) { globs := make([]glob.Glob, len(patterns)) for i, pattern := range patterns { - g, err := glob.Compile("**/" + pattern) + g, err := glob.Compile(pattern) if err != nil { return nil, fmt.Errorf("%w: failed to compile include pattern '%v'", err, pattern) } diff --git a/internal/git/ignore.go b/internal/git/ignore.go new file mode 100644 index 0000000..5fe9532 --- /dev/null +++ b/internal/git/ignore.go @@ -0,0 +1,38 @@ +package git + +import ( + "fmt" + "os" + "strings" + + "github.com/go-git/go-billy/v5/osfs" + "github.com/go-git/go-git/v5/plumbing/format/gitignore" +) + +var ignorePatterns []gitignore.Pattern + +func LoadIgnorePatterns() error { + cwd, err := os.Getwd() + if err != nil { + return fmt.Errorf("%w: failed to get current working directory", err) + } + + fs := osfs.New(cwd) + ignorePatterns, err = gitignore.ReadPatterns(fs, nil) + if err != nil { + return fmt.Errorf("%w: failed to load gitignore patterns", err) + } + + return nil +} + +func Ignore(path string) bool { + split := strings.Split(path, string(os.PathSeparator)) + for _, pattern := range ignorePatterns { + if pattern.Match(split, false) == gitignore.Exclude { + return true + } + } + + return false +} diff --git a/internal/test/temp.go b/internal/test/temp.go index 3e338ad..5be331c 100644 --- a/internal/test/temp.go +++ b/internal/test/temp.go @@ -2,6 +2,8 @@ package test import ( "os" + "path" + "strings" "testing" "git.numtide.com/numtide/treefmt/internal/format" @@ -10,6 +12,11 @@ import ( "github.com/stretchr/testify/require" ) +func WriteGitignore(t *testing.T, dir string, entries []string) { + data := strings.Join(entries, "\n") + require.NoError(t, os.WriteFile(path.Join(dir, ".gitignore"), []byte(data), 0o755)) +} + func WriteConfig(t *testing.T, path string, cfg format.Config) { t.Helper() f, err := os.Create(path)