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  }