github.com/saracen/git-lfs@v2.5.2+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/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 }