git-lfs/git/attribs.go
brian m. carlson cd1577af24
post-checkout: don't modify permissions of untracked files
Git doesn't know about untracked files, so we should avoid modifying
them when we adjust permissions in the post-checkout hook.  Add an
option to our invocation of git ls-files that controls the use of the
--others flag (which controls listing untracked files), and disable it
when we're looking for files to process with the post-checkout hook.
2021-12-06 13:44:33 +00:00

204 lines
5.5 KiB
Go

package git
import (
"io"
"os"
"path/filepath"
"sort"
"github.com/git-lfs/git-lfs/v3/filepathfilter"
"github.com/git-lfs/git-lfs/v3/git/gitattr"
"github.com/git-lfs/git-lfs/v3/tools"
"github.com/rubyist/tracerx"
)
const (
LockableAttrib = "lockable"
FilterAttrib = "filter"
)
// 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
// Path is handled by Git LFS (i.e., filter=lfs)
Tracked bool
}
type AttributeSource struct {
Path string
LineEnding string
}
type attrFile struct {
path string
readMacros bool
}
func (s *AttributeSource) String() string {
return s.Path
}
// GetRootAttributePaths beahves as GetRootAttributePaths, and loads information
// only from the global gitattributes file.
func GetRootAttributePaths(mp *gitattr.MacroProcessor, cfg Env) []AttributePath {
af, _ := cfg.Get("core.attributesfile")
af, err := tools.ExpandConfigPath(af, "git/attributes")
if err != nil {
return nil
}
if _, err := os.Stat(af); os.IsNotExist(err) {
return nil
}
// The working directory for the root gitattributes file is blank.
return attrPathsFromFile(mp, af, "", true)
}
// GetSystemAttributePaths behaves as GetAttributePaths, and loads information
// only from the system gitattributes file, respecting the $PREFIX environment
// variable.
func GetSystemAttributePaths(mp *gitattr.MacroProcessor, 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 attrPathsFromFile(mp, path, "", true)
}
// 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(mp *gitattr.MacroProcessor, workingDir, gitDir string) []AttributePath {
paths := make([]AttributePath, 0)
for _, file := range findAttributeFiles(workingDir, gitDir) {
paths = append(paths, attrPathsFromFile(mp, file.path, workingDir, file.readMacros)...)
}
return paths
}
func attrPathsFromFile(mp *gitattr.MacroProcessor, path, workingDir string, readMacros bool) []AttributePath {
attributes, err := os.Open(path)
if err != nil {
return nil
}
defer attributes.Close()
return AttrPathsFromReader(mp, path, workingDir, attributes, readMacros)
}
func AttrPathsFromReader(mp *gitattr.MacroProcessor, path, workingDir string, rdr io.Reader, readMacros bool) []AttributePath {
var paths []AttributePath
relfile, _ := filepath.Rel(workingDir, path)
reldir := filepath.Dir(relfile)
source := &AttributeSource{Path: relfile}
lines, eol, err := gitattr.ParseLines(rdr)
if err != nil {
return nil
}
lines = mp.ProcessLines(lines, readMacros)
for _, line := range lines {
lockable := false
tracked := false
hasFilter := false
for _, attr := range line.Attrs {
if attr.K == FilterAttrib {
hasFilter = true
tracked = attr.V == "lfs"
} else if attr.K == LockableAttrib && attr.V == "true" {
lockable = true
}
}
if !hasFilter && !lockable {
continue
}
pattern := line.Pattern.String()
if len(reldir) > 0 {
pattern = filepath.Join(reldir, pattern)
}
paths = append(paths, AttributePath{
Path: pattern,
Source: source,
Lockable: lockable,
Tracked: tracked,
})
}
source.LineEnding = eol
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(gitattr.NewMacroProcessor(), 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), filepathfilter.GitAttributes))
}
return filepathfilter.NewFromPatterns(patterns, nil)
}
func findAttributeFiles(workingDir, gitDir string) []attrFile {
var paths []attrFile
repoAttributes := filepath.Join(gitDir, "info", "attributes")
if info, err := os.Stat(repoAttributes); err == nil && !info.IsDir() {
paths = append(paths, attrFile{path: repoAttributes, readMacros: true})
}
lsFiles, err := NewLsFiles(workingDir, true, true)
if err != nil {
tracerx.Printf("Error finding .gitattributes: %v", err)
return paths
}
if gitattributesFiles, present := lsFiles.FilesByName[".gitattributes"]; present {
for _, f := range gitattributesFiles {
tracerx.Printf("findAttributeFiles: located %s", f.FullPath)
paths = append(paths, attrFile{
path: filepath.Join(workingDir, f.FullPath),
readMacros: f.FullPath == ".gitattributes", // Read macros from the top-level attributes
})
}
}
// reverse the order of the files so more specific entries are found first
// when iterating from the front (respects precedence)
sort.Slice(paths[:], func(i, j int) bool {
return len(paths[i].path) > len(paths[j].path)
})
return paths
}