package git import ( "bufio" "bytes" "os" "path/filepath" "strings" "github.com/git-lfs/git-lfs/filepathfilter" "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 Source *AttributeSource // Path also has the 'lockable' attribute Lockable bool } type AttributeSource struct { Path string LineEnding string } func (s *AttributeSource) String() string { return s.Path } // 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, "") } // 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, "") } // GetAttributePaths returns a list of entries in .gitattributes which are // configured with the filter=lfs attribute // workingDir is the root of the working copy // gitDir is the root of the git repo func GetAttributePaths(workingDir, gitDir string) []AttributePath { paths := make([]AttributePath, 0) for _, path := range findAttributeFiles(workingDir, gitDir) { paths = append(paths, attrPaths(path, workingDir)...) } return paths } func attrPaths(path, workingDir string) []AttributePath { attributes, err := os.Open(path) if err != nil { return nil } var paths []AttributePath relfile, _ := filepath.Rel(workingDir, path) reldir := filepath.Dir(relfile) source := &AttributeSource{Path: relfile} 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 } } paths = append(paths, AttributePath{ Path: pattern, Source: source, Lockable: lockable, }) } } source.LineEnding = le.LineEnding() return paths } // 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) patterns := make([]filepathfilter.Pattern, 0, len(paths)) for _, path := range paths { // 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))) } return filepathfilter.NewFromPatterns(patterns, nil) } // 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 } 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 }