github.com/psexton/git-lfs@v2.1.1-0.20170517224304-289a18b2bc53+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 := scanner.Text()
    60  			if strings.Contains(line, "filter=lfs") {
    61  				fields := strings.Fields(line)
    62  				pattern := fields[0]
    63  				if len(reldir) > 0 {
    64  					pattern = filepath.Join(reldir, pattern)
    65  				}
    66  				// Find lockable flag in any position after pattern to avoid
    67  				// edge case of matching "lockable" to a file pattern
    68  				lockable := false
    69  				for _, f := range fields[1:] {
    70  					if f == LockableAttrib {
    71  						lockable = true
    72  						break
    73  					}
    74  				}
    75  				paths = append(paths, AttributePath{
    76  					Path:     pattern,
    77  					Source:   source,
    78  					Lockable: lockable,
    79  				})
    80  			}
    81  		}
    82  
    83  		source.LineEnding = le.LineEnding()
    84  	}
    85  
    86  	return paths
    87  }
    88  
    89  // copies bufio.ScanLines(), counting LF vs CRLF in a file
    90  type lineEndingSplitter struct {
    91  	LFCount   int
    92  	CRLFCount int
    93  }
    94  
    95  func (s *lineEndingSplitter) LineEnding() string {
    96  	if s.CRLFCount > s.LFCount {
    97  		return "\r\n"
    98  	} else if s.LFCount == 0 {
    99  		return ""
   100  	}
   101  	return "\n"
   102  }
   103  
   104  func (s *lineEndingSplitter) ScanLines(data []byte, atEOF bool) (advance int, token []byte, err error) {
   105  	if atEOF && len(data) == 0 {
   106  		return 0, nil, nil
   107  	}
   108  	if i := bytes.IndexByte(data, '\n'); i >= 0 {
   109  		// We have a full newline-terminated line.
   110  		return i + 1, s.dropCR(data[0:i]), nil
   111  	}
   112  	// If we're at EOF, we have a final, non-terminated line. Return it.
   113  	if atEOF {
   114  		return len(data), data, nil
   115  	}
   116  	// Request more data.
   117  	return 0, nil, nil
   118  }
   119  
   120  // dropCR drops a terminal \r from the data.
   121  func (s *lineEndingSplitter) dropCR(data []byte) []byte {
   122  	if len(data) > 0 && data[len(data)-1] == '\r' {
   123  		s.CRLFCount++
   124  		return data[0 : len(data)-1]
   125  	}
   126  	s.LFCount++
   127  	return data
   128  }
   129  
   130  func findAttributeFiles(workingDir, gitDir string) []string {
   131  	var paths []string
   132  
   133  	repoAttributes := filepath.Join(gitDir, "info", "attributes")
   134  	if info, err := os.Stat(repoAttributes); err == nil && !info.IsDir() {
   135  		paths = append(paths, repoAttributes)
   136  	}
   137  
   138  	tools.FastWalkGitRepo(workingDir, func(parentDir string, info os.FileInfo, err error) {
   139  		if err != nil {
   140  			tracerx.Printf("Error finding .gitattributes: %v", err)
   141  			return
   142  		}
   143  
   144  		if info.IsDir() || info.Name() != ".gitattributes" {
   145  			return
   146  		}
   147  		paths = append(paths, filepath.Join(parentDir, info.Name()))
   148  	})
   149  
   150  	// reverse the order of the files so more specific entries are found first
   151  	// when iterating from the front (respects precedence)
   152  	for i, j := 0, len(paths)-1; i < j; i, j = i+1, j-1 {
   153  		paths[i], paths[j] = paths[j], paths[i]
   154  	}
   155  
   156  	return paths
   157  }