From 2f4ac21bad4c14b860a7d5c9d761857cb8d4f89c Mon Sep 17 00:00:00 2001 From: Oleg Sklyar Date: Mon, 19 Jun 2017 00:26:14 +0200 Subject: Adds gitignore support --- plumbing/format/gitignore/dir.go | 50 +++++ plumbing/format/gitignore/dir_test.go | 41 ++++ plumbing/format/gitignore/doc.go | 70 +++++++ plumbing/format/gitignore/matcher.go | 30 +++ plumbing/format/gitignore/matcher_test.go | 17 ++ plumbing/format/gitignore/pattern.go | 150 ++++++++++++++ plumbing/format/gitignore/pattern_test.go | 318 ++++++++++++++++++++++++++++++ 7 files changed, 676 insertions(+) create mode 100644 plumbing/format/gitignore/dir.go create mode 100644 plumbing/format/gitignore/dir_test.go create mode 100644 plumbing/format/gitignore/doc.go create mode 100644 plumbing/format/gitignore/matcher.go create mode 100644 plumbing/format/gitignore/matcher_test.go create mode 100644 plumbing/format/gitignore/pattern.go create mode 100644 plumbing/format/gitignore/pattern_test.go (limited to 'plumbing') diff --git a/plumbing/format/gitignore/dir.go b/plumbing/format/gitignore/dir.go new file mode 100644 index 0000000..16e4617 --- /dev/null +++ b/plumbing/format/gitignore/dir.go @@ -0,0 +1,50 @@ +package gitignore + +import ( + "io/ioutil" + "strings" + + "gopkg.in/src-d/go-billy.v2" +) + +const ( + commentPrefix = "#" + eol = "\n" + gitDir = ".git" + gitignoreFile = ".gitignore" +) + +// ReadPatterns reads gitignore patterns recursively traversing through the directory +// structure. The result is in the ascending order of priority (last higher). +func ReadPatterns(fs billy.Filesystem, path []string) (ps []Pattern, err error) { + if f, err := fs.Open(fs.Join(append(path, gitignoreFile)...)); err == nil { + defer f.Close() + if data, err := ioutil.ReadAll(f); err == nil { + for _, s := range strings.Split(string(data), eol) { + if !strings.HasPrefix(s, commentPrefix) && len(strings.TrimSpace(s)) > 0 { + ps = append(ps, ParsePattern(s, path)) + } + } + } + } + + var fis []billy.FileInfo + fis, err = fs.ReadDir(fs.Join(path...)) + if err != nil { + return + } + for _, fi := range fis { + if fi.IsDir() && fi.Name() != gitDir { + var subps []Pattern + subps, err = ReadPatterns(fs, append(path, fi.Name())) + if err != nil { + return + } + if len(subps) > 0 { + ps = append(ps, subps...) + } + } + } + + return +} diff --git a/plumbing/format/gitignore/dir_test.go b/plumbing/format/gitignore/dir_test.go new file mode 100644 index 0000000..61f3fc0 --- /dev/null +++ b/plumbing/format/gitignore/dir_test.go @@ -0,0 +1,41 @@ +package gitignore + +import ( + "os" + "testing" + + "gopkg.in/src-d/go-billy.v2" + "gopkg.in/src-d/go-billy.v2/memfs" +) + +func setupTestFS(subdirError bool) billy.Filesystem { + fs := memfs.New() + f, _ := fs.Create(".gitignore") + f.Write([]byte("vendor/g*/\n")) + f.Close() + fs.MkdirAll("vendor", os.ModePerm) + f, _ = fs.Create("vendor/.gitignore") + f.Write([]byte("!github.com/\n")) + f.Close() + fs.MkdirAll("another", os.ModePerm) + fs.MkdirAll("vendor/github.com", os.ModePerm) + fs.MkdirAll("vendor/gopkg.in", os.ModePerm) + return fs +} + +func TestDir_ReadPatterns(t *testing.T) { + ps, err := ReadPatterns(setupTestFS(false), nil) + if err != nil { + t.Errorf("no error expected, found %v", err) + } + if len(ps) != 2 { + t.Errorf("expected 2 patterns, found %v", len(ps)) + } + m := NewMatcher(ps) + if !m.Match([]string{"vendor", "gopkg.in"}, true) { + t.Error("expected a match") + } + if m.Match([]string{"vendor", "github.com"}, true) { + t.Error("expected no match") + } +} diff --git a/plumbing/format/gitignore/doc.go b/plumbing/format/gitignore/doc.go new file mode 100644 index 0000000..eecd4ba --- /dev/null +++ b/plumbing/format/gitignore/doc.go @@ -0,0 +1,70 @@ +// Package gitignore implements matching file system paths to gitignore patterns that +// can be automatically read from a git repository tree in the order of definition +// priorities. It support all pattern formats as specified in the original gitignore +// documentation, copied below: +// +// Pattern format +// ============== +// +// - A blank line matches no files, so it can serve as a separator for readability. +// +// - A line starting with # serves as a comment. Put a backslash ("\") in front of +// the first hash for patterns that begin with a hash. +// +// - Trailing spaces are ignored unless they are quoted with backslash ("\"). +// +// - 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". +// +// - 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). +// +// - 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). +// +// - 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". +// +// - A leading slash matches the beginning of the pathname. For example, +// "/*.c" matches "cat-file.c" but not "mozilla-sha1/sha1.c". +// +// Two consecutive asterisks ("**") in patterns matched against full pathname +// may have special meaning: +// +// - 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". +// +// - 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. +// +// - 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. +// +// - Other consecutive asterisks are considered invalid. +// +// Copyright and license +// ===================== +// +// Copyright (c) Oleg Sklyar, Silvertern and source{d} +// +// The package code was donated to source{d} to include, modify and develop +// further as a part of the `go-git` project, release it on the license of +// the whole project or delete it from the project. +package gitignore diff --git a/plumbing/format/gitignore/matcher.go b/plumbing/format/gitignore/matcher.go new file mode 100644 index 0000000..bd1e9e2 --- /dev/null +++ b/plumbing/format/gitignore/matcher.go @@ -0,0 +1,30 @@ +package gitignore + +// Matcher defines a global multi-pattern matcher for gitignore patterns +type Matcher interface { + // Match matches patterns in the order of priorities. As soon as an inclusion or + // exclusion is found, not further matching is performed. + Match(path []string, isDir bool) bool +} + +// NewMatcher constructs a new global matcher. Patterns must be given in the order of +// increasing priority. That is most generic settings files first, then the content of +// the repo .gitignore, then content of .gitignore down the path or the repo and then +// the content command line arguments. +func NewMatcher(ps []Pattern) Matcher { + return &matcher{ps} +} + +type matcher struct { + patterns []Pattern +} + +func (m *matcher) Match(path []string, isDir bool) bool { + n := len(m.patterns) + for i := n - 1; i >= 0; i-- { + if match := m.patterns[i].Match(path, isDir); match > NoMatch { + return match == Exclude + } + } + return false +} diff --git a/plumbing/format/gitignore/matcher_test.go b/plumbing/format/gitignore/matcher_test.go new file mode 100644 index 0000000..268e1c0 --- /dev/null +++ b/plumbing/format/gitignore/matcher_test.go @@ -0,0 +1,17 @@ +package gitignore + +import "testing" + +func TestMatcher_Match(t *testing.T) { + ps := []Pattern{ + ParsePattern("**/middle/v[uo]l?ano", nil), + ParsePattern("!volcano", nil), + } + m := NewMatcher(ps) + if !m.Match([]string{"head", "middle", "vulkano"}, false) { + t.Errorf("expected a match, found mismatch") + } + if m.Match([]string{"head", "middle", "volcano"}, false) { + t.Errorf("expected a mismatch, found a match") + } +} diff --git a/plumbing/format/gitignore/pattern.go b/plumbing/format/gitignore/pattern.go new file mode 100644 index 0000000..bd0825b --- /dev/null +++ b/plumbing/format/gitignore/pattern.go @@ -0,0 +1,150 @@ +package gitignore + +import ( + "path/filepath" + "strings" +) + +// MatchResult defines outcomes of a match, no match, exclusion or inclusion. +type MatchResult int + +const ( + // NoMatch defines the no match outcome of a match check + NoMatch MatchResult = iota + // Exclude defines an exclusion of a file as a result of a match check + Exclude + // Exclude defines an explicit inclusion of a file as a result of a match check + Include +) + +const ( + inclusionPrefix = "!" + zeroToManyDirs = "**" + patternDirSep = "/" +) + +// Pattern defines a single gitignore pattern. +type Pattern interface { + // Match matches the given path to the pattern. + Match(path []string, isDir bool) MatchResult +} + +type pattern struct { + domain []string + pattern []string + inclusion bool + dirOnly bool + isGlob bool +} + +// ParsePattern parses a gitignore pattern string into the Pattern structure. +func ParsePattern(p string, domain []string) Pattern { + res := pattern{domain: domain} + + if strings.HasPrefix(p, inclusionPrefix) { + res.inclusion = true + p = p[1:] + } + + if !strings.HasSuffix(p, "\\ ") { + p = strings.TrimRight(p, " ") + } + + if strings.HasSuffix(p, patternDirSep) { + res.dirOnly = true + p = p[:len(p)-1] + } + + if strings.Contains(p, patternDirSep) { + res.isGlob = true + } + + res.pattern = strings.Split(p, patternDirSep) + return &res +} + +func (p *pattern) Match(path []string, isDir bool) MatchResult { + if len(path) <= len(p.domain) { + return NoMatch + } + for i, e := range p.domain { + if path[i] != e { + return NoMatch + } + } + + path = path[len(p.domain):] + if p.isGlob && !p.globMatch(path, isDir) { + return NoMatch + } else if !p.isGlob && !p.simpleNameMatch(path, isDir) { + return NoMatch + } + + if p.inclusion { + return Include + } else { + return Exclude + } +} + +func (p *pattern) simpleNameMatch(path []string, isDir bool) bool { + for i, name := range path { + if match, err := filepath.Match(p.pattern[0], name); err != nil { + return false + } else if !match { + continue + } + if p.dirOnly && !isDir && i == len(path)-1 { + return false + } + return true + } + return false +} + +func (p *pattern) globMatch(path []string, isDir bool) bool { + matched := false + canTraverse := false + for i, pattern := range p.pattern { + if pattern == "" { + canTraverse = false + continue + } + if pattern == zeroToManyDirs { + if i == len(p.pattern)-1 { + break + } + canTraverse = true + continue + } + if strings.Contains(pattern, zeroToManyDirs) { + return false + } + if len(path) == 0 { + return false + } + if canTraverse { + canTraverse = false + for len(path) > 0 { + e := path[0] + path = path[1:] + if match, err := filepath.Match(pattern, e); err != nil { + return false + } else if match { + matched = true + break + } + } + } else { + if match, err := filepath.Match(pattern, path[0]); err != nil || !match { + return false + } + matched = true + path = path[1:] + } + } + if matched && p.dirOnly && !isDir && len(path) == 0 { + matched = false + } + return matched +} diff --git a/plumbing/format/gitignore/pattern_test.go b/plumbing/format/gitignore/pattern_test.go new file mode 100644 index 0000000..3abb012 --- /dev/null +++ b/plumbing/format/gitignore/pattern_test.go @@ -0,0 +1,318 @@ +package gitignore + +import "testing" + +func TestPatternSimpleMatch_inclusion(t *testing.T) { + p := ParsePattern("!vul?ano", nil) + if res := p.Match([]string{"value", "vulkano", "tail"}, false); res != Include { + t.Errorf("expected Exclude, found %v", res) + } +} + +func TestPatternMatch_domainLonger_mismatch(t *testing.T) { + p := ParsePattern("value", []string{"head", "middle", "tail"}) + if res := p.Match([]string{"head", "middle"}, false); res != NoMatch { + t.Errorf("expected NoMatch, found %v", res) + } +} + +func TestPatternMatch_domainSameLength_mismatch(t *testing.T) { + p := ParsePattern("value", []string{"head", "middle", "tail"}) + if res := p.Match([]string{"head", "middle", "tail"}, false); res != NoMatch { + t.Errorf("expected NoMatch, found %v", res) + } +} + +func TestPatternMatch_domainMismatch_mismatch(t *testing.T) { + p := ParsePattern("value", []string{"head", "middle", "tail"}) + if res := p.Match([]string{"head", "middle", "_tail_", "value"}, false); res != NoMatch { + t.Errorf("expected NoMatch, found %v", res) + } +} + +func TestPatternSimpleMatch_withDomain(t *testing.T) { + p := ParsePattern("middle/", []string{"value", "volcano"}) + if res := p.Match([]string{"value", "volcano", "middle", "tail"}, false); res != Exclude { + t.Errorf("expected Exclude, found %v", res) + } +} + +func TestPatternSimpleMatch_onlyMatchInDomain_mismatch(t *testing.T) { + p := ParsePattern("volcano/", []string{"value", "volcano"}) + if res := p.Match([]string{"value", "volcano", "tail"}, true); res != NoMatch { + t.Errorf("expected NoMatch, found %v", res) + } +} + +func TestPatternSimpleMatch_atStart(t *testing.T) { + p := ParsePattern("value", nil) + if res := p.Match([]string{"value", "tail"}, false); res != Exclude { + t.Errorf("expected Exclude, found %v", res) + } +} + +func TestPatternSimpleMatch_inTheMiddle(t *testing.T) { + p := ParsePattern("value", nil) + if res := p.Match([]string{"head", "value", "tail"}, false); res != Exclude { + t.Errorf("expected Exclude, found %v", res) + } +} + +func TestPatternSimpleMatch_atEnd(t *testing.T) { + p := ParsePattern("value", nil) + if res := p.Match([]string{"head", "value"}, false); res != Exclude { + t.Errorf("expected Exclude, found %v", res) + } +} + +func TestPatternSimpleMatch_atStart_dirWanted(t *testing.T) { + p := ParsePattern("value/", nil) + if res := p.Match([]string{"value", "tail"}, false); res != Exclude { + t.Errorf("expected Exclude, found %v", res) + } +} + +func TestPatternSimpleMatch_inTheMiddle_dirWanted(t *testing.T) { + p := ParsePattern("value/", nil) + if res := p.Match([]string{"head", "value", "tail"}, false); res != Exclude { + t.Errorf("expected Exclude, found %v", res) + } +} + +func TestPatternSimpleMatch_atEnd_dirWanted(t *testing.T) { + p := ParsePattern("value/", nil) + if res := p.Match([]string{"head", "value"}, true); res != Exclude { + t.Errorf("expected Exclude, found %v", res) + } +} + +func TestPatternSimpleMatch_atEnd_dirWanted_notADir_mismatch(t *testing.T) { + p := ParsePattern("value/", nil) + if res := p.Match([]string{"head", "value"}, false); res != NoMatch { + t.Errorf("expected NoMatch, found %v", res) + } +} + +func TestPatternSimpleMatch_mismatch(t *testing.T) { + p := ParsePattern("value", nil) + if res := p.Match([]string{"head", "val", "tail"}, false); res != NoMatch { + t.Errorf("expected NoMatch, found %v", res) + } +} + +func TestPatternSimpleMatch_valueLonger_mismatch(t *testing.T) { + p := ParsePattern("val", nil) + if res := p.Match([]string{"head", "value", "tail"}, false); res != NoMatch { + t.Errorf("expected NoMatch, found %v", res) + } +} + +func TestPatternSimpleMatch_withAsterisk(t *testing.T) { + p := ParsePattern("v*o", nil) + if res := p.Match([]string{"value", "vulkano", "tail"}, false); res != Exclude { + t.Errorf("expected Exclude, found %v", res) + } +} + +func TestPatternSimpleMatch_withQuestionMark(t *testing.T) { + p := ParsePattern("vul?ano", nil) + if res := p.Match([]string{"value", "vulkano", "tail"}, false); res != Exclude { + t.Errorf("expected Exclude, found %v", res) + } +} + +func TestPatternSimpleMatch_magicChars(t *testing.T) { + p := ParsePattern("v[ou]l[kc]ano", nil) + if res := p.Match([]string{"value", "volcano"}, false); res != Exclude { + t.Errorf("expected Exclude, found %v", res) + } +} + +func TestPatternSimpleMatch_wrongPattern_mismatch(t *testing.T) { + p := ParsePattern("v[ou]l[", nil) + if res := p.Match([]string{"value", "vol["}, false); res != NoMatch { + t.Errorf("expected NoMatch, found %v", res) + } +} + +func TestPatternGlobMatch_fromRootWithSlash(t *testing.T) { + p := ParsePattern("/value/vul?ano", nil) + if res := p.Match([]string{"value", "vulkano", "tail"}, false); res != Exclude { + t.Errorf("expected Exclude, found %v", res) + } +} + +func TestPatternGlobMatch_withDomain(t *testing.T) { + p := ParsePattern("middle/tail/", []string{"value", "volcano"}) + if res := p.Match([]string{"value", "volcano", "middle", "tail"}, true); res != Exclude { + t.Errorf("expected Exclude, found %v", res) + } +} + +func TestPatternGlobMatch_onlyMatchInDomain_mismatch(t *testing.T) { + p := ParsePattern("volcano/tail", []string{"value", "volcano"}) + if res := p.Match([]string{"value", "volcano", "tail"}, false); res != NoMatch { + t.Errorf("expected NoMatch, found %v", res) + } +} + +func TestPatternGlobMatch_fromRootWithoutSlash(t *testing.T) { + p := ParsePattern("value/vul?ano", nil) + if res := p.Match([]string{"value", "vulkano", "tail"}, false); res != Exclude { + t.Errorf("expected Exclude, found %v", res) + } +} + +func TestPatternGlobMatch_fromRoot_mismatch(t *testing.T) { + p := ParsePattern("value/vulkano", nil) + if res := p.Match([]string{"value", "volcano"}, false); res != NoMatch { + t.Errorf("expected NoMatch, found %v", res) + } +} + +func TestPatternGlobMatch_fromRoot_tooShort_mismatch(t *testing.T) { + p := ParsePattern("value/vul?ano", nil) + if res := p.Match([]string{"value"}, false); res != NoMatch { + t.Errorf("expected NoMatch, found %v", res) + } +} + +func TestPatternGlobMatch_fromRoot_notAtRoot_mismatch(t *testing.T) { + p := ParsePattern("/value/volcano", nil) + if res := p.Match([]string{"value", "value", "volcano"}, false); res != NoMatch { + t.Errorf("expected NoMatch, found %v", res) + } +} + +func TestPatternGlobMatch_leadingAsterisks_atStart(t *testing.T) { + p := ParsePattern("**/*lue/vol?ano", nil) + if res := p.Match([]string{"value", "volcano", "tail"}, false); res != Exclude { + t.Errorf("expected Exclude, found %v", res) + } +} + +func TestPatternGlobMatch_leadingAsterisks_notAtStart(t *testing.T) { + p := ParsePattern("**/*lue/vol?ano", nil) + if res := p.Match([]string{"head", "value", "volcano", "tail"}, false); res != Exclude { + t.Errorf("expected Exclude, found %v", res) + } +} + +func TestPatternGlobMatch_leadingAsterisks_mismatch(t *testing.T) { + p := ParsePattern("**/*lue/vol?ano", nil) + if res := p.Match([]string{"head", "value", "Volcano", "tail"}, false); res != NoMatch { + t.Errorf("expected NoMatch, found %v", res) + } +} + +func TestPatternGlobMatch_leadingAsterisks_isDir(t *testing.T) { + p := ParsePattern("**/*lue/vol?ano/", nil) + if res := p.Match([]string{"head", "value", "volcano", "tail"}, false); res != Exclude { + t.Errorf("expected Exclude, found %v", res) + } +} + +func TestPatternGlobMatch_leadingAsterisks_isDirAtEnd(t *testing.T) { + p := ParsePattern("**/*lue/vol?ano/", nil) + if res := p.Match([]string{"head", "value", "volcano"}, true); res != Exclude { + t.Errorf("expected Exclude, found %v", res) + } +} + +func TestPatternGlobMatch_leadingAsterisks_isDir_mismatch(t *testing.T) { + p := ParsePattern("**/*lue/vol?ano/", nil) + if res := p.Match([]string{"head", "value", "Colcano"}, true); res != NoMatch { + t.Errorf("expected NoMatch, found %v", res) + } +} + +func TestPatternGlobMatch_leadingAsterisks_isDirNoDirAtEnd_mismatch(t *testing.T) { + p := ParsePattern("**/*lue/vol?ano/", nil) + if res := p.Match([]string{"head", "value", "volcano"}, false); res != NoMatch { + t.Errorf("expected NoMatch, found %v", res) + } +} + +func TestPatternGlobMatch_tailingAsterisks(t *testing.T) { + p := ParsePattern("/*lue/vol?ano/**", nil) + if res := p.Match([]string{"value", "volcano", "tail", "moretail"}, false); res != Exclude { + t.Errorf("expected Exclude, found %v", res) + } +} + +func TestPatternGlobMatch_tailingAsterisks_exactMatch(t *testing.T) { + p := ParsePattern("/*lue/vol?ano/**", nil) + if res := p.Match([]string{"value", "volcano"}, false); res != Exclude { + t.Errorf("expected Exclude, found %v", res) + } +} + +func TestPatternGlobMatch_middleAsterisks_emptyMatch(t *testing.T) { + p := ParsePattern("/*lue/**/vol?ano", nil) + if res := p.Match([]string{"value", "volcano"}, false); res != Exclude { + t.Errorf("expected Exclude, found %v", res) + } +} + +func TestPatternGlobMatch_middleAsterisks_oneMatch(t *testing.T) { + p := ParsePattern("/*lue/**/vol?ano", nil) + if res := p.Match([]string{"value", "middle", "volcano"}, false); res != Exclude { + t.Errorf("expected Exclude, found %v", res) + } +} + +func TestPatternGlobMatch_middleAsterisks_multiMatch(t *testing.T) { + p := ParsePattern("/*lue/**/vol?ano", nil) + if res := p.Match([]string{"value", "middle1", "middle2", "volcano"}, false); res != Exclude { + t.Errorf("expected Exclude, found %v", res) + } +} + +func TestPatternGlobMatch_middleAsterisks_isDir_trailing(t *testing.T) { + p := ParsePattern("/*lue/**/vol?ano/", nil) + if res := p.Match([]string{"value", "middle1", "middle2", "volcano"}, true); res != Exclude { + t.Errorf("expected Exclude, found %v", res) + } +} + +func TestPatternGlobMatch_middleAsterisks_isDir_trailing_mismatch(t *testing.T) { + p := ParsePattern("/*lue/**/vol?ano/", nil) + if res := p.Match([]string{"value", "middle1", "middle2", "volcano"}, false); res != NoMatch { + t.Errorf("expected NoMatch, found %v", res) + } +} + +func TestPatternGlobMatch_middleAsterisks_isDir(t *testing.T) { + p := ParsePattern("/*lue/**/vol?ano/", nil) + if res := p.Match([]string{"value", "middle1", "middle2", "volcano", "tail"}, false); res != Exclude { + t.Errorf("expected Exclude, found %v", res) + } +} + +func TestPatternGlobMatch_wrongDoubleAsterisk_mismatch(t *testing.T) { + p := ParsePattern("/*lue/**foo/vol?ano", nil) + if res := p.Match([]string{"value", "foo", "volcano", "tail"}, false); res != NoMatch { + t.Errorf("expected NoMatch, found %v", res) + } +} + +func TestPatternGlobMatch_magicChars(t *testing.T) { + p := ParsePattern("**/head/v[ou]l[kc]ano", nil) + if res := p.Match([]string{"value", "head", "volcano"}, false); res != Exclude { + t.Errorf("expected Exclude, found %v", res) + } +} + +func TestPatternGlobMatch_wrongPattern_noTraversal_mismatch(t *testing.T) { + p := ParsePattern("**/head/v[ou]l[", nil) + if res := p.Match([]string{"value", "head", "vol["}, false); res != NoMatch { + t.Errorf("expected NoMatch, found %v", res) + } +} + +func TestPatternGlobMatch_wrongPattern_onTraversal_mismatch(t *testing.T) { + p := ParsePattern("/value/**/v[ou]l[", nil) + if res := p.Match([]string{"value", "head", "vol["}, false); res != NoMatch { + t.Errorf("expected NoMatch, found %v", res) + } +} -- cgit