github.com/stffabi/git-lfs@v2.3.5-0.20180214015214-8eeaa8d88902+incompatible/git/attribs.go (about) 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/tools" 11 "github.com/rubyist/tracerx" 12 ) 13 14 const ( 15 LockableAttrib = "lockable" 16 ) 17 18 // AttributePath is a path entry in a gitattributes file which has the LFS filter 19 type AttributePath struct { 20 // Path entry in the attribute file 21 Path string 22 // The attribute file which was the source of this entry 23 Source *AttributeSource 24 // Path also has the 'lockable' attribute 25 Lockable bool 26 } 27 28 type AttributeSource struct { 29 Path string 30 LineEnding string 31 } 32 33 func (s *AttributeSource) String() string { 34 return s.Path 35 } 36 37 // GetAttributePaths returns a list of entries in .gitattributes which are 38 // configured with the filter=lfs attribute 39 // workingDir is the root of the working copy 40 // gitDir is the root of the git repo 41 func GetAttributePaths(workingDir, gitDir string) []AttributePath { 42 paths := make([]AttributePath, 0) 43 44 for _, path := range findAttributeFiles(workingDir, gitDir) { 45 attributes, err := os.Open(path) 46 if err != nil { 47 continue 48 } 49 50 relfile, _ := filepath.Rel(workingDir, path) 51 reldir := filepath.Dir(relfile) 52 source := &AttributeSource{Path: relfile} 53 54 le := &lineEndingSplitter{} 55 scanner := bufio.NewScanner(attributes) 56 scanner.Split(le.ScanLines) 57 58 for scanner.Scan() { 59 line := strings.TrimSpace(scanner.Text()) 60 61 if strings.HasPrefix(line, "#") { 62 continue 63 } 64 65 // Check for filter=lfs (signifying that LFS is tracking 66 // this file) or "lockable", which indicates that the 67 // file is lockable (and may or may not be tracked by 68 // Git LFS). 69 if strings.Contains(line, "filter=lfs") || 70 strings.HasSuffix(line, "lockable") { 71 72 fields := strings.Fields(line) 73 pattern := fields[0] 74 if len(reldir) > 0 { 75 pattern = filepath.Join(reldir, pattern) 76 } 77 // Find lockable flag in any position after pattern to avoid 78 // edge case of matching "lockable" to a file pattern 79 lockable := false 80 for _, f := range fields[1:] { 81 if f == LockableAttrib { 82 lockable = true 83 break 84 } 85 } 86 paths = append(paths, AttributePath{ 87 Path: pattern, 88 Source: source, 89 Lockable: lockable, 90 }) 91 } 92 } 93 94 source.LineEnding = le.LineEnding() 95 } 96 97 return paths 98 } 99 100 // copies bufio.ScanLines(), counting LF vs CRLF in a file 101 type lineEndingSplitter struct { 102 LFCount int 103 CRLFCount int 104 } 105 106 func (s *lineEndingSplitter) LineEnding() string { 107 if s.CRLFCount > s.LFCount { 108 return "\r\n" 109 } else if s.LFCount == 0 { 110 return "" 111 } 112 return "\n" 113 } 114 115 func (s *lineEndingSplitter) ScanLines(data []byte, atEOF bool) (advance int, token []byte, err error) { 116 if atEOF && len(data) == 0 { 117 return 0, nil, nil 118 } 119 if i := bytes.IndexByte(data, '\n'); i >= 0 { 120 // We have a full newline-terminated line. 121 return i + 1, s.dropCR(data[0:i]), nil 122 } 123 // If we're at EOF, we have a final, non-terminated line. Return it. 124 if atEOF { 125 return len(data), data, nil 126 } 127 // Request more data. 128 return 0, nil, nil 129 } 130 131 // dropCR drops a terminal \r from the data. 132 func (s *lineEndingSplitter) dropCR(data []byte) []byte { 133 if len(data) > 0 && data[len(data)-1] == '\r' { 134 s.CRLFCount++ 135 return data[0 : len(data)-1] 136 } 137 s.LFCount++ 138 return data 139 } 140 141 func findAttributeFiles(workingDir, gitDir string) []string { 142 var paths []string 143 144 repoAttributes := filepath.Join(gitDir, "info", "attributes") 145 if info, err := os.Stat(repoAttributes); err == nil && !info.IsDir() { 146 paths = append(paths, repoAttributes) 147 } 148 149 tools.FastWalkGitRepo(workingDir, func(parentDir string, info os.FileInfo, err error) { 150 if err != nil { 151 tracerx.Printf("Error finding .gitattributes: %v", err) 152 return 153 } 154 155 if info.IsDir() || info.Name() != ".gitattributes" { 156 return 157 } 158 paths = append(paths, filepath.Join(parentDir, info.Name())) 159 }) 160 161 // reverse the order of the files so more specific entries are found first 162 // when iterating from the front (respects precedence) 163 for i, j := 0, len(paths)-1; i < j; i, j = i+1, j-1 { 164 paths[i], paths[j] = paths[j], paths[i] 165 } 166 167 return paths 168 }