cd1577af24
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.
204 lines
5.5 KiB
Go
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
|
|
}
|