aboutsummaryrefslogtreecommitdiffstats
path: root/plumbing/format/gitattributes/attributes.go
diff options
context:
space:
mode:
authorArran Walker <arran.walker@fiveturns.org>2019-04-23 10:50:46 +0000
committerArran Walker <arran.walker@fiveturns.org>2019-04-24 09:32:18 +0000
commit86bdbfbf45a0c13aca146955a3325207ebd66c75 (patch)
treec0ff62fa4f6778c3e67f992692dc486ec03bdc14 /plumbing/format/gitattributes/attributes.go
parenteb243ba9a55ac029ab3f9b15157920c46e24078b (diff)
downloadgo-git-86bdbfbf45a0c13aca146955a3325207ebd66c75.tar.gz
plumbing: format/gitattributes support
Implements gitattributes parsing, matching and attribute extraction. Signed-off-by: Arran Walker <arran.walker@fiveturns.org>
Diffstat (limited to 'plumbing/format/gitattributes/attributes.go')
-rw-r--r--plumbing/format/gitattributes/attributes.go214
1 files changed, 214 insertions, 0 deletions
diff --git a/plumbing/format/gitattributes/attributes.go b/plumbing/format/gitattributes/attributes.go
new file mode 100644
index 0000000..d13c2a9
--- /dev/null
+++ b/plumbing/format/gitattributes/attributes.go
@@ -0,0 +1,214 @@
+package gitattributes
+
+import (
+ "errors"
+ "io"
+ "io/ioutil"
+ "strings"
+)
+
+const (
+ commentPrefix = "#"
+ eol = "\n"
+ macroPrefix = "[attr]"
+)
+
+var (
+ ErrMacroNotAllowed = errors.New("macro not allowed")
+ ErrInvalidAttributeName = errors.New("Invalid attribute name")
+)
+
+type MatchAttribute struct {
+ Name string
+ Pattern Pattern
+ Attributes []Attribute
+}
+
+type attributeState byte
+
+const (
+ attributeUnknown attributeState = 0
+ attributeSet attributeState = 1
+ attributeUnspecified attributeState = '!'
+ attributeUnset attributeState = '-'
+ attributeSetValue attributeState = '='
+)
+
+type Attribute interface {
+ Name() string
+ IsSet() bool
+ IsUnset() bool
+ IsUnspecified() bool
+ IsValueSet() bool
+ Value() string
+ String() string
+}
+
+type attribute struct {
+ name string
+ state attributeState
+ value string
+}
+
+func (a attribute) Name() string {
+ return a.name
+}
+
+func (a attribute) IsSet() bool {
+ return a.state == attributeSet
+}
+
+func (a attribute) IsUnset() bool {
+ return a.state == attributeUnset
+}
+
+func (a attribute) IsUnspecified() bool {
+ return a.state == attributeUnspecified
+}
+
+func (a attribute) IsValueSet() bool {
+ return a.state == attributeSetValue
+}
+
+func (a attribute) Value() string {
+ return a.value
+}
+
+func (a attribute) String() string {
+ switch a.state {
+ case attributeSet:
+ return a.name + ": set"
+ case attributeUnset:
+ return a.name + ": unset"
+ case attributeUnspecified:
+ return a.name + ": unspecified"
+ default:
+ return a.name + ": " + a.value
+ }
+}
+
+// ReadAttributes reads patterns and attributes from the gitattributes format.
+func ReadAttributes(r io.Reader, domain []string, allowMacro bool) (attributes []MatchAttribute, err error) {
+ data, err := ioutil.ReadAll(r)
+ if err != nil {
+ return nil, err
+ }
+
+ for _, line := range strings.Split(string(data), eol) {
+ attribute, err := ParseAttributesLine(line, domain, allowMacro)
+ if err != nil {
+ return attributes, err
+ }
+ if len(attribute.Name) == 0 {
+ continue
+ }
+
+ attributes = append(attributes, attribute)
+ }
+
+ return attributes, nil
+}
+
+// ParseAttributesLine parses a gitattribute line, extracting path pattern and
+// attributes.
+func ParseAttributesLine(line string, domain []string, allowMacro bool) (m MatchAttribute, err error) {
+ line = strings.TrimSpace(line)
+
+ if strings.HasPrefix(line, commentPrefix) || len(line) == 0 {
+ return
+ }
+
+ name, unquoted := unquote(line)
+ attrs := strings.Fields(unquoted)
+ if len(name) == 0 {
+ name = attrs[0]
+ attrs = attrs[1:]
+ }
+
+ var macro bool
+ macro, name, err = checkMacro(name, allowMacro)
+ if err != nil {
+ return
+ }
+
+ m.Name = name
+ m.Attributes = make([]Attribute, 0, len(attrs))
+
+ for _, attrName := range attrs {
+ attr := attribute{
+ name: attrName,
+ state: attributeSet,
+ }
+
+ // ! and - prefixes
+ state := attributeState(attr.name[0])
+ if state == attributeUnspecified || state == attributeUnset {
+ attr.state = state
+ attr.name = attr.name[1:]
+ }
+
+ kv := strings.SplitN(attrName, "=", 2)
+ if len(kv) == 2 {
+ attr.name = kv[0]
+ attr.value = kv[1]
+ attr.state = attributeSetValue
+ }
+
+ if !validAttributeName(attr.name) {
+ return m, ErrInvalidAttributeName
+ }
+ m.Attributes = append(m.Attributes, attr)
+ }
+
+ if !macro {
+ m.Pattern = ParsePattern(name, domain)
+ }
+ return
+}
+
+func checkMacro(name string, allowMacro bool) (macro bool, macroName string, err error) {
+ if !strings.HasPrefix(name, macroPrefix) {
+ return false, name, nil
+ }
+ if !allowMacro {
+ return true, name, ErrMacroNotAllowed
+ }
+
+ macroName = name[len(macroPrefix):]
+ if !validAttributeName(macroName) {
+ return true, name, ErrInvalidAttributeName
+ }
+ return true, macroName, nil
+}
+
+func validAttributeName(name string) bool {
+ if len(name) == 0 || name[0] == '-' {
+ return false
+ }
+
+ for _, ch := range name {
+ if !(ch == '-' || ch == '.' || ch == '_' ||
+ ('0' <= ch && ch <= '9') ||
+ ('a' <= ch && ch <= 'z') ||
+ ('A' <= ch && ch <= 'Z')) {
+ return false
+ }
+ }
+ return true
+}
+
+func unquote(str string) (string, string) {
+ if str[0] != '"' {
+ return "", str
+ }
+
+ for i := 1; i < len(str); i++ {
+ switch str[i] {
+ case '\\':
+ i++
+ case '"':
+ return str[1:i], str[i+1:]
+ }
+ }
+ return "", str
+}