1package gitattributes
2
3import (
4 "bufio"
5 "errors"
6 "io"
7 "strings"
8)
9
10const (
11 commentPrefix = "#"
12 eol = "\n"
13 macroPrefix = "[attr]"
14)
15
16var (
17 ErrMacroNotAllowed = errors.New("macro not allowed")
18 ErrInvalidAttributeName = errors.New("invalid attribute name")
19)
20
21type MatchAttribute struct {
22 Name string
23 Pattern Pattern
24 Attributes []Attribute
25}
26
27type attributeState byte
28
29const (
30 attributeUnknown attributeState = 0
31 attributeSet attributeState = 1
32 attributeUnspecified attributeState = '!'
33 attributeUnset attributeState = '-'
34 attributeSetValue attributeState = '='
35)
36
37type Attribute interface {
38 Name() string
39 IsSet() bool
40 IsUnset() bool
41 IsUnspecified() bool
42 IsValueSet() bool
43 Value() string
44 String() string
45}
46
47type attribute struct {
48 name string
49 state attributeState
50 value string
51}
52
53func (a attribute) Name() string {
54 return a.name
55}
56
57func (a attribute) IsSet() bool {
58 return a.state == attributeSet
59}
60
61func (a attribute) IsUnset() bool {
62 return a.state == attributeUnset
63}
64
65func (a attribute) IsUnspecified() bool {
66 return a.state == attributeUnspecified
67}
68
69func (a attribute) IsValueSet() bool {
70 return a.state == attributeSetValue
71}
72
73func (a attribute) Value() string {
74 return a.value
75}
76
77func (a attribute) String() string {
78 switch a.state {
79 case attributeSet:
80 return a.name + ": set"
81 case attributeUnset:
82 return a.name + ": unset"
83 case attributeUnspecified:
84 return a.name + ": unspecified"
85 default:
86 return a.name + ": " + a.value
87 }
88}
89
90// ReadAttributes reads patterns and attributes from the gitattributes format.
91func ReadAttributes(r io.Reader, domain []string, allowMacro bool) (attributes []MatchAttribute, err error) {
92 scanner := bufio.NewScanner(r)
93
94 for scanner.Scan() {
95 attribute, err := ParseAttributesLine(scanner.Text(), domain, allowMacro)
96 if err != nil {
97 return attributes, err
98 }
99 if len(attribute.Name) == 0 {
100 continue
101 }
102
103 attributes = append(attributes, attribute)
104 }
105
106 if err := scanner.Err(); err != nil {
107 return attributes, err
108 }
109
110 return attributes, nil
111}
112
113// ParseAttributesLine parses a gitattribute line, extracting path pattern and
114// attributes.
115func ParseAttributesLine(line string, domain []string, allowMacro bool) (m MatchAttribute, err error) {
116 line = strings.TrimSpace(line)
117
118 if strings.HasPrefix(line, commentPrefix) || len(line) == 0 {
119 return
120 }
121
122 name, unquoted := unquote(line)
123 attrs := strings.Fields(unquoted)
124 if len(name) == 0 {
125 name = attrs[0]
126 attrs = attrs[1:]
127 }
128
129 var macro bool
130 macro, name, err = checkMacro(name, allowMacro)
131 if err != nil {
132 return
133 }
134
135 m.Name = name
136 m.Attributes = make([]Attribute, 0, len(attrs))
137
138 for _, attrName := range attrs {
139 attr := attribute{
140 name: attrName,
141 state: attributeSet,
142 }
143
144 // ! and - prefixes
145 state := attributeState(attr.name[0])
146 if state == attributeUnspecified || state == attributeUnset {
147 attr.state = state
148 attr.name = attr.name[1:]
149 }
150
151 kv := strings.SplitN(attrName, "=", 2)
152 if len(kv) == 2 {
153 attr.name = kv[0]
154 attr.value = kv[1]
155 attr.state = attributeSetValue
156 }
157
158 if !validAttributeName(attr.name) {
159 return m, ErrInvalidAttributeName
160 }
161 m.Attributes = append(m.Attributes, attr)
162 }
163
164 if !macro {
165 m.Pattern = ParsePattern(name, domain)
166 }
167 return
168}
169
170func checkMacro(name string, allowMacro bool) (macro bool, macroName string, err error) {
171 if !strings.HasPrefix(name, macroPrefix) {
172 return false, name, nil
173 }
174 if !allowMacro {
175 return true, name, ErrMacroNotAllowed
176 }
177
178 macroName = name[len(macroPrefix):]
179 if !validAttributeName(macroName) {
180 return true, name, ErrInvalidAttributeName
181 }
182 return true, macroName, nil
183}
184
185func validAttributeName(name string) bool {
186 if len(name) == 0 || name[0] == '-' {
187 return false
188 }
189
190 for _, ch := range name {
191 if !(ch == '-' || ch == '.' || ch == '_' ||
192 ('0' <= ch && ch <= '9') ||
193 ('a' <= ch && ch <= 'z') ||
194 ('A' <= ch && ch <= 'Z')) {
195 return false
196 }
197 }
198 return true
199}
200
201func unquote(str string) (string, string) {
202 if str[0] != '"' {
203 return "", str
204 }
205
206 for i := 1; i < len(str); i++ {
207 switch str[i] {
208 case '\\':
209 i++
210 case '"':
211 return str[1:i], str[i+1:]
212 }
213 }
214 return "", str
215}