package git
import (
"errors"
"io"
"os"
"path"
"path/filepath"
"sort"
"strings"
"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/git-lfs/git-lfs/v3/tr"
"github.com/rubyist/tracerx"
)
const (
LockableAttrib = "lockable"
FilterAttrib = "filter"
)
type AttributePath struct {
Path string
Source *AttributeSource
Lockable bool
Tracked bool
}
type AttributeSource struct {
Path string
LineEnding string
}
type attrFile struct {
path string
readMacros bool
}
func (s *AttributeSource) String() string {
return s.Path
}
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
}
return attrPathsFromFile(mp, af, "", true)
}
func GetSystemAttributePaths(mp *gitattr.MacroProcessor, env Env) ([]AttributePath, error) {
var path string
if IsGitVersionAtLeast("2.42.0") {
cmd, err := gitNoLFS("var", "GIT_ATTR_SYSTEM")
if err != nil {
return nil, errors.New(tr.Tr.Get("failed to find `git var GIT_ATTR_SYSTEM`: %v", err))
}
out, err := cmd.Output()
if err != nil {
return nil, errors.New(tr.Tr.Get("failed to call `git var GIT_ATTR_SYSTEM`: %v", err))
}
paths := strings.Split(string(out), "\n")
if len(paths) == 0 {
return nil, nil
}
path = paths[0]
} else {
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, nil
}
return attrPathsFromFile(mp, path, "", true), nil
}
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, fpath, workingDir string, rdr io.Reader, readMacros bool) []AttributePath {
var paths []AttributePath
relfile, _ := filepath.Rel(workingDir, fpath)
reldir := filepath.ToSlash(tools.TrimCurrentPrefix(filepath.Dir(relfile)))
if reldir == "." {
reldir = ""
}
source := &AttributeSource{Path: relfile}
lines, eol, err := gitattr.ParseLines(rdr)
if err != nil {
return nil
}
patternLines := mp.ProcessLines(lines, readMacros)
for _, line := range patternLines {
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 = path.Join(reldir, pattern)
}
paths = append(paths, AttributePath{
Path: pattern,
Source: source,
Lockable: lockable,
Tracked: tracked,
})
}
source.LineEnding = eol
return paths
}
func GetAttributeFilter(workingDir, gitDir string) *filepathfilter.Filter {
paths := GetAttributePaths(gitattr.NewMacroProcessor(), workingDir, gitDir)
patterns := make([]filepathfilter.Pattern, 0, len(paths))
for _, path := range paths {
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",
})
}
}
sort.Slice(paths[:], func(i, j int) bool {
return len(paths[i].path) > len(paths[j].path)
})
return paths
}