diff --git a/lib/ignore/ignore.go b/lib/ignore/ignore.go index 31f70e74..b9670387 100644 --- a/lib/ignore/ignore.go +++ b/lib/ignore/ignore.go @@ -69,6 +69,7 @@ type Matcher struct { matches *cache curHash string stop chan struct{} + modtimes map[string]time.Time mut sync.Mutex } @@ -85,25 +86,41 @@ func New(withCache bool) *Matcher { } func (m *Matcher) Load(file string) error { - // No locking, Parse() does the locking + m.mut.Lock() + defer m.mut.Unlock() + + if m.patternsUnchanged(file) { + return nil + } 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) + m.parseLocked(&bytes.Buffer{}, file) return err } defer fd.Close() - return m.Parse(fd, file) + info, err := fd.Stat() + if err != nil { + m.parseLocked(&bytes.Buffer{}, file) + return err + } + + m.modtimes = map[string]time.Time{ + file: info.ModTime(), + } + + return m.parseLocked(fd, file) } func (m *Matcher) Parse(r io.Reader, file string) error { m.mut.Lock() defer m.mut.Unlock() + return m.parseLocked(r, file) +} - seen := map[string]bool{file: true} - patterns, err := parseIgnoreFile(r, file, seen) +func (m *Matcher) parseLocked(r io.Reader, file string) error { + patterns, err := parseIgnoreFile(r, file, m.modtimes) // Error is saved and returned at the end. We process the patterns // (possibly blank) anyway. @@ -122,6 +139,26 @@ func (m *Matcher) Parse(r io.Reader, file string) error { return err } +// patternsUnchanged returns true if none of the files making up the loaded +// patterns have changed since last check. +func (m *Matcher) patternsUnchanged(file string) bool { + if _, ok := m.modtimes[file]; !ok { + return false + } + + for filename, modtime := range m.modtimes { + info, err := os.Stat(filename) + if err != nil { + return false + } + if !info.ModTime().Equal(modtime) { + return false + } + } + + return true +} + func (m *Matcher) Match(file string) (result Result) { if m == nil { return resultNotMatched @@ -221,11 +258,10 @@ func hashPatterns(patterns []Pattern) string { return fmt.Sprintf("%x", h.Sum(nil)) } -func loadIgnoreFile(file string, seen map[string]bool) ([]Pattern, error) { - if seen[file] { +func loadIgnoreFile(file string, modtimes map[string]time.Time) ([]Pattern, error) { + if _, ok := modtimes[file]; ok { return nil, fmt.Errorf("Multiple include of ignore file %q", file) } - seen[file] = true fd, err := os.Open(file) if err != nil { @@ -233,10 +269,16 @@ func loadIgnoreFile(file string, seen map[string]bool) ([]Pattern, error) { } defer fd.Close() - return parseIgnoreFile(fd, file, seen) + info, err := fd.Stat() + if err != nil { + return nil, err + } + modtimes[file] = info.ModTime() + + return parseIgnoreFile(fd, file, modtimes) } -func parseIgnoreFile(fd io.Reader, currentFile string, seen map[string]bool) ([]Pattern, error) { +func parseIgnoreFile(fd io.Reader, currentFile string, modtimes map[string]time.Time) ([]Pattern, error) { var patterns []Pattern defaultResult := resultInclude @@ -302,7 +344,7 @@ func parseIgnoreFile(fd io.Reader, currentFile string, seen map[string]bool) ([] } else if strings.HasPrefix(line, "#include ") { includeRel := line[len("#include "):] includeFile := filepath.Join(filepath.Dir(currentFile), includeRel) - includes, err := loadIgnoreFile(includeFile, seen) + includes, err := loadIgnoreFile(includeFile, modtimes) if err != nil { return fmt.Errorf("include of %q: %v", includeRel, err) } diff --git a/lib/ignore/ignore_test.go b/lib/ignore/ignore_test.go index f14bac71..98a59432 100644 --- a/lib/ignore/ignore_test.go +++ b/lib/ignore/ignore_test.go @@ -14,6 +14,7 @@ import ( "path/filepath" "runtime" "testing" + "time" ) func TestIgnore(t *testing.T) { @@ -276,9 +277,12 @@ func TestCaching(t *testing.T) { t.Fatal("Expected 4 cached results") } - // Modify the include file, expect empty cache + // Modify the include file, expect empty cache. Ensure the timestamp on + // the file changes. + time.Sleep(time.Second) fd2.WriteString("/z/\n") + fd2.Sync() err = pats.Load(fd1.Name()) if err != nil { @@ -307,7 +311,9 @@ func TestCaching(t *testing.T) { // Modify the root file, expect cache to be invalidated + time.Sleep(time.Second) fd1.WriteString("/a/\n") + fd1.Sync() err = pats.Load(fd1.Name()) if err != nil { @@ -472,6 +478,8 @@ func TestCacheReload(t *testing.T) { // Rewrite file to match f1 and f3 + time.Sleep(time.Second) + err = fd.Truncate(0) if err != nil { t.Fatal(err)