package gitattributes import ( "bufio" "errors" "io" "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) { scanner := bufio.NewScanner(r) for scanner.Scan() { attribute, err := ParseAttributesLine(scanner.Text(), domain, allowMacro) if err != nil { return attributes, err } if len(attribute.Name) == 0 { continue } attributes = append(attributes, attribute) } if err := scanner.Err(); err != nil { return attributes, err } 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 }