fork of go-git with some jj specific features
at main 215 lines 4.2 kB view raw
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}