10 "github.com/git-lfs/git-lfs/filepathfilter"
11 "github.com/git-lfs/git-lfs/tools"
12 "github.com/rubyist/tracerx"
16 LockableAttrib = "lockable"
19 // AttributePath is a path entry in a gitattributes file which has the LFS filter
20 type AttributePath struct {
21 // Path entry in the attribute file
23 // The attribute file which was the source of this entry
24 Source *AttributeSource
25 // Path also has the 'lockable' attribute
29 type AttributeSource struct {
34 func (s *AttributeSource) String() string {
38 // GetRootAttributePaths beahves as GetRootAttributePaths, and loads information
39 // only from the global gitattributes file.
40 func GetRootAttributePaths(cfg Env) []AttributePath {
41 af, ok := cfg.Get("core.attributesfile")
46 // The working directory for the root gitattributes file is blank.
47 return attrPaths(af, "")
50 // GetSystemAttributePaths behaves as GetAttributePaths, and loads information
51 // only from the system gitattributes file, respecting the $PREFIX environment
53 func GetSystemAttributePaths(env Env) []AttributePath {
54 prefix, _ := env.Get("PREFIX")
56 prefix = string(filepath.Separator)
59 path := filepath.Join(prefix, "etc", "gitattributes")
61 if _, err := os.Stat(path); os.IsNotExist(err) {
65 return attrPaths(path, "")
68 // GetAttributePaths returns a list of entries in .gitattributes which are
69 // configured with the filter=lfs attribute
70 // workingDir is the root of the working copy
71 // gitDir is the root of the git repo
72 func GetAttributePaths(workingDir, gitDir string) []AttributePath {
73 paths := make([]AttributePath, 0)
75 for _, path := range findAttributeFiles(workingDir, gitDir) {
76 paths = append(paths, attrPaths(path, workingDir)...)
82 func attrPaths(path, workingDir string) []AttributePath {
83 attributes, err := os.Open(path)
88 var paths []AttributePath
90 relfile, _ := filepath.Rel(workingDir, path)
91 reldir := filepath.Dir(relfile)
92 source := &AttributeSource{Path: relfile}
94 le := &lineEndingSplitter{}
95 scanner := bufio.NewScanner(attributes)
96 scanner.Split(le.ScanLines)
99 line := strings.TrimSpace(scanner.Text())
101 if strings.HasPrefix(line, "#") {
105 // Check for filter=lfs (signifying that LFS is tracking
106 // this file) or "lockable", which indicates that the
107 // file is lockable (and may or may not be tracked by
109 if strings.Contains(line, "filter=lfs") ||
110 strings.HasSuffix(line, "lockable") {
112 fields := strings.Fields(line)
115 pattern = filepath.Join(reldir, pattern)
117 // Find lockable flag in any position after pattern to avoid
118 // edge case of matching "lockable" to a file pattern
120 for _, f := range fields[1:] {
121 if f == LockableAttrib {
126 paths = append(paths, AttributePath{
134 source.LineEnding = le.LineEnding()
139 // GetAttributeFilter returns a list of entries in .gitattributes which are
140 // configured with the filter=lfs attribute as a file path filter which
141 // file paths can be matched against
142 // workingDir is the root of the working copy
143 // gitDir is the root of the git repo
144 func GetAttributeFilter(workingDir, gitDir string) *filepathfilter.Filter {
145 paths := GetAttributePaths(workingDir, gitDir)
146 patterns := make([]filepathfilter.Pattern, 0, len(paths))
148 for _, path := range paths {
149 // Convert all separators to `/` before creating a pattern to
150 // avoid characters being escaped in situations like `subtree\*.md`
151 patterns = append(patterns, filepathfilter.NewPattern(filepath.ToSlash(path.Path)))
154 return filepathfilter.NewFromPatterns(patterns, nil)
157 // copies bufio.ScanLines(), counting LF vs CRLF in a file
158 type lineEndingSplitter struct {
163 func (s *lineEndingSplitter) LineEnding() string {
164 if s.CRLFCount > s.LFCount {
166 } else if s.LFCount == 0 {
172 func (s *lineEndingSplitter) ScanLines(data []byte, atEOF bool) (advance int, token []byte, err error) {
173 if atEOF && len(data) == 0 {
176 if i := bytes.IndexByte(data, '\n'); i >= 0 {
177 // We have a full newline-terminated line.
178 return i + 1, s.dropCR(data[0:i]), nil
180 // If we're at EOF, we have a final, non-terminated line. Return it.
182 return len(data), data, nil
184 // Request more data.
188 // dropCR drops a terminal \r from the data.
189 func (s *lineEndingSplitter) dropCR(data []byte) []byte {
190 if len(data) > 0 && data[len(data)-1] == '\r' {
192 return data[0 : len(data)-1]
198 func findAttributeFiles(workingDir, gitDir string) []string {
201 repoAttributes := filepath.Join(gitDir, "info", "attributes")
202 if info, err := os.Stat(repoAttributes); err == nil && !info.IsDir() {
203 paths = append(paths, repoAttributes)
206 tools.FastWalkGitRepo(workingDir, func(parentDir string, info os.FileInfo, err error) {
208 tracerx.Printf("Error finding .gitattributes: %v", err)
212 if info.IsDir() || info.Name() != ".gitattributes" {
215 paths = append(paths, filepath.Join(parentDir, info.Name()))
218 // reverse the order of the files so more specific entries are found first
219 // when iterating from the front (respects precedence)
220 for i, j := 0, len(paths)-1; i < j; i, j = i+1, j-1 {
221 paths[i], paths[j] = paths[j], paths[i]