Imported Upstream version 2.5.1
[scm/test.git] / git / attribs.go
1 package git
2
3 import (
4         "bufio"
5         "bytes"
6         "os"
7         "path/filepath"
8         "strings"
9
10         "github.com/git-lfs/git-lfs/filepathfilter"
11         "github.com/git-lfs/git-lfs/tools"
12         "github.com/rubyist/tracerx"
13 )
14
15 const (
16         LockableAttrib = "lockable"
17 )
18
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
22         Path string
23         // The attribute file which was the source of this entry
24         Source *AttributeSource
25         // Path also has the 'lockable' attribute
26         Lockable bool
27 }
28
29 type AttributeSource struct {
30         Path       string
31         LineEnding string
32 }
33
34 func (s *AttributeSource) String() string {
35         return s.Path
36 }
37
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")
42         if !ok {
43                 return nil
44         }
45
46         // The working directory for the root gitattributes file is blank.
47         return attrPaths(af, "")
48 }
49
50 // GetSystemAttributePaths behaves as GetAttributePaths, and loads information
51 // only from the system gitattributes file, respecting the $PREFIX environment
52 // variable.
53 func GetSystemAttributePaths(env Env) []AttributePath {
54         prefix, _ := env.Get("PREFIX")
55         if len(prefix) == 0 {
56                 prefix = string(filepath.Separator)
57         }
58
59         path := filepath.Join(prefix, "etc", "gitattributes")
60
61         if _, err := os.Stat(path); os.IsNotExist(err) {
62                 return nil
63         }
64
65         return attrPaths(path, "")
66 }
67
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)
74
75         for _, path := range findAttributeFiles(workingDir, gitDir) {
76                 paths = append(paths, attrPaths(path, workingDir)...)
77         }
78
79         return paths
80 }
81
82 func attrPaths(path, workingDir string) []AttributePath {
83         attributes, err := os.Open(path)
84         if err != nil {
85                 return nil
86         }
87
88         var paths []AttributePath
89
90         relfile, _ := filepath.Rel(workingDir, path)
91         reldir := filepath.Dir(relfile)
92         source := &AttributeSource{Path: relfile}
93
94         le := &lineEndingSplitter{}
95         scanner := bufio.NewScanner(attributes)
96         scanner.Split(le.ScanLines)
97
98         for scanner.Scan() {
99                 line := strings.TrimSpace(scanner.Text())
100
101                 if strings.HasPrefix(line, "#") {
102                         continue
103                 }
104
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
108                 // Git LFS).
109                 if strings.Contains(line, "filter=lfs") ||
110                         strings.HasSuffix(line, "lockable") {
111
112                         fields := strings.Fields(line)
113                         pattern := fields[0]
114                         if len(reldir) > 0 {
115                                 pattern = filepath.Join(reldir, pattern)
116                         }
117                         // Find lockable flag in any position after pattern to avoid
118                         // edge case of matching "lockable" to a file pattern
119                         lockable := false
120                         for _, f := range fields[1:] {
121                                 if f == LockableAttrib {
122                                         lockable = true
123                                         break
124                                 }
125                         }
126                         paths = append(paths, AttributePath{
127                                 Path:     pattern,
128                                 Source:   source,
129                                 Lockable: lockable,
130                         })
131                 }
132         }
133
134         source.LineEnding = le.LineEnding()
135
136         return paths
137 }
138
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))
147
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)))
152         }
153
154         return filepathfilter.NewFromPatterns(patterns, nil)
155 }
156
157 // copies bufio.ScanLines(), counting LF vs CRLF in a file
158 type lineEndingSplitter struct {
159         LFCount   int
160         CRLFCount int
161 }
162
163 func (s *lineEndingSplitter) LineEnding() string {
164         if s.CRLFCount > s.LFCount {
165                 return "\r\n"
166         } else if s.LFCount == 0 {
167                 return ""
168         }
169         return "\n"
170 }
171
172 func (s *lineEndingSplitter) ScanLines(data []byte, atEOF bool) (advance int, token []byte, err error) {
173         if atEOF && len(data) == 0 {
174                 return 0, nil, nil
175         }
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
179         }
180         // If we're at EOF, we have a final, non-terminated line. Return it.
181         if atEOF {
182                 return len(data), data, nil
183         }
184         // Request more data.
185         return 0, nil, nil
186 }
187
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' {
191                 s.CRLFCount++
192                 return data[0 : len(data)-1]
193         }
194         s.LFCount++
195         return data
196 }
197
198 func findAttributeFiles(workingDir, gitDir string) []string {
199         var paths []string
200
201         repoAttributes := filepath.Join(gitDir, "info", "attributes")
202         if info, err := os.Stat(repoAttributes); err == nil && !info.IsDir() {
203                 paths = append(paths, repoAttributes)
204         }
205
206         tools.FastWalkGitRepo(workingDir, func(parentDir string, info os.FileInfo, err error) {
207                 if err != nil {
208                         tracerx.Printf("Error finding .gitattributes: %v", err)
209                         return
210                 }
211
212                 if info.IsDir() || info.Name() != ".gitattributes" {
213                         return
214                 }
215                 paths = append(paths, filepath.Join(parentDir, info.Name()))
216         })
217
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]
222         }
223
224         return paths
225 }