From 02cb885795cf279c353ea9a003c4e8fdce4b1c7a Mon Sep 17 00:00:00 2001 From: Singee Date: Fri, 13 Oct 2023 15:58:14 +0800 Subject: [PATCH] lint-staged: support ignore --- README.md | 2 +- go.mod | 14 +- go.sum | 39 +- internal/ext/lint-staged/errors.go | 1 + internal/ext/lint-staged/ignore.go | 32 ++ internal/ext/lint-staged/run.go | 10 + internal/ext/lint-staged/state.go | 1 + internal/lib/gitignore/ignore.go | 240 ----------- internal/lib/gitignore/ignore_ported_test.go | 272 ------------- internal/lib/gitignore/ignore_test.go | 394 ------------------- internal/lib/ignore/ignore.go | 93 +++++ 11 files changed, 181 insertions(+), 917 deletions(-) create mode 100644 internal/ext/lint-staged/ignore.go delete mode 100644 internal/lib/gitignore/ignore.go delete mode 100644 internal/lib/gitignore/ignore_ported_test.go delete mode 100644 internal/lib/gitignore/ignore_test.go create mode 100644 internal/lib/ignore/ignore.go diff --git a/README.md b/README.md index a6d96a8..e968502 100644 --- a/README.md +++ b/README.md @@ -159,7 +159,7 @@ If you really want to use kitty on Windows, PR is welcome. - [husky](https://github.com/typicode/husky/tree/main) - [lint-staged](https://github.com/okonet/lint-staged) -- [go-gitignore](https://github.com/sabhiram/go-gitignore) +- [go-git](https://github.com/go-git/go-git) ## License diff --git a/go.mod b/go.mod index b7b7199..a8fc757 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,8 @@ require ( github.com/ImSingee/tt v1.0.4 github.com/alessio/shellescape v1.4.2 github.com/charmbracelet/bubbletea v0.24.2 + github.com/go-git/go-billy/v5 v5.5.0 + github.com/go-git/go-git/v5 v5.9.0 github.com/gobwas/glob v0.2.3 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 github.com/google/uuid v1.3.1 @@ -17,10 +19,14 @@ require ( ) require ( + github.com/acomagu/bufpipe v1.0.4 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // 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/inconshreveable/mousetrap v1.1.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-localereader v0.0.1 // indirect @@ -32,9 +38,11 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rivo/uniseg v0.4.4 // indirect github.com/spf13/pflag v1.0.5 // indirect + golang.org/x/net v0.15.0 // indirect golang.org/x/sync v0.1.0 // indirect - golang.org/x/sys v0.11.0 // indirect - golang.org/x/term v0.11.0 // indirect - golang.org/x/text v0.12.0 // indirect + golang.org/x/sys v0.12.0 // indirect + golang.org/x/term v0.12.0 // indirect + golang.org/x/text v0.13.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 63a69a6..15e4bac 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,8 @@ github.com/ImSingee/semver v0.1.0 h1:h0l6RYP2KK3PKQlvd4TDsvHWThwcjGHm47GtTk9hMR8 github.com/ImSingee/semver v0.1.0/go.mod h1:oFE1h7iyQ3+khH/BqhWtr03FMSKeWIApAhIrWF/bf+E= github.com/ImSingee/tt v1.0.4 h1:avDmypiAGmTEaRVJ1hweLzggyKRVfuUfLsGrebTrAyQ= github.com/ImSingee/tt v1.0.4/go.mod h1:7O7v+cIBruYWGFObw85DDjH0gNLquen1cJqMR3GOgxw= +github.com/acomagu/bufpipe v1.0.4 h1:e3H4WUzM3npvo5uv95QuJM3cQspFNtFBzvJ2oNjKIDQ= +github.com/acomagu/bufpipe v1.0.4/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4= github.com/alessio/shellescape v1.4.2 h1:MHPfaU+ddJ0/bYWpgIeUnQUqKrlJ1S7BfEYPM4uEoM0= github.com/alessio/shellescape v1.4.2/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= @@ -13,18 +15,33 @@ github.com/charmbracelet/bubbletea v0.24.2/go.mod h1:XdrNrV4J8GiyshTtx3DNuYkR1FD github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY= github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +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.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.9.0 h1:cD9SFA7sHVRdJ7AYck1ZaAa/yeuBvGPxwXDL8cxrObY= +github.com/go-git/go-git/v5 v5.9.0/go.mod h1:RKIqga24sWdMGZF+1Ekv9kylsDz6LzdTSI2s/OsZWE0= 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.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +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/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A= +github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA= github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= @@ -40,12 +57,16 @@ 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.1 h1:UzuTb/+hhlBugQz28rpzey4ZuKcZ03MeKsoG7IJZIxs= github.com/muesli/termenv v0.15.1/go.mod h1:HeAQPTzpfs016yGtA4g00CsdYnVLJvxsS4ANqrZs2sQ= +github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= +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/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= @@ -55,17 +76,21 @@ github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcU github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/ysmood/gson v0.7.3 h1:QFkWbTH8MxyUTKPkVWAENJhxqdBa4lYTQWqZCiLG6kE= github.com/ysmood/gson v0.7.3/go.mod h1:3Kzs5zDl21g5F/BlLTNcuAGAYLKt2lV5G8D1zF3RNmg= +golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= -golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.11.0 h1:F9tnn/DA/Im8nCwm+fX+1/eBwi4qFjRT++MhtVC4ZX0= -golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= -golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc= -golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.12.0 h1:/ZfYdc3zq+q02Rv9vGqTeSItdzZTSNDmfTi0mBAuidU= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 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/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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/ext/lint-staged/errors.go b/internal/ext/lint-staged/errors.go index a28194f..d9efc75 100644 --- a/internal/ext/lint-staged/errors.go +++ b/internal/ext/lint-staged/errors.go @@ -10,6 +10,7 @@ var ( ErrGetBackupStash = fmt.Errorf("get backup stash error") ErrGetStagedFiles = fmt.Errorf("get staged files error") ErrGitRepo = fmt.Errorf("git repo error") + ErrIgnore = fmt.Errorf("load ignore rules error") ErrHideUnstagedChanges = fmt.Errorf("hide unstaged changes error") ErrInvalidOptions = fmt.Errorf("invalid options") ErrRestoreMergeStatus = fmt.Errorf("restore merge status error") diff --git a/internal/ext/lint-staged/ignore.go b/internal/ext/lint-staged/ignore.go new file mode 100644 index 0000000..91273a8 --- /dev/null +++ b/internal/ext/lint-staged/ignore.go @@ -0,0 +1,32 @@ +package lintstaged + +import ( + "strings" + + "github.com/ImSingee/go-ex/ee" + + "github.com/ImSingee/kitty/internal/lib/ignore" +) + +type IgnoreChecker struct { + matcher ignore.Matcher +} + +var ignoreFilenames = []string{ + ".kittyignore", + ".lintstagedignore", +} + +func NewIgnoreChecker(repoRoot string) (*IgnoreChecker, error) { + ps, err := ignore.ReadPatterns(repoRoot, nil, ignoreFilenames...) + if err != nil { + return nil, ee.Wrap(err, "cannot read ignore patterns") + } + return &IgnoreChecker{ignore.NewMatcher(ps)}, nil +} + +func (c *IgnoreChecker) ShouldIgnore(gitRelativePath string) bool { + parts := strings.Split(gitRelativePath, "/") + + return c.matcher.Match(parts, false) +} diff --git a/internal/ext/lint-staged/run.go b/internal/ext/lint-staged/run.go index f43236d..afaca57 100644 --- a/internal/ext/lint-staged/run.go +++ b/internal/ext/lint-staged/run.go @@ -92,6 +92,13 @@ func runAll(options *Options) (*State, error) { slog.Debug("Grouped staged files by config", "count", usedConfigsCount, "filesByConfig", debugFilesByConfig) } + ctx.ignoreChecker, err = NewIgnoreChecker(gitDir) + if err != nil { + ctx.errors.Add(ErrIgnore) + pp.ERedPrintf("%s Cannot load ignore rules (%s)!\n", x, err.Error()) + return ctx, ee.Phantom + } + chunkedFilenamesArray := chunkFiles(files.RelativePathsToGitRoot(), defaultMaxArgLength()) slog.Debug("Get chunked filenames arrays", "groupCount", len(chunkedFilenamesArray), "arrays", chunkedFilenamesArray) @@ -329,6 +336,9 @@ func generateTaskForRule(ctx *State, wd string, rule *Rule, files Files, options files = mr.Filter(files, func(in *File, index int) bool { return strings.HasPrefix(in.AbsolutePath(), wd+string(filepath.Separator)) }) + files = mr.Filter(files, func(in *File, index int) bool { + return !ctx.ignoreChecker.ShouldIgnore(in.GitRelativePath()) + }) files = mr.Filter(files, func(in *File, index int) bool { // TODO the glob only support simple case now, will make it enhance return glob.Match(rule.GlobString, rule.Glob, in.AbsolutePath()) diff --git a/internal/ext/lint-staged/state.go b/internal/ext/lint-staged/state.go index 209327e..58f559b 100644 --- a/internal/ext/lint-staged/state.go +++ b/internal/ext/lint-staged/state.go @@ -13,6 +13,7 @@ type State struct { hasPartiallyStagedFiles bool taskResults *sync.Map + ignoreChecker *IgnoreChecker output []string // all outputs will print to stderr at end errors *set.Set[error] diff --git a/internal/lib/gitignore/ignore.go b/internal/lib/gitignore/ignore.go deleted file mode 100644 index c7c657d..0000000 --- a/internal/lib/gitignore/ignore.go +++ /dev/null @@ -1,240 +0,0 @@ -/* -Package ignore is a library which returns a new ignorer object which can -test against various paths. This is particularly useful when trying -to filter files based on a .gitignore document - -The rules for parsing the input file are the same as the ones listed -in the Git docs here: http://git-scm.com/docs/gitignore - -The summarized version of the same has been copied here: - - 1. A blank line matches no files, so it can serve as a separator - for readability. - 2. A line starting with # serves as a comment. Put a backslash ("\") - in front of the first hash for patterns that begin with a hash. - 3. Trailing spaces are ignored unless they are quoted with backslash ("\"). - 4. An optional prefix "!" which negates the pattern; any matching file - excluded by a previous pattern will become included again. It is not - possible to re-include a file if a parent directory of that file is - excluded. Git doesn’t list excluded directories for performance reasons, - so any patterns on contained files have no effect, no matter where they - are defined. Put a backslash ("\") in front of the first "!" for - patterns that begin with a literal "!", for example, "\!important!.txt". - 5. If the pattern ends with a slash, it is removed for the purpose of the - following description, but it would only find a match with a directory. - In other words, foo/ will match a directory foo and paths underneath it, - but will not match a regular file or a symbolic link foo (this is - consistent with the way how pathspec works in general in Git). - 6. If the pattern does not contain a slash /, Git treats it as a shell glob - pattern and checks for a match against the pathname relative to the - location of the .gitignore file (relative to the toplevel of the work - tree if not from a .gitignore file). - 7. Otherwise, Git treats the pattern as a shell glob suitable for - consumption by fnmatch(3) with the FNM_PATHNAME flag: wildcards in the - pattern will not match a / in the pathname. For example, - "Documentation/*.html" matches "Documentation/git.html" but not - "Documentation/ppc/ppc.html" or "tools/perf/Documentation/perf.html". - 8. A leading slash matches the beginning of the pathname. For example, - "/*.c" matches "cat-file.c" but not "mozilla-sha1/sha1.c". - 9. Two consecutive asterisks ("**") in patterns matched against full - pathname may have special meaning: - i. A leading "**" followed by a slash means match in all directories. - For example, "** /foo" matches file or directory "foo" anywhere, - the same as pattern "foo". "** /foo/bar" matches file or directory - "bar" anywhere that is directly under directory "foo". - ii. A trailing "/**" matches everything inside. For example, "abc/**" - matches all files inside directory "abc", relative to the location - of the .gitignore file, with infinite depth. - iii. A slash followed by two consecutive asterisks then a slash matches - zero or more directories. For example, "a/** /b" matches "a/b", - "a/x/b", "a/x/y/b" and so on. - iv. Other consecutive asterisks are considered invalid. -*/ -package ignore - -import ( - "io/ioutil" - "os" - "regexp" - "strings" -) - -//////////////////////////////////////////////////////////// - -// IgnoreParser is an interface with `MatchesPaths`. -type IgnoreParser interface { - MatchesPath(f string) bool - MatchesPathHow(f string) (bool, *IgnorePattern) -} - -//////////////////////////////////////////////////////////// - -// This function pretty much attempts to mimic the parsing rules -// listed above at the start of this file -func getPatternFromLine(line string) (*regexp.Regexp, bool) { - // Trim OS-specific carriage returns. - line = strings.TrimRight(line, "\r") - - // Strip comments [Rule 2] - if strings.HasPrefix(line, `#`) { - return nil, false - } - - // Trim string [Rule 3] - // TODO: Handle [Rule 3], when the " " is escaped with a \ - line = strings.Trim(line, " ") - - // Exit for no-ops and return nil which will prevent us from - // appending a pattern against this line - if line == "" { - return nil, false - } - - // TODO: Handle [Rule 4] which negates the match for patterns leading with "!" - negatePattern := false - if line[0] == '!' { - negatePattern = true - line = line[1:] - } - - // Handle [Rule 2, 4], when # or ! is escaped with a \ - // Handle [Rule 4] once we tag negatePattern, strip the leading ! char - if regexp.MustCompile(`^(\#|\!)`).MatchString(line) { - line = line[1:] - } - - // If we encounter a foo/*.blah in a folder, prepend the / char - if regexp.MustCompile(`([^\/+])/.*\*\.`).MatchString(line) && line[0] != '/' { - line = "/" + line - } - - // Handle escaping the "." char - line = regexp.MustCompile(`\.`).ReplaceAllString(line, `\.`) - - magicStar := "#$~" - - // Handle "/**/" usage - if strings.HasPrefix(line, "/**/") { - line = line[1:] - } - line = regexp.MustCompile(`/\*\*/`).ReplaceAllString(line, `(/|/.+/)`) - line = regexp.MustCompile(`\*\*/`).ReplaceAllString(line, `(|.`+magicStar+`/)`) - line = regexp.MustCompile(`/\*\*`).ReplaceAllString(line, `(|/.`+magicStar+`)`) - - // Handle escaping the "*" char - line = regexp.MustCompile(`\\\*`).ReplaceAllString(line, `\`+magicStar) - line = regexp.MustCompile(`\*`).ReplaceAllString(line, `([^/]*)`) - - // Handle escaping the "?" char - line = strings.Replace(line, "?", `\?`, -1) - - line = strings.Replace(line, magicStar, "*", -1) - - // Temporary regex - var expr = "" - if strings.HasSuffix(line, "/") { - expr = line + "(|.*)$" - } else { - expr = line + "(|/.*)$" - } - if strings.HasPrefix(expr, "/") { - expr = "^(|/)" + expr[1:] - } else { - expr = "^(|.*/)" + expr - } - pattern, _ := regexp.Compile(expr) - - return pattern, negatePattern -} - -//////////////////////////////////////////////////////////// - -// IgnorePattern encapsulates a pattern and if it is a negated pattern. -type IgnorePattern struct { - Pattern *regexp.Regexp - Negate bool - LineNo int - Line string -} - -// GitIgnore wraps a list of ignore pattern. -type GitIgnore struct { - patterns []*IgnorePattern -} - -// CompileIgnoreLines accepts a variadic set of strings, and returns a GitIgnore -// instance which converts and appends the lines in the input to regexp.Regexp -// patterns held within the GitIgnore objects "patterns" field. -func CompileIgnoreLines(lines ...string) *GitIgnore { - gi := &GitIgnore{} - for i, line := range lines { - pattern, negatePattern := getPatternFromLine(line) - if pattern != nil { - // LineNo is 1-based numbering to match `git check-ignore -v` output - ip := &IgnorePattern{pattern, negatePattern, i + 1, line} - gi.patterns = append(gi.patterns, ip) - } - } - return gi -} - -// CompileIgnoreFile uses an ignore file as the input, parses the lines out of -// the file and invokes the CompileIgnoreLines method. -func CompileIgnoreFile(fpath string) (*GitIgnore, error) { - bs, err := ioutil.ReadFile(fpath) - if err != nil { - return nil, err - } - - s := strings.Split(string(bs), "\n") - return CompileIgnoreLines(s...), nil -} - -// CompileIgnoreFileAndLines accepts a ignore file as the input, parses the -// lines out of the file and invokes the CompileIgnoreLines method with -// additional lines. -func CompileIgnoreFileAndLines(fpath string, lines ...string) (*GitIgnore, error) { - bs, err := ioutil.ReadFile(fpath) - if err != nil { - return nil, err - } - - gi := CompileIgnoreLines(append(strings.Split(string(bs), "\n"), lines...)...) - return gi, nil -} - -//////////////////////////////////////////////////////////// - -// MatchesPath returns true if the given GitIgnore structure would target -// a given path string `f`. -func (gi *GitIgnore) MatchesPath(f string) bool { - matchesPath, _ := gi.MatchesPathHow(f) - return matchesPath -} - -// MatchesPathHow returns true, `pattern` if the given GitIgnore structure would target -// a given path string `f`. -// The IgnorePattern has the Line, LineNo fields. -func (gi *GitIgnore) MatchesPathHow(f string) (bool, *IgnorePattern) { - // Replace OS-specific path separator. - f = strings.Replace(f, string(os.PathSeparator), "/", -1) - - matchesPath := false - var mip *IgnorePattern - for _, ip := range gi.patterns { - if ip.Pattern.MatchString(f) { - // If this is a regular target (not negated with a gitignore - // exclude "!" etc) - if !ip.Negate { - matchesPath = true - mip = ip - } else if matchesPath { - // Negated pattern, and matchesPath is already set - matchesPath = false - } - } - } - return matchesPath, mip -} - -//////////////////////////////////////////////////////////// diff --git a/internal/lib/gitignore/ignore_ported_test.go b/internal/lib/gitignore/ignore_ported_test.go deleted file mode 100644 index 729a56d..0000000 --- a/internal/lib/gitignore/ignore_ported_test.go +++ /dev/null @@ -1,272 +0,0 @@ -// Implement tests, ported from https://github.com/kaelzhang/node-ignore.git -package ignore - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestSimple(test *testing.T) { - lines := []string{"foo"} - object := CompileIgnoreLines(lines...) - - shouldMatch(test, object, "foo") - shouldMatch(test, object, "foo/") - shouldMatch(test, object, "/foo") - shouldNotMatch(test, object, "fooo") - shouldNotMatch(test, object, "ofoo") -} - -func TestAnywhere(test *testing.T) { - lines := []string{"**/foo"} - object := CompileIgnoreLines(lines...) - - shouldMatch(test, object, "foo") - shouldMatch(test, object, "foo/") - shouldMatch(test, object, "/foo") - shouldNotMatch(test, object, "fooo") - shouldNotMatch(test, object, "ofoo") -} - -func TestAnywhereFromRoot(test *testing.T) { - lines := []string{"/**/foo"} - object := CompileIgnoreLines(lines...) - - shouldMatch(test, object, "foo") - shouldMatch(test, object, "foo/") - shouldMatch(test, object, "/foo") - shouldNotMatch(test, object, "fooo") - shouldNotMatch(test, object, "ofoo") -} - -func TestSimpleDir(test *testing.T) { - lines := []string{"foo/"} - object := CompileIgnoreLines(lines...) - - shouldMatch(test, object, "foo/") - shouldMatch(test, object, "foo/a") - shouldMatch(test, object, "/foo/") - shouldNotMatch(test, object, "foo") - shouldNotMatch(test, object, "/foo") -} - -func TestRootExtensionOnly(test *testing.T) { - lines := []string{"/.js"} - object := CompileIgnoreLines(lines...) - - shouldMatch(test, object, ".js") - shouldMatch(test, object, ".js/") - shouldMatch(test, object, ".js/a") - // ??? - // shouldNotMatch(test, object, "/.js") - shouldNotMatch(test, object, ".jsa") -} - -func TestRootExtension(test *testing.T) { - lines := []string{"/*.js"} - object := CompileIgnoreLines(lines...) - - shouldMatch(test, object, ".js") - shouldMatch(test, object, ".js/") - shouldMatch(test, object, ".js/a") - shouldMatch(test, object, "a.js/a") - shouldMatch(test, object, "a.js/a.js") - // ??? - // shouldNotMatch(test, object, "/.js") - shouldNotMatch(test, object, ".jsa") -} - -func TestExtension(test *testing.T) { - lines := []string{"*.js"} - object := CompileIgnoreLines(lines...) - - shouldMatch(test, object, ".js") - shouldMatch(test, object, ".js/") - shouldMatch(test, object, ".js/a") - shouldMatch(test, object, "a.js/a") - shouldMatch(test, object, "a.js/a.js") - shouldMatch(test, object, "/.js") - shouldNotMatch(test, object, ".jsa") -} - -func TestStarExtension(test *testing.T) { - lines := []string{".js*"} - object := CompileIgnoreLines(lines...) - - shouldMatch(test, object, ".js") - shouldMatch(test, object, ".js/") - shouldMatch(test, object, ".js/a") - shouldNotMatch(test, object, "a.js/a") - shouldNotMatch(test, object, "a.js/a.js") - shouldMatch(test, object, "/.js") - shouldMatch(test, object, ".jsa") -} - -func TestDoubleStar(test *testing.T) { - lines := []string{"foo/**/"} - object := CompileIgnoreLines(lines...) - - shouldMatch(test, object, "foo/") - shouldMatch(test, object, "foo/abc/") - shouldMatch(test, object, "foo/x/y/z/") - shouldNotMatch(test, object, "foo") - shouldNotMatch(test, object, "/foo") -} - -func TestStars(test *testing.T) { - lines := []string{"foo/**/*.bar"} - object := CompileIgnoreLines(lines...) - - shouldNotMatch(test, object, "foo/") - shouldNotMatch(test, object, "abc.bar") - shouldMatch(test, object, "foo/abc.bar") - shouldMatch(test, object, "foo/abc.bar/") - shouldMatch(test, object, "foo/x/y/z.bar") - shouldMatch(test, object, "foo/x/y/z.bar/") -} - -func TestCases_Comment(test *testing.T) { - lines := []string{"#abc"} - object := CompileIgnoreLines(lines...) - - shouldNotMatch(test, object, "#abc") -} - -func TestCases_EscapedComment(test *testing.T) { - lines := []string{`\#abc`} - object := CompileIgnoreLines(lines...) - - shouldMatch(test, object, "#abc") -} - -func TestCases_CouldFilterPaths(test *testing.T) { - lines := []string{"abc", "!abc/b"} - object := CompileIgnoreLines(lines...) - - shouldMatch(test, object, "abc/a.js") - shouldNotMatch(test, object, "abc/b/b.js") -} - -func TestCases_IgnoreSelect(test *testing.T) { - lines := []string{"abc", "!abc/b", "#e", `\#f`} - object := CompileIgnoreLines(lines...) - - shouldMatch(test, object, "abc/a.js") - shouldNotMatch(test, object, "abc/b/b.js") - shouldNotMatch(test, object, "#e") - shouldMatch(test, object, "#f") -} - -func TestCases_EscapeRegexMetacharacters(test *testing.T) { - lines := []string{"*.js", `!\*.js`, "!a#b.js", "!?.js", "#abc", `\#abc`} - object := CompileIgnoreLines(lines...) - - shouldNotMatch(test, object, "*.js") - shouldMatch(test, object, "abc.js") - shouldNotMatch(test, object, "a#b.js") - shouldNotMatch(test, object, "abc") - shouldMatch(test, object, "#abc") - shouldNotMatch(test, object, "?.js") -} - -func TestCases_QuestionMark(test *testing.T) { - lines := []string{"/.project", "thumbs.db", "*.swp", ".sonar/*", ".*.sw?"} - object := CompileIgnoreLines(lines...) - - shouldMatch(test, object, ".project") - shouldNotMatch(test, object, "abc/.project") - shouldNotMatch(test, object, ".a.sw") - shouldMatch(test, object, ".a.sw?") - shouldMatch(test, object, "thumbs.db") -} - -func TestCases_DirEndedWithStar(test *testing.T) { - lines := []string{"abc/*"} - object := CompileIgnoreLines(lines...) - - shouldNotMatch(test, object, "abc") -} - -func TestCases_FileEndedWithStar(test *testing.T) { - lines := []string{"abc.js*"} - object := CompileIgnoreLines(lines...) - - shouldMatch(test, object, "abc.js/") - shouldMatch(test, object, "abc.js/abc") - shouldMatch(test, object, "abc.jsa/") - shouldMatch(test, object, "abc.jsa/abc") -} - -func TestCases_WildcardAsFilename(test *testing.T) { - lines := []string{"*.b"} - object := CompileIgnoreLines(lines...) - - shouldMatch(test, object, "b/a.b") - shouldMatch(test, object, "b/.b") - shouldNotMatch(test, object, "b/.ba") - shouldMatch(test, object, "b/c/a.b") -} - -func TestCases_SlashAtBeginningAndComeWithWildcard(test *testing.T) { - lines := []string{"/*.c"} - object := CompileIgnoreLines(lines...) - - shouldMatch(test, object, ".c") - shouldMatch(test, object, "c.c") - shouldNotMatch(test, object, "c/c.c") - shouldNotMatch(test, object, "c/d") -} - -func TestCases_DotFile(test *testing.T) { - lines := []string{".d"} - object := CompileIgnoreLines(lines...) - - shouldMatch(test, object, ".d") - shouldNotMatch(test, object, ".dd") - shouldNotMatch(test, object, "d.d") - shouldMatch(test, object, "d/.d") - shouldNotMatch(test, object, "d/d.d") - shouldNotMatch(test, object, "d/e") -} - -func TestCases_DotDir(test *testing.T) { - lines := []string{".e"} - object := CompileIgnoreLines(lines...) - - shouldMatch(test, object, ".e/") - shouldNotMatch(test, object, ".ee/") - shouldNotMatch(test, object, "e.e/") - shouldMatch(test, object, ".e/e") - shouldMatch(test, object, "e/.e") - shouldNotMatch(test, object, "e/e.e") - shouldNotMatch(test, object, "e/f") -} - -func TestCases_PatternOnce(test *testing.T) { - lines := []string{"node_modules/"} - object := CompileIgnoreLines(lines...) - - shouldMatch(test, object, "node_modules/gulp/node_modules/abc.md") - shouldMatch(test, object, "node_modules/gulp/node_modules/abc.json") -} - -func TestCases_PatternTwice(test *testing.T) { - lines := []string{"node_modules/", "node_modules/"} - object := CompileIgnoreLines(lines...) - - shouldMatch(test, object, "node_modules/gulp/node_modules/abc.md") - shouldMatch(test, object, "node_modules/gulp/node_modules/abc.json") -} - -//////////////////////////////////////////////////////////// - -func shouldMatch(test *testing.T, object *GitIgnore, path string) { - assert.Equal(test, true, object.MatchesPath(path), path+" should match") -} - -func shouldNotMatch(test *testing.T, object *GitIgnore, path string) { - assert.Equal(test, false, object.MatchesPath(path), path+" should not match") -} - -//////////////////////////////////////////////////////////// diff --git a/internal/lib/gitignore/ignore_test.go b/internal/lib/gitignore/ignore_test.go deleted file mode 100644 index 399d311..0000000 --- a/internal/lib/gitignore/ignore_test.go +++ /dev/null @@ -1,394 +0,0 @@ -// Implement tests for the `ignore` library -package ignore - -import ( - "fmt" - "io/ioutil" - "os" - "path/filepath" - "runtime" - "testing" - - "github.com/stretchr/testify/assert" -) - -const ( - TEST_DIR = "test_fixtures" -) - -//////////////////////////////////////////////////////////// - -// Helper function to setup a test fixture dir and write to -// it a file with the name "fname" and content "content" -func writeFileToTestDir(fname, content string) { - testDirPath := "." + string(filepath.Separator) + TEST_DIR - testFilePath := testDirPath + string(filepath.Separator) + fname - _ = os.MkdirAll(testDirPath, 0755) - _ = ioutil.WriteFile(testFilePath, []byte(content), os.ModePerm) -} - -func cleanupTestDir() { - _ = os.RemoveAll(fmt.Sprintf(".%s%s", string(filepath.Separator), TEST_DIR)) -} - -//////////////////////////////////////////////////////////// - -// Validate "CompileIgnoreLines()" -func TestCompileIgnoreLines(t *testing.T) { - lines := []string{"abc/def", "a/b/c", "b"} - object := CompileIgnoreLines(lines...) - - // MatchesPath - // Paths which are targeted by the above "lines" - assert.Equal(t, true, object.MatchesPath("abc/def/child"), "abc/def/child should match") - assert.Equal(t, true, object.MatchesPath("a/b/c/d"), "a/b/c/d should match") - - // Paths which are not targeted by the above "lines" - assert.Equal(t, false, object.MatchesPath("abc"), "abc should not match") - assert.Equal(t, false, object.MatchesPath("def"), "def should not match") - assert.Equal(t, false, object.MatchesPath("bd"), "bd should not match") - - object = CompileIgnoreLines("abc/def", "a/b/c", "b") - - // Paths which are targeted by the above "lines" - assert.Equal(t, true, object.MatchesPath("abc/def/child"), "abc/def/child should match") - assert.Equal(t, true, object.MatchesPath("a/b/c/d"), "a/b/c/d should match") - - // Paths which are not targeted by the above "lines" - assert.Equal(t, false, object.MatchesPath("abc"), "abc should not match") - assert.Equal(t, false, object.MatchesPath("def"), "def should not match") - assert.Equal(t, false, object.MatchesPath("bd"), "bd should not match") -} - -// Validate the invalid files -func TestCompileIgnoreFile_InvalidFile(t *testing.T) { - object, err := CompileIgnoreFile("./test_fixtures/invalid.file") - assert.Nil(t, object, "object should be nil") - assert.NotNil(t, err, "err should be unknown file / dir") -} - -// Validate the an empty files -func TestCompileIgnoreLines_EmptyFile(t *testing.T) { - writeFileToTestDir("test.gitignore", ``) - defer cleanupTestDir() - - object, err := CompileIgnoreFile("./test_fixtures/test.gitignore") - assert.Nil(t, err, "err should be nil") - assert.NotNil(t, object, "object should not be nil") - - assert.Equal(t, false, object.MatchesPath("a"), "should not match any path") - assert.Equal(t, false, object.MatchesPath("a/b"), "should not match any path") - assert.Equal(t, false, object.MatchesPath(".foobar"), "should not match any path") -} - -// Validate the correct handling of the negation operator "!" -func TestCompileIgnoreLines_HandleIncludePattern(t *testing.T) { - writeFileToTestDir("test.gitignore", ` -# exclude everything except directory foo/bar -/* -!/foo -/foo/* -!/foo/bar -`) - defer cleanupTestDir() - - object, err := CompileIgnoreFile("./test_fixtures/test.gitignore") - assert.Nil(t, err, "err should be nil") - assert.NotNil(t, object, "object should not be nil") - - assert.Equal(t, true, object.MatchesPath("a"), "a should match") - assert.Equal(t, true, object.MatchesPath("foo/baz"), "foo/baz should match") - assert.Equal(t, false, object.MatchesPath("foo"), "foo should not match") - assert.Equal(t, false, object.MatchesPath("/foo/bar"), "/foo/bar should not match") -} - -// Validate the correct handling of comments and empty lines -func TestCompileIgnoreLines_HandleSpaces(t *testing.T) { - writeFileToTestDir("test.gitignore", ` -# -# A comment - -# Another comment - - - # Invalid Comment - -abc/def -`) - defer cleanupTestDir() - - object, err := CompileIgnoreFile("./test_fixtures/test.gitignore") - assert.Nil(t, err, "err should be nil") - assert.NotNil(t, object, "object should not be nil") - - assert.Equal(t, 2, len(object.patterns), "should have two regex pattern") - assert.Equal(t, false, object.MatchesPath("abc/abc"), "/abc/abc should not match") - assert.Equal(t, true, object.MatchesPath("abc/def"), "/abc/def should match") -} - -// Validate the correct handling of leading / chars -func TestCompileIgnoreLines_HandleLeadingSlash(t *testing.T) { - writeFileToTestDir("test.gitignore", ` -/a/b/c -d/e/f -/g -`) - defer cleanupTestDir() - - object, err := CompileIgnoreFile("./test_fixtures/test.gitignore") - assert.Nil(t, err, "err should be nil") - assert.NotNil(t, object, "object should not be nil") - - assert.Equal(t, 3, len(object.patterns), "should have 3 regex patterns") - assert.Equal(t, true, object.MatchesPath("a/b/c"), "a/b/c should match") - assert.Equal(t, true, object.MatchesPath("a/b/c/d"), "a/b/c/d should match") - assert.Equal(t, true, object.MatchesPath("d/e/f"), "d/e/f should match") - assert.Equal(t, true, object.MatchesPath("g"), "g should match") -} - -// Validate the correct handling of files starting with # or ! -func TestCompileIgnoreLines_HandleLeadingSpecialChars(t *testing.T) { - writeFileToTestDir("test.gitignore", ` -# Comment -\#file.txt -\!file.txt -file.txt -`) - defer cleanupTestDir() - - object, err := CompileIgnoreFile("./test_fixtures/test.gitignore") - assert.Nil(t, err, "err should be nil") - assert.NotNil(t, object, "object should not be nil") - - assert.Equal(t, true, object.MatchesPath("#file.txt"), "#file.txt should match") - assert.Equal(t, true, object.MatchesPath("!file.txt"), "!file.txt should match") - assert.Equal(t, true, object.MatchesPath("a/!file.txt"), "a/!file.txt should match") - assert.Equal(t, true, object.MatchesPath("file.txt"), "file.txt should match") - assert.Equal(t, true, object.MatchesPath("a/file.txt"), "a/file.txt should match") - assert.Equal(t, false, object.MatchesPath("file2.txt"), "file2.txt should not match") - -} - -// Validate the correct handling matching files only within a given folder -func TestCompileIgnoreLines_HandleAllFilesInDir(t *testing.T) { - writeFileToTestDir("test.gitignore", ` -Documentation/*.html -`) - defer cleanupTestDir() - - object, err := CompileIgnoreFile("./test_fixtures/test.gitignore") - assert.Nil(t, err, "err should be nil") - assert.NotNil(t, object, "object should not be nil") - - assert.Equal(t, true, object.MatchesPath("Documentation/git.html"), "Documentation/git.html should match") - assert.Equal(t, false, object.MatchesPath("Documentation/ppc/ppc.html"), "Documentation/ppc/ppc.html should not match") - assert.Equal(t, false, object.MatchesPath("tools/perf/Documentation/perf.html"), "tools/perf/Documentation/perf.html should not match") -} - -// Validate the correct handling of "**" -func TestCompileIgnoreLines_HandleDoubleStar(t *testing.T) { - writeFileToTestDir("test.gitignore", ` -**/foo -bar -`) - defer cleanupTestDir() - - object, err := CompileIgnoreFile("./test_fixtures/test.gitignore") - assert.Nil(t, err, "err should be nil") - assert.NotNil(t, object, "object should not be nil") - - assert.Equal(t, true, object.MatchesPath("foo"), "foo should match") - assert.Equal(t, true, object.MatchesPath("baz/foo"), "baz/foo should match") - assert.Equal(t, true, object.MatchesPath("bar"), "bar should match") - assert.Equal(t, true, object.MatchesPath("baz/bar"), "baz/bar should match") -} - -// Validate the correct handling of leading slash -func TestCompileIgnoreLines_HandleLeadingSlashPath(t *testing.T) { - writeFileToTestDir("test.gitignore", ` -/*.c -`) - defer cleanupTestDir() - - object, err := CompileIgnoreFile("./test_fixtures/test.gitignore") - assert.Nil(t, err, "err should be nil") - assert.NotNil(t, object, "object should not be nil") - - assert.Equal(t, true, object.MatchesPath("hello.c"), "hello.c should match") - assert.Equal(t, false, object.MatchesPath("foo/hello.c"), "foo/hello.c should not match") -} - -func TestCompileIgnoreFileAndLines(t *testing.T) { - writeFileToTestDir("test.gitignore", ` -/*.c -`) - defer cleanupTestDir() - - object, err := CompileIgnoreFileAndLines("./test_fixtures/test.gitignore", "**/foo", "bar") - assert.Nil(t, err, "err should be nil") - assert.NotNil(t, object, "object should not be nil") - - assert.Equal(t, true, object.MatchesPath("hello.c"), "hello.c should match") - assert.Equal(t, false, object.MatchesPath("baz/hello.c"), "baz/hello.c should not match") - - assert.Equal(t, true, object.MatchesPath("foo"), "foo should match") - assert.Equal(t, true, object.MatchesPath("baz/foo"), "baz/foo should match") - assert.Equal(t, true, object.MatchesPath("bar"), "bar should match") - assert.Equal(t, true, object.MatchesPath("baz/bar"), "baz/bar should match") -} - -func TestCompileIgnoreFileAndLines_InvalidFile(t *testing.T) { - object, err := CompileIgnoreFileAndLines("./test_fixtures/invalid.file") - assert.Nil(t, object, "object should be nil") - assert.NotNil(t, err, "err should be unknown file / dir") -} - -func ExampleCompileIgnoreLines() { - ignoreObject := CompileIgnoreLines([]string{"node_modules", "*.out", "foo/*.c"}...) - - // You can test the ignoreObject against various paths using the - // "MatchesPath()" interface method. This pretty much is up to - // the users interpretation. In the case of a ".gitignore" file, - // a "match" would indicate that a given path would be ignored. - fmt.Println(ignoreObject.MatchesPath("node_modules/test/foo.js")) - fmt.Println(ignoreObject.MatchesPath("node_modules2/test.out")) - fmt.Println(ignoreObject.MatchesPath("test/foo.js")) - - // Output: - // true - // true - // false -} - -func TestCompileIgnoreLines_CheckNestedDotFiles(t *testing.T) { - lines := []string{ - "**/external/**/*.md", - "**/external/**/*.json", - "**/external/**/*.gzip", - "**/external/**/.*ignore", - - "**/external/foobar/*.css", - "**/external/barfoo/less", - "**/external/barfoo/scss", - } - object := CompileIgnoreLines(lines...) - assert.NotNil(t, object, "returned object should not be nil") - - assert.Equal(t, true, object.MatchesPath("external/foobar/angular.foo.css"), "external/foobar/angular.foo.css") - assert.Equal(t, true, object.MatchesPath("external/barfoo/.gitignore"), "external/barfoo/.gitignore") - assert.Equal(t, true, object.MatchesPath("external/barfoo/.bower.json"), "external/barfoo/.bower.json") -} - -func TestCompileIgnoreLines_CarriageReturn(t *testing.T) { - lines := []string{"abc/def\r", "a/b/c\r", "b\r"} - object := CompileIgnoreLines(lines...) - - assert.Equal(t, true, object.MatchesPath("abc/def/child"), "abc/def/child should match") - assert.Equal(t, true, object.MatchesPath("a/b/c/d"), "a/b/c/d should match") - - assert.Equal(t, false, object.MatchesPath("abc"), "abc should not match") - assert.Equal(t, false, object.MatchesPath("def"), "def should not match") - assert.Equal(t, false, object.MatchesPath("bd"), "bd should not match") -} - -func TestCompileIgnoreLines_WindowsPath(t *testing.T) { - if runtime.GOOS != "windows" { - return - } - lines := []string{"abc/def", "a/b/c", "b"} - object := CompileIgnoreLines(lines...) - - assert.Equal(t, true, object.MatchesPath("abc\\def\\child"), "abc\\def\\child should match") - assert.Equal(t, true, object.MatchesPath("a\\b\\c\\d"), "a\\b\\c\\d should match") -} - -func TestWildCardFiles(t *testing.T) { - gitIgnore := []string{"*.swp", "/foo/*.wat", "bar/*.txt"} - object := CompileIgnoreLines(gitIgnore...) - - // Paths which are targeted by the above "lines" - assert.Equal(t, true, object.MatchesPath("yo.swp"), "should ignore all swp files") - assert.Equal(t, true, object.MatchesPath("something/else/but/it/hasyo.swp"), "should ignore all swp files in other directories") - - assert.Equal(t, true, object.MatchesPath("foo/bar.wat"), "should ignore all wat files in foo - nonpreceding /") - assert.Equal(t, true, object.MatchesPath("/foo/something.wat"), "should ignore all wat files in foo - preceding /") - - assert.Equal(t, true, object.MatchesPath("bar/something.txt"), "should ignore all txt files in bar - nonpreceding /") - assert.Equal(t, true, object.MatchesPath("/bar/somethingelse.txt"), "should ignore all txt files in bar - preceding /") - - // Paths which are not targeted by the above "lines" - assert.Equal(t, false, object.MatchesPath("something/not/infoo/wat.wat"), "wat files should only be ignored in foo") - assert.Equal(t, false, object.MatchesPath("something/not/infoo/wat.txt"), "txt files should only be ignored in bar") -} - -func TestPrecedingSlash(t *testing.T) { - gitIgnore := []string{"/foo", "bar/"} - object := CompileIgnoreLines(gitIgnore...) - - assert.Equal(t, true, object.MatchesPath("foo/bar.wat"), "should ignore all files in foo - nonpreceding /") - assert.Equal(t, true, object.MatchesPath("/foo/something.txt"), "should ignore all files in foo - preceding /") - - assert.Equal(t, true, object.MatchesPath("bar/something.txt"), "should ignore all files in bar - nonpreceding /") - assert.Equal(t, true, object.MatchesPath("/bar/somethingelse.go"), "should ignore all files in bar - preceding /") - assert.Equal(t, true, object.MatchesPath("/boo/something/bar/boo.txt"), "should block all files if bar is a sub directory") - - assert.Equal(t, false, object.MatchesPath("something/foo/something.txt"), "should only ignore top level foo directories- not nested") -} - -func TestMatchesLineNumbers(t *testing.T) { - gitIgnore := []string{"/foo", "bar/", "*.swp"} - object := CompileIgnoreLines(gitIgnore...) - - var matchesPath bool - var reason *IgnorePattern - - // /foo - matchesPath, reason = object.MatchesPathHow("foo/bar.wat") - assert.Equal(t, true, matchesPath, "should ignore all files in foo - nonpreceding /") - assert.NotNil(t, reason, "reason should not be nil") - assert.Equal(t, 1, reason.LineNo, "should match with line 1") - assert.Equal(t, gitIgnore[0], reason.Line, "should match with line /foo") - - matchesPath, reason = object.MatchesPathHow("/foo/something.txt") - assert.Equal(t, true, matchesPath, "should ignore all files in foo - preceding /") - assert.NotNil(t, reason, "reason should not be nil") - assert.Equal(t, 1, reason.LineNo, "should match with line 1") - assert.Equal(t, gitIgnore[0], reason.Line, "should match with line /foo") - - // bar/ - matchesPath, reason = object.MatchesPathHow("bar/something.txt") - assert.Equal(t, true, matchesPath, "should ignore all files in bar - nonpreceding /") - assert.NotNil(t, reason, "reason should not be nil") - assert.Equal(t, 2, reason.LineNo, "should match with line 2") - assert.Equal(t, gitIgnore[1], reason.Line, "should match with line bar/") - - matchesPath, reason = object.MatchesPathHow("/bar/somethingelse.go") - assert.Equal(t, true, matchesPath, "should ignore all files in bar - preceding /") - assert.NotNil(t, reason, "reason should not be nil") - assert.Equal(t, 2, reason.LineNo, "should match with line 2") - assert.Equal(t, gitIgnore[1], reason.Line, "should match with line bar/") - - matchesPath, reason = object.MatchesPathHow("/boo/something/bar/boo.txt") - assert.Equal(t, true, matchesPath, "should block all files if bar is a sub directory") - assert.NotNil(t, reason, "reason should not be nil") - assert.Equal(t, 2, reason.LineNo, "should match with line 2") - assert.Equal(t, gitIgnore[1], reason.Line, "should match with line bar/") - - // *.swp - matchesPath, reason = object.MatchesPathHow("yo.swp") - assert.Equal(t, true, matchesPath, "should ignore all swp files") - assert.NotNil(t, reason, "reason should not be nil") - assert.Equal(t, 3, reason.LineNo, "should match with line 3") - assert.Equal(t, gitIgnore[2], reason.Line, "should match with line *.swp") - - matchesPath, reason = object.MatchesPathHow("something/else/but/it/hasyo.swp") - assert.Equal(t, true, matchesPath, "should ignore all swp files in other directories") - assert.NotNil(t, reason, "reason should not be nil") - assert.Equal(t, 3, reason.LineNo, "should match with line 3") - assert.Equal(t, gitIgnore[2], reason.Line, "should match with line *.swp") - - // other - matchesPath, reason = object.MatchesPathHow("something/foo/something.txt") - assert.Equal(t, false, matchesPath, "should only ignore top level foo directories- not nested") - assert.Nil(t, reason, "reason should be nil as no match should happen") -} diff --git a/internal/lib/ignore/ignore.go b/internal/lib/ignore/ignore.go new file mode 100644 index 0000000..dba599a --- /dev/null +++ b/internal/lib/ignore/ignore.go @@ -0,0 +1,93 @@ +package ignore + +import ( + "bufio" + "os" + "path/filepath" + "strings" + + "github.com/go-git/go-git/v5/plumbing/format/gitignore" +) + +type Matcher = gitignore.Matcher +type Pattern = gitignore.Pattern + +const ( + commentPrefix = "#" + gitDir = ".git" +) + +func NewMatcher(ps []Pattern) Matcher { + return gitignore.NewMatcher(ps) +} + +// ReadPatterns read and parse ignoreFileNames recursively +// +// The result is in the ascending order of priority (last higher). +func ReadPatterns(root string, dirs []string, ignoreFileNames ...string) ([]Pattern, error) { + ps, err := readIgnoreFiles(root, dirs, ignoreFileNames...) + if err != nil { + return nil, err + } + + sub, err := os.ReadDir(filepath.Join(root, filepath.Join(dirs...))) + if err != nil { + return nil, err + } + + for _, fi := range sub { + if !fi.IsDir() || fi.Name() == gitDir { + continue + } + + nextDirs := make([]string, 0, len(dirs)+1) + nextDirs = append(nextDirs, dirs...) + nextDirs = append(nextDirs, fi.Name()) + + subps, err := ReadPatterns(root, nextDirs, ignoreFileNames...) + if err != nil { + return nil, err + } + ps = append(ps, subps...) + } + + return ps, nil +} + +func readIgnoreFiles(root string, dirs []string, ignoreFiles ...string) (ps []Pattern, err error) { + for _, ignoreFile := range ignoreFiles { + subps, err := readIgnoreFile(root, dirs, ignoreFile) + if err != nil { + return nil, err + } + + ps = append(ps, subps...) + } + return +} + +// readIgnoreFile reads a specific git ignore file. +func readIgnoreFile(root string, dirs []string, ignoreFile string) (ps []Pattern, err error) { + filename := filepath.Join(root, filepath.Join(dirs...), ignoreFile) + + f, err := os.Open(filename) + if err != nil { + if os.IsNotExist(err) { + return nil, nil + } + return nil, err + } + defer f.Close() + + scanner := bufio.NewScanner(f) + for scanner.Scan() { + s := strings.TrimSpace(scanner.Text()) + if strings.HasPrefix(s, commentPrefix) { + continue + } + + ps = append(ps, gitignore.ParsePattern(s, dirs)) + } + + return +}