diff --git a/files/file_walker.go b/files/file_walker.go index 072f2a9..87d7825 100644 --- a/files/file_walker.go +++ b/files/file_walker.go @@ -63,7 +63,7 @@ func WalkFolder( ) *File { var wg sync.WaitGroup c := make(chan bool, 2*runtime.NumCPU()) - root := walkSubFolderConcurrently(path, nil, ignoringReadDir(ignoreFunction, readDir), c, &wg, progress) + root := walkSubFolderConcurrently(path, nil, readDir, ignoreFunction, c, &wg, progress) wg.Wait() close(progress) root.UpdateSize() @@ -74,12 +74,14 @@ func walkSubFolderConcurrently( path string, parent *File, readDir ReadDir, + ignoreFunction ShouldIgnoreFolder, c chan bool, wg *sync.WaitGroup, progress chan<- int, ) *File { result := &File{} - entries, err := readDir(path) + readDir2 := ignoringReadDir(ignoreFunction, readDir) + entries, err := readDir2(path) if err != nil { log.Println(err) return result @@ -90,21 +92,22 @@ func walkSubFolderConcurrently( defer updateProgress(progress, &numSubFolders) var mutex sync.Mutex for _, entry := range entries { + fullPath := filepath.Join(path, entry.Name()) if entry.IsDir() { numSubFolders++ subFolderPath := filepath.Join(path, entry.Name()) wg.Add(1) go func() { c <- true - subFolder := walkSubFolderConcurrently(subFolderPath, result, readDir, c, wg, progress) + subFolder := walkSubFolderConcurrently(subFolderPath, result, readDir, ignoreFunction, c, wg, progress) mutex.Lock() result.Files = append(result.Files, subFolder) mutex.Unlock() <-c wg.Done() }() - } else { - size := entry.Size() + } else if !ignoreFunction(fullPath) { + size := entry.Size() file := &File{ entry.Name(), result, diff --git a/files/gitmatch.go b/files/gitmatch.go new file mode 100644 index 0000000..014add7 --- /dev/null +++ b/files/gitmatch.go @@ -0,0 +1,212 @@ +package files + +//this is essentially copied from filepath.Match but adjusted to support matching separators with ** + +import ( + "strings" + "runtime" + "os" + "errors" + "unicode/utf8" +) + +const ( + Separator = os.PathSeparator + ListSeparator = os.PathListSeparator +) + +var ErrBadPattern = errors.New("syntax error in pattern") + +// getEsc gets a possibly-escaped character from chunk, for a character class. +func getEsc(chunk string) (r rune, nchunk string, err error) { + if len(chunk) == 0 || chunk[0] == '-' || chunk[0] == ']' { + err = ErrBadPattern + return + } + if chunk[0] == '\\' && runtime.GOOS != "windows" { + chunk = chunk[1:] + if len(chunk) == 0 { + err = ErrBadPattern + return + } + } + r, n := utf8.DecodeRuneInString(chunk) + if r == utf8.RuneError && n == 1 { + err = ErrBadPattern + } + nchunk = chunk[n:] + if len(nchunk) == 0 { + err = ErrBadPattern + } + return +} + + +// matchChunk checks whether chunk matches the beginning of s. +// If so, it returns the remainder of s (after the match). +// Chunk is all single-character operators: literals, char classes, and ?. +func matchChunk(chunk, s string) (rest string, ok bool, err error) { + // failed records whether the match has failed. + // After the match fails, the loop continues on processing chunk, + // checking that the pattern is well-formed but no longer reading s. + failed := false + for len(chunk) > 0 { + if !failed && len(s) == 0 { + failed = true + } + switch chunk[0] { + case '[': + // character class + var r rune + if !failed { + var n int + r, n = utf8.DecodeRuneInString(s) + s = s[n:] + } + chunk = chunk[1:] + // possibly negated + negated := false + if len(chunk) > 0 && chunk[0] == '^' { + negated = true + chunk = chunk[1:] + } + // parse all ranges + match := false + nrange := 0 + for { + if len(chunk) > 0 && chunk[0] == ']' && nrange > 0 { + chunk = chunk[1:] + break + } + var lo, hi rune + if lo, chunk, err = getEsc(chunk); err != nil { + return "", false, err + } + hi = lo + if chunk[0] == '-' { + if hi, chunk, err = getEsc(chunk[1:]); err != nil { + return "", false, err + } + } + if lo <= r && r <= hi { + match = true + } + nrange++ + } + if match == negated { + failed = true + } + + case '?': + if !failed { + if s[0] == Separator { + failed = true + } + _, n := utf8.DecodeRuneInString(s) + s = s[n:] + } + chunk = chunk[1:] + + case '\\': + if runtime.GOOS != "windows" { + chunk = chunk[1:] + if len(chunk) == 0 { + return "", false, ErrBadPattern + } + } + fallthrough + + default: + if !failed { + if chunk[0] != s[0] { + failed = true + } + s = s[1:] + } + chunk = chunk[1:] + } + } + if failed { + return "", false, nil + } + return s, true, nil +} + +func scanChunk(pattern string) (star int, chunk, rest string) { + for len(pattern) > 0 && pattern[0] == '*' { + pattern = pattern[1:] + star += 1 + } + // for star >= 2 && pattern[0] == '/' { + // pattern = pattern[1:] + // } + inrange := false + var i int +Scan: + for i = 0; i < len(pattern); i++ { + switch pattern[i] { + case '\\': + if runtime.GOOS != "windows" { + // error check handled in matchChunk: bad pattern. + if i+1 < len(pattern) { + i++ + } + } + case '[': + inrange = true + case ']': + inrange = false + case '*': + if !inrange { + break Scan + } + } + } + return star, pattern[0:i], pattern[i:] +} + +func Match(pattern, name string) (matched bool, err error) { +Pattern: + for len(pattern) > 0 { + var star int + var chunk string + star, chunk, pattern = scanChunk(pattern) + if star >= 1 && chunk == "" { + // Trailing * matches rest of string unless it has a /. + return !strings.Contains(name, string(Separator)), nil + } + // Look for match at current position. + t, ok, err := matchChunk(chunk, name) + // if we're the last chunk, make sure we've exhausted the name + // otherwise we'll give a false result even if we could still match + // using the star + if ok && (len(t) == 0 || len(pattern) > 0) { + name = t + continue + } + if err != nil { + return false, err + } + if star >= 1 { + // Look for match skipping i+1 bytes. + // Cannot skip /. + for i := 0; i < len(name) && ((name[i] != Separator && star == 1) || star >= 2) ; i++ { + t, ok, err := matchChunk(chunk, name[i+1:]) + if ok { + // if we're the last chunk, make sure we exhausted the name + if len(pattern) == 0 && len(t) > 0 { + continue + } + name = t + continue Pattern + } + if err != nil { + return false, err + } + } + } + return false, nil + } + return len(name) == 0, nil +} + diff --git a/ignore.go b/ignore.go index b533551..29718a5 100644 --- a/ignore.go +++ b/ignore.go @@ -35,13 +35,13 @@ func readIgnoreFile() []string { } func ignoreBasedOnIgnoreFile(ignoreFile []string) files.ShouldIgnoreFolder { - ignoredFolders := map[string]struct{}{} - for _, line := range ignoreFile { - ignoredFolders[line] = struct{}{} - } return func(absolutePath string) bool { - _, name := filepath.Split(absolutePath) - _, ignored := ignoredFolders[name] - return ignored + for _, pattern := range ignoreFile { + matchResult, err := files.Match(pattern, absolutePath) + if matchResult && err == nil { + return true + } + } + return false } } diff --git a/ignore_test.go b/ignore_test.go index 674f4e7..9b64700 100644 --- a/ignore_test.go +++ b/ignore_test.go @@ -7,8 +7,37 @@ import ( ) func TestIgnoreBasedOnIgnoreFile(t *testing.T) { - ignored := []string{"node_modules"} - ignoreFunction := ignoreBasedOnIgnoreFile(ignored) - assert.True(t, ignoreFunction("something/node_modules")) - assert.False(t, ignoreFunction("something/notIgnored")) + //these tests might fail on windows + tests := []struct { + ignored []string + file string + match bool + }{ + {[]string{"**/what/test"}, "what/what/test", true}, + {[]string{"**node_modules"}, "something/node_modules", true}, + {[]string{"**node_modules"}, "something/notIgnored", false}, + {[]string{"**/what/test"}, "/home/user/test", false}, + {[]string{"**user/test"}, "user/test", true}, + {[]string{"**test"}, "test", true}, + {[]string{"**png"}, "path/to/test.png", true}, + {[]string{"**folder**png"}, "path/to/folder/test.png", true}, + {[]string{"**folder**png"}, "path/folder/to/test.png", true}, + {[]string{"**/*folder*/**png"}, "path/hellofolder/to/test.png", true}, + {[]string{"**/*folder*/**png"}, "path/folder2/to/test.png", true}, + {[]string{"**/*folder*/**png"}, "path/folder/to/test.png", true}, + {[]string{"**/*folder*/**png"}, "path/foler/to/test.png", false}, + {[]string{"**/test.png"}, "path/to/test.png", true}, + {[]string{"*png"}, "test.png", true}, + {[]string{"dir/*png"}, "dir/test.png", true}, + {[]string{"*dir*/*png"}, "testdir2/test.png", true}, + {[]string{"*dir*/*png"}, "testdir/test.png", true}, + {[]string{"*dir*dir*/*png"}, "dirtestdir/test.png", true}, + {[]string{"*dir*dir*/*png"}, "dir/testdir/test.png", false}, + {[]string{"wrong/pattern"}, "test", false}, + } + + for _, tt := range tests { + ignoreFunction := ignoreBasedOnIgnoreFile(tt.ignored) + assert.True(t, tt.match == ignoreFunction(tt.file)) + } }