1package gitattributes
2
3import (
4 "path/filepath"
5 "strings"
6)
7
8const (
9 patternDirSep = "/"
10 zeroToManyDirs = "**"
11)
12
13// Pattern defines a gitattributes pattern.
14type Pattern interface {
15 // Match matches the given path to the pattern.
16 Match(path []string) bool
17}
18
19type pattern struct {
20 domain []string
21 pattern []string
22}
23
24// ParsePattern parses a gitattributes pattern string into the Pattern
25// structure.
26func ParsePattern(p string, domain []string) Pattern {
27 return &pattern{
28 domain: domain,
29 pattern: strings.Split(p, patternDirSep),
30 }
31}
32
33func (p *pattern) Match(path []string) bool {
34 if len(path) <= len(p.domain) {
35 return false
36 }
37 for i, e := range p.domain {
38 if path[i] != e {
39 return false
40 }
41 }
42
43 if len(p.pattern) == 1 {
44 // for a simple rule, .gitattribute matching rules differs from
45 // .gitignore and only the last part of the path is considered.
46 path = path[len(path)-1:]
47 } else {
48 path = path[len(p.domain):]
49 }
50
51 pattern := p.pattern
52 var match, doublestar bool
53 var err error
54 for _, part := range path {
55 // path is deeper than pattern
56 if len(pattern) == 0 {
57 return false
58 }
59
60 // skip empty
61 if pattern[0] == "" {
62 pattern = pattern[1:]
63 }
64
65 // eat doublestar
66 if pattern[0] == zeroToManyDirs {
67 pattern = pattern[1:]
68 if len(pattern) == 0 {
69 return true
70 }
71 doublestar = true
72 }
73
74 switch {
75 case strings.Contains(pattern[0], "**"):
76 return false
77
78 // keep going down the path until we hit a match
79 case doublestar:
80 match, err = filepath.Match(pattern[0], part)
81 if err != nil {
82 return false
83 }
84
85 if match {
86 doublestar = false
87 pattern = pattern[1:]
88 }
89
90 default:
91 match, err = filepath.Match(pattern[0], part)
92 if err != nil {
93 return false
94 }
95 if !match {
96 return false
97 }
98 pattern = pattern[1:]
99 }
100 }
101
102 if len(pattern) > 0 {
103 return false
104 }
105 return match
106}