1934b3a5b6
Actual speed difference according to benchmarks is hidden in the noise Also make the "pattern" field for each entry match what is actually evaluated.
319 lines
6.5 KiB
Go
319 lines
6.5 KiB
Go
// Copyright (C) 2014 The Syncthing Authors.
|
|
//
|
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
|
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
|
|
package ignore
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"crypto/md5"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gobwas/glob"
|
|
"github.com/syncthing/syncthing/lib/sync"
|
|
)
|
|
|
|
type Pattern struct {
|
|
pattern string
|
|
match glob.Glob
|
|
include bool
|
|
foldCase bool
|
|
}
|
|
|
|
func (p Pattern) String() string {
|
|
ret := p.pattern
|
|
if !p.include {
|
|
ret = "!" + ret
|
|
}
|
|
if p.foldCase {
|
|
ret = "(?i)" + ret
|
|
}
|
|
return ret
|
|
}
|
|
|
|
type Matcher struct {
|
|
patterns []Pattern
|
|
withCache bool
|
|
matches *cache
|
|
curHash string
|
|
stop chan struct{}
|
|
mut sync.Mutex
|
|
}
|
|
|
|
func New(withCache bool) *Matcher {
|
|
m := &Matcher{
|
|
withCache: withCache,
|
|
stop: make(chan struct{}),
|
|
mut: sync.NewMutex(),
|
|
}
|
|
if withCache {
|
|
go m.clean(2 * time.Hour)
|
|
}
|
|
return m
|
|
}
|
|
|
|
func (m *Matcher) Load(file string) error {
|
|
// No locking, Parse() does the locking
|
|
|
|
fd, err := os.Open(file)
|
|
if err != nil {
|
|
// We do a parse with empty patterns to clear out the hash, cache etc.
|
|
m.Parse(&bytes.Buffer{}, file)
|
|
return err
|
|
}
|
|
defer fd.Close()
|
|
|
|
return m.Parse(fd, file)
|
|
}
|
|
|
|
func (m *Matcher) Parse(r io.Reader, file string) error {
|
|
m.mut.Lock()
|
|
defer m.mut.Unlock()
|
|
|
|
seen := map[string]bool{file: true}
|
|
patterns, err := parseIgnoreFile(r, file, seen)
|
|
// Error is saved and returned at the end. We process the patterns
|
|
// (possibly blank) anyway.
|
|
|
|
newHash := hashPatterns(patterns)
|
|
if newHash == m.curHash {
|
|
// We've already loaded exactly these patterns.
|
|
return err
|
|
}
|
|
|
|
m.curHash = newHash
|
|
m.patterns = patterns
|
|
if m.withCache {
|
|
m.matches = newCache(patterns)
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
func (m *Matcher) Match(file string) (result bool) {
|
|
if m == nil {
|
|
return false
|
|
}
|
|
|
|
m.mut.Lock()
|
|
defer m.mut.Unlock()
|
|
|
|
if len(m.patterns) == 0 {
|
|
return false
|
|
}
|
|
|
|
if m.matches != nil {
|
|
// Check the cache for a known result.
|
|
res, ok := m.matches.get(file)
|
|
if ok {
|
|
return res
|
|
}
|
|
|
|
// Update the cache with the result at return time
|
|
defer func() {
|
|
m.matches.set(file, result)
|
|
}()
|
|
}
|
|
|
|
// Check all the patterns for a match.
|
|
file = filepath.ToSlash(file)
|
|
var lowercaseFile string
|
|
for _, pattern := range m.patterns {
|
|
if pattern.foldCase {
|
|
if lowercaseFile == "" {
|
|
lowercaseFile = strings.ToLower(file)
|
|
}
|
|
if pattern.match.Match(lowercaseFile) {
|
|
return pattern.include
|
|
}
|
|
} else {
|
|
if pattern.match.Match(file) {
|
|
return pattern.include
|
|
}
|
|
}
|
|
}
|
|
|
|
// Default to false.
|
|
return false
|
|
}
|
|
|
|
// Patterns return a list of the loaded patterns, as they've been parsed
|
|
func (m *Matcher) Patterns() []string {
|
|
if m == nil {
|
|
return nil
|
|
}
|
|
|
|
m.mut.Lock()
|
|
defer m.mut.Unlock()
|
|
|
|
patterns := make([]string, len(m.patterns))
|
|
for i, pat := range m.patterns {
|
|
patterns[i] = pat.String()
|
|
}
|
|
return patterns
|
|
}
|
|
|
|
func (m *Matcher) Hash() string {
|
|
m.mut.Lock()
|
|
defer m.mut.Unlock()
|
|
return m.curHash
|
|
}
|
|
|
|
func (m *Matcher) Stop() {
|
|
close(m.stop)
|
|
}
|
|
|
|
func (m *Matcher) clean(d time.Duration) {
|
|
t := time.NewTimer(d / 2)
|
|
for {
|
|
select {
|
|
case <-m.stop:
|
|
return
|
|
case <-t.C:
|
|
m.mut.Lock()
|
|
if m.matches != nil {
|
|
m.matches.clean(d)
|
|
}
|
|
t.Reset(d / 2)
|
|
m.mut.Unlock()
|
|
}
|
|
}
|
|
}
|
|
|
|
func hashPatterns(patterns []Pattern) string {
|
|
h := md5.New()
|
|
for _, pat := range patterns {
|
|
h.Write([]byte(pat.String()))
|
|
h.Write([]byte("\n"))
|
|
}
|
|
return fmt.Sprintf("%x", h.Sum(nil))
|
|
}
|
|
|
|
func loadIgnoreFile(file string, seen map[string]bool) ([]Pattern, error) {
|
|
if seen[file] {
|
|
return nil, fmt.Errorf("Multiple include of ignore file %q", file)
|
|
}
|
|
seen[file] = true
|
|
|
|
fd, err := os.Open(file)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer fd.Close()
|
|
|
|
return parseIgnoreFile(fd, file, seen)
|
|
}
|
|
|
|
func parseIgnoreFile(fd io.Reader, currentFile string, seen map[string]bool) ([]Pattern, error) {
|
|
var patterns []Pattern
|
|
|
|
addPattern := func(line string) error {
|
|
pattern := Pattern{
|
|
pattern: line,
|
|
include: true,
|
|
foldCase: runtime.GOOS == "darwin" || runtime.GOOS == "windows",
|
|
}
|
|
|
|
if strings.HasPrefix(line, "!") {
|
|
line = line[1:]
|
|
pattern.include = false
|
|
}
|
|
|
|
if strings.HasPrefix(line, "(?i)") {
|
|
line = strings.ToLower(line[4:])
|
|
pattern.foldCase = true
|
|
}
|
|
|
|
var err error
|
|
if strings.HasPrefix(line, "/") {
|
|
// Pattern is rooted in the current dir only
|
|
pattern.match, err = glob.Compile(line[1:])
|
|
if err != nil {
|
|
return fmt.Errorf("invalid pattern %q in ignore file", line)
|
|
}
|
|
patterns = append(patterns, pattern)
|
|
} else if strings.HasPrefix(line, "**/") {
|
|
// Add the pattern as is, and without **/ so it matches in current dir
|
|
pattern.match, err = glob.Compile(line)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid pattern %q in ignore file", line)
|
|
}
|
|
patterns = append(patterns, pattern)
|
|
|
|
line = line[3:]
|
|
pattern.pattern = line
|
|
pattern.match, err = glob.Compile(line)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid pattern %q in ignore file", line)
|
|
}
|
|
patterns = append(patterns, pattern)
|
|
} else if strings.HasPrefix(line, "#include ") {
|
|
includeRel := line[len("#include "):]
|
|
includeFile := filepath.Join(filepath.Dir(currentFile), includeRel)
|
|
includes, err := loadIgnoreFile(includeFile, seen)
|
|
if err != nil {
|
|
return fmt.Errorf("include of %q: %v", includeRel, err)
|
|
}
|
|
patterns = append(patterns, includes...)
|
|
} else {
|
|
// Path name or pattern, add it so it matches files both in
|
|
// current directory and subdirs.
|
|
pattern.match, err = glob.Compile(line)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid pattern %q in ignore file", line)
|
|
}
|
|
patterns = append(patterns, pattern)
|
|
|
|
line := "**/" + line
|
|
pattern.pattern = line
|
|
pattern.match, err = glob.Compile(line)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid pattern %q in ignore file", line)
|
|
}
|
|
patterns = append(patterns, pattern)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
scanner := bufio.NewScanner(fd)
|
|
var err error
|
|
for scanner.Scan() {
|
|
line := strings.TrimSpace(scanner.Text())
|
|
switch {
|
|
case line == "":
|
|
continue
|
|
case strings.HasPrefix(line, "//"):
|
|
continue
|
|
}
|
|
|
|
line = filepath.ToSlash(line)
|
|
switch {
|
|
case strings.HasPrefix(line, "#"):
|
|
err = addPattern(line)
|
|
case strings.HasSuffix(line, "/**"):
|
|
err = addPattern(line)
|
|
case strings.HasSuffix(line, "/"):
|
|
err = addPattern(line)
|
|
default:
|
|
err = addPattern(line)
|
|
if err == nil {
|
|
err = addPattern(line + "/**")
|
|
}
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return patterns, nil
|
|
}
|