ignore.go 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  1. // Copyright (C) 2014 The Syncthing Authors.
  2. //
  3. // This program is free software: you can redistribute it and/or modify it
  4. // under the terms of the GNU General Public License as published by the Free
  5. // Software Foundation, either version 3 of the License, or (at your option)
  6. // any later version.
  7. //
  8. // This program is distributed in the hope that it will be useful, but WITHOUT
  9. // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  10. // FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
  11. // more details.
  12. //
  13. // You should have received a copy of the GNU General Public License along
  14. // with this program. If not, see <http://www.gnu.org/licenses/>.
  15. package ignore
  16. import (
  17. "bufio"
  18. "fmt"
  19. "io"
  20. "os"
  21. "path/filepath"
  22. "regexp"
  23. "strings"
  24. "sync"
  25. "github.com/syncthing/syncthing/internal/fnmatch"
  26. )
  27. var caches = make(map[string]MatcherCache)
  28. type Pattern struct {
  29. match *regexp.Regexp
  30. include bool
  31. }
  32. type Matcher struct {
  33. patterns []Pattern
  34. oldMatches map[string]bool
  35. newMatches map[string]bool
  36. mut sync.Mutex
  37. }
  38. type MatcherCache struct {
  39. patterns []Pattern
  40. matches *map[string]bool
  41. }
  42. func Load(file string, cache bool) (*Matcher, error) {
  43. seen := make(map[string]bool)
  44. matcher, err := loadIgnoreFile(file, seen)
  45. if !cache || err != nil {
  46. return matcher, err
  47. }
  48. // Get the current cache object for the given file
  49. cached, ok := caches[file]
  50. if !ok || !patternsEqual(cached.patterns, matcher.patterns) {
  51. // Nothing in cache or a cache mismatch, create a new cache which will
  52. // store matches for the given set of patterns.
  53. // Initialize oldMatches to indicate that we are interested in
  54. // caching.
  55. matcher.oldMatches = make(map[string]bool)
  56. matcher.newMatches = make(map[string]bool)
  57. caches[file] = MatcherCache{
  58. patterns: matcher.patterns,
  59. matches: &matcher.newMatches,
  60. }
  61. return matcher, nil
  62. }
  63. // Patterns haven't changed, so we can reuse the old matches, create a new
  64. // matches map and update the pointer. (This prevents matches map from
  65. // growing indefinately, as we only cache whatever we've matched in the last
  66. // iteration, rather than through runtime history)
  67. matcher.oldMatches = *cached.matches
  68. matcher.newMatches = make(map[string]bool)
  69. cached.matches = &matcher.newMatches
  70. caches[file] = cached
  71. return matcher, nil
  72. }
  73. func Parse(r io.Reader, file string) (*Matcher, error) {
  74. seen := map[string]bool{
  75. file: true,
  76. }
  77. return parseIgnoreFile(r, file, seen)
  78. }
  79. func (m *Matcher) Match(file string) (result bool) {
  80. if len(m.patterns) == 0 {
  81. return false
  82. }
  83. // We have old matches map set, means we should do caching
  84. if m.oldMatches != nil {
  85. // Capture the result to the new matches regardless of who returns it
  86. defer func() {
  87. m.mut.Lock()
  88. m.newMatches[file] = result
  89. m.mut.Unlock()
  90. }()
  91. // Check perhaps we've seen this file before, and we already know
  92. // what the outcome is going to be.
  93. result, ok := m.oldMatches[file]
  94. if ok {
  95. return result
  96. }
  97. }
  98. for _, pattern := range m.patterns {
  99. if pattern.match.MatchString(file) {
  100. return pattern.include
  101. }
  102. }
  103. return false
  104. }
  105. // Patterns return a list of the loaded regexp patterns, as strings
  106. func (m *Matcher) Patterns() []string {
  107. patterns := make([]string, len(m.patterns))
  108. for i, pat := range m.patterns {
  109. if pat.include {
  110. patterns[i] = pat.match.String()
  111. } else {
  112. patterns[i] = "(?exclude)" + pat.match.String()
  113. }
  114. }
  115. return patterns
  116. }
  117. func loadIgnoreFile(file string, seen map[string]bool) (*Matcher, error) {
  118. if seen[file] {
  119. return nil, fmt.Errorf("Multiple include of ignore file %q", file)
  120. }
  121. seen[file] = true
  122. fd, err := os.Open(file)
  123. if err != nil {
  124. return nil, err
  125. }
  126. defer fd.Close()
  127. return parseIgnoreFile(fd, file, seen)
  128. }
  129. func parseIgnoreFile(fd io.Reader, currentFile string, seen map[string]bool) (*Matcher, error) {
  130. var exps Matcher
  131. addPattern := func(line string) error {
  132. include := true
  133. if strings.HasPrefix(line, "!") {
  134. line = line[1:]
  135. include = false
  136. }
  137. if strings.HasPrefix(line, "/") {
  138. // Pattern is rooted in the current dir only
  139. exp, err := fnmatch.Convert(line[1:], fnmatch.FNM_PATHNAME)
  140. if err != nil {
  141. return fmt.Errorf("Invalid pattern %q in ignore file", line)
  142. }
  143. exps.patterns = append(exps.patterns, Pattern{exp, include})
  144. } else if strings.HasPrefix(line, "**/") {
  145. // Add the pattern as is, and without **/ so it matches in current dir
  146. exp, err := fnmatch.Convert(line, fnmatch.FNM_PATHNAME)
  147. if err != nil {
  148. return fmt.Errorf("Invalid pattern %q in ignore file", line)
  149. }
  150. exps.patterns = append(exps.patterns, Pattern{exp, include})
  151. exp, err = fnmatch.Convert(line[3:], fnmatch.FNM_PATHNAME)
  152. if err != nil {
  153. return fmt.Errorf("Invalid pattern %q in ignore file", line)
  154. }
  155. exps.patterns = append(exps.patterns, Pattern{exp, include})
  156. } else if strings.HasPrefix(line, "#include ") {
  157. includeFile := filepath.Join(filepath.Dir(currentFile), line[len("#include "):])
  158. includes, err := loadIgnoreFile(includeFile, seen)
  159. if err != nil {
  160. return err
  161. } else {
  162. exps.patterns = append(exps.patterns, includes.patterns...)
  163. }
  164. } else {
  165. // Path name or pattern, add it so it matches files both in
  166. // current directory and subdirs.
  167. exp, err := fnmatch.Convert(line, fnmatch.FNM_PATHNAME)
  168. if err != nil {
  169. return fmt.Errorf("Invalid pattern %q in ignore file", line)
  170. }
  171. exps.patterns = append(exps.patterns, Pattern{exp, include})
  172. exp, err = fnmatch.Convert("**/"+line, fnmatch.FNM_PATHNAME)
  173. if err != nil {
  174. return fmt.Errorf("Invalid pattern %q in ignore file", line)
  175. }
  176. exps.patterns = append(exps.patterns, Pattern{exp, include})
  177. }
  178. return nil
  179. }
  180. scanner := bufio.NewScanner(fd)
  181. var err error
  182. for scanner.Scan() {
  183. line := strings.TrimSpace(scanner.Text())
  184. switch {
  185. case line == "":
  186. continue
  187. case strings.HasPrefix(line, "//"):
  188. continue
  189. case strings.HasPrefix(line, "#"):
  190. err = addPattern(line)
  191. case strings.HasSuffix(line, "/**"):
  192. err = addPattern(line)
  193. case strings.HasSuffix(line, "/"):
  194. err = addPattern(line)
  195. if err == nil {
  196. err = addPattern(line + "**")
  197. }
  198. default:
  199. err = addPattern(line)
  200. if err == nil {
  201. err = addPattern(line + "/**")
  202. }
  203. }
  204. if err != nil {
  205. return nil, err
  206. }
  207. }
  208. return &exps, nil
  209. }
  210. func patternsEqual(a, b []Pattern) bool {
  211. if len(a) != len(b) {
  212. return false
  213. }
  214. for i := range a {
  215. if a[i].include != b[i].include || a[i].match.String() != b[i].match.String() {
  216. return false
  217. }
  218. }
  219. return true
  220. }