Skip to content
This repository was archived by the owner on Sep 11, 2020. It is now read-only.

Commit bbca4e0

Browse files
authored
Merge pull request #1130 from saracen/gitattributes
plumbing: format/gitattributes support
2 parents 4a62292 + 86bdbfb commit bbca4e0

File tree

8 files changed

+1043
-0
lines changed

8 files changed

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

0 commit comments

Comments
 (0)