2016-12-19 16:45:08 +00:00
|
|
|
package git
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bufio"
|
2017-02-04 00:13:42 +00:00
|
|
|
"bytes"
|
2016-12-19 16:45:08 +00:00
|
|
|
"os"
|
|
|
|
"path/filepath"
|
|
|
|
"strings"
|
|
|
|
|
2018-05-25 21:06:54 +00:00
|
|
|
"github.com/git-lfs/git-lfs/filepathfilter"
|
2016-12-19 16:45:08 +00:00
|
|
|
"github.com/git-lfs/git-lfs/tools"
|
|
|
|
"github.com/rubyist/tracerx"
|
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
LockableAttrib = "lockable"
|
|
|
|
)
|
|
|
|
|
|
|
|
// AttributePath is a path entry in a gitattributes file which has the LFS filter
|
|
|
|
type AttributePath struct {
|
|
|
|
// Path entry in the attribute file
|
|
|
|
Path string
|
|
|
|
// The attribute file which was the source of this entry
|
2017-02-04 00:13:42 +00:00
|
|
|
Source *AttributeSource
|
2016-12-19 16:45:08 +00:00
|
|
|
// Path also has the 'lockable' attribute
|
|
|
|
Lockable bool
|
|
|
|
}
|
|
|
|
|
2017-02-04 00:13:42 +00:00
|
|
|
type AttributeSource struct {
|
|
|
|
Path string
|
|
|
|
LineEnding string
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *AttributeSource) String() string {
|
|
|
|
return s.Path
|
|
|
|
}
|
|
|
|
|
2018-06-19 00:19:10 +00:00
|
|
|
// GetRootAttributePaths beahves as GetRootAttributePaths, and loads information
|
|
|
|
// only from the global gitattributes file.
|
|
|
|
func GetRootAttributePaths(cfg Env) []AttributePath {
|
|
|
|
af, ok := cfg.Get("core.attributesfile")
|
|
|
|
if !ok {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// The working directory for the root gitattributes file is blank.
|
|
|
|
return attrPaths(af, "")
|
|
|
|
}
|
|
|
|
|
2018-06-19 00:19:25 +00:00
|
|
|
// GetSystemAttributePaths behaves as GetAttributePaths, and loads information
|
|
|
|
// only from the system gitattributes file, respecting the $PREFIX environment
|
|
|
|
// variable.
|
|
|
|
func GetSystemAttributePaths(env Env) []AttributePath {
|
|
|
|
prefix, _ := env.Get("PREFIX")
|
|
|
|
if len(prefix) == 0 {
|
|
|
|
prefix = string(filepath.Separator)
|
|
|
|
}
|
|
|
|
|
|
|
|
path := filepath.Join(prefix, "etc", "gitattributes")
|
|
|
|
|
|
|
|
if _, err := os.Stat(path); os.IsNotExist(err) {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
return attrPaths(path, "")
|
|
|
|
}
|
|
|
|
|
2016-12-19 16:45:08 +00:00
|
|
|
// GetAttributePaths returns a list of entries in .gitattributes which are
|
|
|
|
// configured with the filter=lfs attribute
|
2017-11-01 13:58:15 +00:00
|
|
|
// workingDir is the root of the working copy
|
2016-12-19 16:45:08 +00:00
|
|
|
// gitDir is the root of the git repo
|
|
|
|
func GetAttributePaths(workingDir, gitDir string) []AttributePath {
|
|
|
|
paths := make([]AttributePath, 0)
|
|
|
|
|
|
|
|
for _, path := range findAttributeFiles(workingDir, gitDir) {
|
2018-06-19 00:18:48 +00:00
|
|
|
paths = append(paths, attrPaths(path, workingDir)...)
|
|
|
|
}
|
2016-12-19 16:45:08 +00:00
|
|
|
|
2018-06-19 00:18:48 +00:00
|
|
|
return paths
|
|
|
|
}
|
2017-02-04 00:13:42 +00:00
|
|
|
|
2018-06-19 00:18:48 +00:00
|
|
|
func attrPaths(path, workingDir string) []AttributePath {
|
|
|
|
attributes, err := os.Open(path)
|
|
|
|
if err != nil {
|
|
|
|
return nil
|
|
|
|
}
|
2016-12-19 16:45:08 +00:00
|
|
|
|
2018-06-19 00:18:48 +00:00
|
|
|
var paths []AttributePath
|
2017-11-01 13:58:18 +00:00
|
|
|
|
2018-06-19 00:18:48 +00:00
|
|
|
relfile, _ := filepath.Rel(workingDir, path)
|
|
|
|
reldir := filepath.Dir(relfile)
|
|
|
|
source := &AttributeSource{Path: relfile}
|
2017-08-25 00:31:00 +00:00
|
|
|
|
2018-06-19 00:18:48 +00:00
|
|
|
le := &lineEndingSplitter{}
|
|
|
|
scanner := bufio.NewScanner(attributes)
|
|
|
|
scanner.Split(le.ScanLines)
|
|
|
|
|
|
|
|
for scanner.Scan() {
|
|
|
|
line := strings.TrimSpace(scanner.Text())
|
|
|
|
|
|
|
|
if strings.HasPrefix(line, "#") {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check for filter=lfs (signifying that LFS is tracking
|
|
|
|
// this file) or "lockable", which indicates that the
|
|
|
|
// file is lockable (and may or may not be tracked by
|
|
|
|
// Git LFS).
|
|
|
|
if strings.Contains(line, "filter=lfs") ||
|
|
|
|
strings.HasSuffix(line, "lockable") {
|
|
|
|
|
|
|
|
fields := strings.Fields(line)
|
|
|
|
pattern := fields[0]
|
|
|
|
if len(reldir) > 0 {
|
|
|
|
pattern = filepath.Join(reldir, pattern)
|
|
|
|
}
|
|
|
|
// Find lockable flag in any position after pattern to avoid
|
|
|
|
// edge case of matching "lockable" to a file pattern
|
|
|
|
lockable := false
|
|
|
|
for _, f := range fields[1:] {
|
|
|
|
if f == LockableAttrib {
|
|
|
|
lockable = true
|
|
|
|
break
|
2017-01-09 10:54:21 +00:00
|
|
|
}
|
2016-12-19 16:45:08 +00:00
|
|
|
}
|
2018-06-19 00:18:48 +00:00
|
|
|
paths = append(paths, AttributePath{
|
|
|
|
Path: pattern,
|
|
|
|
Source: source,
|
|
|
|
Lockable: lockable,
|
|
|
|
})
|
2016-12-19 16:45:08 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-06-19 00:18:48 +00:00
|
|
|
source.LineEnding = le.LineEnding()
|
|
|
|
|
2016-12-19 16:45:08 +00:00
|
|
|
return paths
|
|
|
|
}
|
|
|
|
|
2018-05-25 21:06:54 +00:00
|
|
|
// GetAttributeFilter returns a list of entries in .gitattributes which are
|
|
|
|
// configured with the filter=lfs attribute as a file path filter which
|
|
|
|
// file paths can be matched against
|
|
|
|
// workingDir is the root of the working copy
|
|
|
|
// gitDir is the root of the git repo
|
|
|
|
func GetAttributeFilter(workingDir, gitDir string) *filepathfilter.Filter {
|
|
|
|
paths := GetAttributePaths(workingDir, gitDir)
|
2018-05-30 17:23:14 +00:00
|
|
|
patterns := make([]filepathfilter.Pattern, 0, len(paths))
|
2018-05-25 21:06:54 +00:00
|
|
|
|
2018-05-30 17:23:14 +00:00
|
|
|
for _, path := range paths {
|
2018-05-31 21:33:46 +00:00
|
|
|
// Convert all separators to `/` before creating a pattern to
|
|
|
|
// avoid characters being escaped in situations like `subtree\*.md`
|
|
|
|
patterns = append(patterns, filepathfilter.NewPattern(filepath.ToSlash(path.Path)))
|
2018-05-25 21:06:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return filepathfilter.NewFromPatterns(patterns, nil)
|
|
|
|
}
|
|
|
|
|
2017-02-04 00:13:42 +00:00
|
|
|
// copies bufio.ScanLines(), counting LF vs CRLF in a file
|
|
|
|
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 {
|
|
|
|
// We have a full newline-terminated line.
|
|
|
|
return i + 1, s.dropCR(data[0:i]), nil
|
|
|
|
}
|
|
|
|
// If we're at EOF, we have a final, non-terminated line. Return it.
|
|
|
|
if atEOF {
|
|
|
|
return len(data), data, nil
|
|
|
|
}
|
|
|
|
// Request more data.
|
|
|
|
return 0, nil, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// dropCR drops a terminal \r from the data.
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2016-12-19 16:45:08 +00:00
|
|
|
func findAttributeFiles(workingDir, gitDir string) []string {
|
|
|
|
var paths []string
|
|
|
|
|
|
|
|
repoAttributes := filepath.Join(gitDir, "info", "attributes")
|
|
|
|
if info, err := os.Stat(repoAttributes); err == nil && !info.IsDir() {
|
|
|
|
paths = append(paths, repoAttributes)
|
|
|
|
}
|
|
|
|
|
|
|
|
tools.FastWalkGitRepo(workingDir, func(parentDir string, info os.FileInfo, err error) {
|
|
|
|
if err != nil {
|
|
|
|
tracerx.Printf("Error finding .gitattributes: %v", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if info.IsDir() || info.Name() != ".gitattributes" {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
paths = append(paths, filepath.Join(parentDir, info.Name()))
|
|
|
|
})
|
|
|
|
|
|
|
|
// reverse the order of the files so more specific entries are found first
|
|
|
|
// when iterating from the front (respects precedence)
|
|
|
|
for i, j := 0, len(paths)-1; i < j; i, j = i+1, j-1 {
|
|
|
|
paths[i], paths[j] = paths[j], paths[i]
|
|
|
|
}
|
|
|
|
|
|
|
|
return paths
|
|
|
|
}
|