aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorOleg Sklyar <osklyar@gmx.com>2017-06-19 00:26:14 +0200
committerOleg Sklyar <osklyar@gmx.com>2017-06-19 00:26:14 +0200
commit2f4ac21bad4c14b860a7d5c9d761857cb8d4f89c (patch)
tree869b08e65c35bf80dfd15f665d100e5ca3539917
parent2a00316b65585be2bf68e1ea9c0e42c6af4f5679 (diff)
downloadgo-git-2f4ac21bad4c14b860a7d5c9d761857cb8d4f89c.tar.gz
Adds gitignore support
-rw-r--r--plumbing/format/gitignore/dir.go50
-rw-r--r--plumbing/format/gitignore/dir_test.go41
-rw-r--r--plumbing/format/gitignore/doc.go70
-rw-r--r--plumbing/format/gitignore/matcher.go30
-rw-r--r--plumbing/format/gitignore/matcher_test.go17
-rw-r--r--plumbing/format/gitignore/pattern.go150
-rw-r--r--plumbing/format/gitignore/pattern_test.go318
-rw-r--r--worktree_status.go32
-rw-r--r--worktree_test.go50
9 files changed, 758 insertions, 0 deletions
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)
+ }
+}
diff --git a/worktree_status.go b/worktree_status.go
index eb4a83a..7cc4b0f 100644
--- a/worktree_status.go
+++ b/worktree_status.go
@@ -8,6 +8,7 @@ import (
"gopkg.in/src-d/go-git.v4/plumbing"
"gopkg.in/src-d/go-git.v4/plumbing/filemode"
+ "gopkg.in/src-d/go-git.v4/plumbing/format/gitignore"
"gopkg.in/src-d/go-git.v4/plumbing/format/index"
"gopkg.in/src-d/go-git.v4/plumbing/object"
"gopkg.in/src-d/go-git.v4/utils/ioutil"
@@ -67,6 +68,8 @@ func (w *Worktree) status(commit plumbing.Hash) (Status, error) {
return nil, err
}
+ right = w.excludeIgnoredChanges(right)
+
for _, ch := range right {
a, err := ch.Action()
if err != nil {
@@ -117,6 +120,35 @@ func (w *Worktree) diffStagingWithWorktree() (merkletrie.Changes, error) {
return merkletrie.DiffTree(from, to, diffTreeIsEquals)
}
+func (w *Worktree) excludeIgnoredChanges(changes merkletrie.Changes) merkletrie.Changes {
+ patterns, err := gitignore.ReadPatterns(w.fs, nil)
+ if err != nil || len(patterns) == 0 {
+ return changes
+ }
+ m := gitignore.NewMatcher(patterns)
+
+ var res merkletrie.Changes
+ for _, ch := range changes {
+ var path []string
+ for _, n := range ch.To {
+ path = append(path, n.Name())
+ }
+ if len(path) == 0 {
+ for _, n := range ch.From {
+ path = append(path, n.Name())
+ }
+ }
+ if len(path) != 0 {
+ isDir := (len(ch.To) > 0 && ch.To.IsDir()) || (len(ch.From) > 0 && ch.From.IsDir())
+ if m.Match(path, isDir) {
+ continue
+ }
+ }
+ res = append(res, ch)
+ }
+ return res
+}
+
func (w *Worktree) getSubmodulesStatus() (map[string]plumbing.Hash, error) {
o := map[string]plumbing.Hash{}
diff --git a/worktree_test.go b/worktree_test.go
index 6ca2ed0..56c1d9a 100644
--- a/worktree_test.go
+++ b/worktree_test.go
@@ -428,6 +428,56 @@ func (s *WorktreeSuite) TestStatusModified(c *C) {
c.Assert(status.File(".gitignore").Worktree, Equals, Modified)
}
+func (s *WorktreeSuite) TestStatusIgnored(c *C) {
+ dir, _ := ioutil.TempDir("", "status")
+ defer os.RemoveAll(dir)
+
+ fs := osfs.New(filepath.Join(dir, "worktree"))
+ w := &Worktree{
+ r: s.Repository,
+ fs: fs,
+ }
+
+ w.Checkout(&CheckoutOptions{})
+
+ fs.MkdirAll("another", os.ModePerm)
+ f, _ := fs.Create("another/file")
+ f.Close()
+ fs.MkdirAll("vendor/github.com", os.ModePerm)
+ f, _ = fs.Create("vendor/github.com/file")
+ f.Close()
+ fs.MkdirAll("vendor/gopkg.in", os.ModePerm)
+ f, _ = fs.Create("vendor/gopkg.in/file")
+ f.Close()
+
+ status, _ := w.Status()
+ c.Assert(len(status), Equals, 3)
+ _, ok := status["another/file"]
+ c.Assert(ok, Equals, true)
+ _, ok = status["vendor/github.com/file"]
+ c.Assert(ok, Equals, true)
+ _, ok = status["vendor/gopkg.in/file"]
+ c.Assert(ok, Equals, true)
+
+ f, _ = fs.Create(".gitignore")
+ f.Write([]byte("vendor/g*/"))
+ f.Close()
+ f, _ = fs.Create("vendor/.gitignore")
+ f.Write([]byte("!github.com/\n"))
+ f.Close()
+
+ status, _ = w.Status()
+ c.Assert(len(status), Equals, 4)
+ _, ok = status[".gitignore"]
+ c.Assert(ok, Equals, true)
+ _, ok = status["another/file"]
+ c.Assert(ok, Equals, true)
+ _, ok = status["vendor/.gitignore"]
+ c.Assert(ok, Equals, true)
+ _, ok = status["vendor/github.com/file"]
+ c.Assert(ok, Equals, true)
+}
+
func (s *WorktreeSuite) TestStatusUntracked(c *C) {
fs := memfs.New()
w := &Worktree{