package gitattr
import (
"bufio"
"bytes"
"io"
"strconv"
"strings"
"github.com/git-lfs/git-lfs/v3/errors"
"github.com/git-lfs/git-lfs/v3/tr"
"github.com/git-lfs/wildmatch/v2"
)
const attrPrefix = "[attr]"
type Line interface {
Attrs() []*Attr
}
type PatternLine interface {
Pattern() *wildmatch.Wildmatch
Line
}
type MacroLine interface {
Macro() string
Line
}
type lineAttrs struct {
attrs []*Attr
}
func (l *lineAttrs) Attrs() []*Attr {
return l.attrs
}
type patternLine struct {
pattern *wildmatch.Wildmatch
lineAttrs
}
func (pl *patternLine) Pattern() *wildmatch.Wildmatch {
return pl.pattern
}
type macroLine struct {
macro string
lineAttrs
}
func (ml *macroLine) Macro() string {
return ml.macro
}
type Attr struct {
K string
V string
Unspecified bool
}
func ParseLines(r io.Reader) ([]Line, string, error) {
var lines []Line
splitter := &lineEndingSplitter{}
scanner := bufio.NewScanner(r)
scanner.Split(splitter.ScanLines)
for scanner.Scan() {
text := strings.TrimSpace(scanner.Text())
if len(text) == 0 {
continue
}
var pattern string
var applied string
var macro string
switch text[0] {
case '#':
continue
case '"':
var err error
last := strings.LastIndex(text, "\"")
if last == 0 {
return nil, "", errors.New(tr.Tr.Get("unbalanced quote: %s", text))
}
pattern, err = strconv.Unquote(text[:last+1])
if err != nil {
return nil, "", errors.Wrap(err, tr.Tr.Get("unable to unquote: %s", text[:last+1]))
}
applied = strings.TrimSpace(text[last+1:])
default:
splits := strings.SplitN(text, " ", 2)
if strings.HasPrefix(splits[0], attrPrefix) {
macro = splits[0][len(attrPrefix):]
} else {
pattern = splits[0]
}
if len(splits) == 2 {
applied = splits[1]
}
}
var lineAttrs lineAttrs
for _, s := range strings.Split(applied, " ") {
if s == "" {
continue
}
var attr Attr
if strings.HasPrefix(s, "-") {
attr.K = strings.TrimPrefix(s, "-")
attr.V = "false"
} else if strings.HasPrefix(s, "!") {
attr.K = strings.TrimPrefix(s, "!")
attr.Unspecified = true
} else if eq := strings.Index(s, "="); eq > -1 {
attr.K = s[:eq]
attr.V = s[eq+1:]
} else {
attr.K = s
attr.V = "true"
}
lineAttrs.attrs = append(lineAttrs.attrs, &attr)
}
var line Line
if pattern != "" {
matchPattern := wildmatch.NewWildmatch(pattern,
wildmatch.Basename, wildmatch.SystemCase,
wildmatch.GitAttributes,
)
line = &patternLine{matchPattern, lineAttrs}
} else {
line = ¯oLine{macro, lineAttrs}
}
lines = append(lines, line)
}
if err := scanner.Err(); err != nil {
return nil, "", err
}
return lines, splitter.LineEnding(), nil
}
type lineEndingSplitter struct {
LFCount int
CRLFCount int
}
func (s *lineEndingSplitter) LineEnding() string {
if s.CRLFCount > s.LFCount {
return "\r\n"
} else if s.LFCount == 0 {
return ""
}
return "\n"
}
func (s *lineEndingSplitter) ScanLines(data []byte, atEOF bool) (advance int, token []byte, err error) {
if atEOF && len(data) == 0 {
return 0, nil, nil
}
if i := bytes.IndexByte(data, '\n'); i >= 0 {
return i + 1, s.dropCR(data[0:i]), nil
}
if atEOF {
return len(data), data, nil
}
return 0, nil, nil
}
func (s *lineEndingSplitter) dropCR(data []byte) []byte {
if len(data) > 0 && data[len(data)-1] == '\r' {
s.CRLFCount++
return data[0 : len(data)-1]
}
s.LFCount++
return data
}