github.com/2lambda123/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  }