Merge pull request #3391 from bk2204/attr-alias
Support attribute macros
This commit is contained in:
commit
0115ad1065
@ -11,6 +11,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/git-lfs/git-lfs/git"
|
"github.com/git-lfs/git-lfs/git"
|
||||||
|
"github.com/git-lfs/git-lfs/git/gitattr"
|
||||||
"github.com/git-lfs/git-lfs/tools"
|
"github.com/git-lfs/git-lfs/tools"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
@ -50,9 +51,13 @@ func trackCommand(cmd *cobra.Command, args []string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mp := gitattr.NewMacroProcessor()
|
||||||
|
|
||||||
// Intentionally do _not_ consider global- and system-level
|
// Intentionally do _not_ consider global- and system-level
|
||||||
// .gitattributes here.
|
// .gitattributes here. Parse them still to expand any macros.
|
||||||
knownPatterns := git.GetAttributePaths(cfg.LocalWorkingDir(), cfg.LocalGitDir())
|
git.GetSystemAttributePaths(mp, cfg.Os)
|
||||||
|
git.GetRootAttributePaths(mp, cfg.Git)
|
||||||
|
knownPatterns := git.GetAttributePaths(mp, cfg.LocalWorkingDir(), cfg.LocalGitDir())
|
||||||
lineEnd := getAttributeLineEnding(knownPatterns)
|
lineEnd := getAttributeLineEnding(knownPatterns)
|
||||||
if len(lineEnd) == 0 {
|
if len(lineEnd) == 0 {
|
||||||
lineEnd = gitLineEnding(cfg.Git)
|
lineEnd = gitLineEnding(cfg.Git)
|
||||||
@ -243,9 +248,16 @@ func listPatterns() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getAllKnownPatterns() []git.AttributePath {
|
func getAllKnownPatterns() []git.AttributePath {
|
||||||
knownPatterns := git.GetAttributePaths(cfg.LocalWorkingDir(), cfg.LocalGitDir())
|
mp := gitattr.NewMacroProcessor()
|
||||||
knownPatterns = append(knownPatterns, git.GetRootAttributePaths(cfg.Git)...)
|
|
||||||
knownPatterns = append(knownPatterns, git.GetSystemAttributePaths(cfg.Os)...)
|
// Parse these in this order so that macros in one file are properly
|
||||||
|
// expanded when referred to in a later file, then order them in the
|
||||||
|
// order we want.
|
||||||
|
systemPatterns := git.GetSystemAttributePaths(mp, cfg.Os)
|
||||||
|
globalPatterns := git.GetRootAttributePaths(mp, cfg.Git)
|
||||||
|
knownPatterns := git.GetAttributePaths(mp, cfg.LocalWorkingDir(), cfg.LocalGitDir())
|
||||||
|
knownPatterns = append(knownPatterns, globalPatterns...)
|
||||||
|
knownPatterns = append(knownPatterns, systemPatterns...)
|
||||||
|
|
||||||
return knownPatterns
|
return knownPatterns
|
||||||
}
|
}
|
||||||
|
165
git/attribs.go
165
git/attribs.go
@ -1,20 +1,18 @@
|
|||||||
package git
|
package git
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
|
||||||
"bytes"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/git-lfs/git-lfs/filepathfilter"
|
"github.com/git-lfs/git-lfs/filepathfilter"
|
||||||
|
"github.com/git-lfs/git-lfs/git/gitattr"
|
||||||
"github.com/git-lfs/git-lfs/tools"
|
"github.com/git-lfs/git-lfs/tools"
|
||||||
"github.com/rubyist/tracerx"
|
"github.com/rubyist/tracerx"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
LockableAttrib = "lockable"
|
LockableAttrib = "lockable"
|
||||||
FilterDisableAttrib = "-filter"
|
FilterAttrib = "filter"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AttributePath is a path entry in a gitattributes file which has the LFS filter
|
// AttributePath is a path entry in a gitattributes file which has the LFS filter
|
||||||
@ -34,26 +32,36 @@ type AttributeSource struct {
|
|||||||
LineEnding string
|
LineEnding string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type attrFile struct {
|
||||||
|
path string
|
||||||
|
readMacros bool
|
||||||
|
}
|
||||||
|
|
||||||
func (s *AttributeSource) String() string {
|
func (s *AttributeSource) String() string {
|
||||||
return s.Path
|
return s.Path
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetRootAttributePaths beahves as GetRootAttributePaths, and loads information
|
// GetRootAttributePaths beahves as GetRootAttributePaths, and loads information
|
||||||
// only from the global gitattributes file.
|
// only from the global gitattributes file.
|
||||||
func GetRootAttributePaths(cfg Env) []AttributePath {
|
func GetRootAttributePaths(mp *gitattr.MacroProcessor, cfg Env) []AttributePath {
|
||||||
af, ok := cfg.Get("core.attributesfile")
|
af, _ := cfg.Get("core.attributesfile")
|
||||||
if !ok {
|
af, err := tools.ExpandConfigPath(af, "git/attributes")
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := os.Stat(af); os.IsNotExist(err) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// The working directory for the root gitattributes file is blank.
|
// The working directory for the root gitattributes file is blank.
|
||||||
return attrPaths(af, "")
|
return attrPaths(mp, af, "", true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetSystemAttributePaths behaves as GetAttributePaths, and loads information
|
// GetSystemAttributePaths behaves as GetAttributePaths, and loads information
|
||||||
// only from the system gitattributes file, respecting the $PREFIX environment
|
// only from the system gitattributes file, respecting the $PREFIX environment
|
||||||
// variable.
|
// variable.
|
||||||
func GetSystemAttributePaths(env Env) []AttributePath {
|
func GetSystemAttributePaths(mp *gitattr.MacroProcessor, env Env) []AttributePath {
|
||||||
prefix, _ := env.Get("PREFIX")
|
prefix, _ := env.Get("PREFIX")
|
||||||
if len(prefix) == 0 {
|
if len(prefix) == 0 {
|
||||||
prefix = string(filepath.Separator)
|
prefix = string(filepath.Separator)
|
||||||
@ -65,24 +73,24 @@ func GetSystemAttributePaths(env Env) []AttributePath {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return attrPaths(path, "")
|
return attrPaths(mp, path, "", true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAttributePaths returns a list of entries in .gitattributes which are
|
// GetAttributePaths returns a list of entries in .gitattributes which are
|
||||||
// configured with the filter=lfs attribute
|
// configured with the filter=lfs attribute
|
||||||
// workingDir is the root of the working copy
|
// workingDir is the root of the working copy
|
||||||
// gitDir is the root of the git repo
|
// gitDir is the root of the git repo
|
||||||
func GetAttributePaths(workingDir, gitDir string) []AttributePath {
|
func GetAttributePaths(mp *gitattr.MacroProcessor, workingDir, gitDir string) []AttributePath {
|
||||||
paths := make([]AttributePath, 0)
|
paths := make([]AttributePath, 0)
|
||||||
|
|
||||||
for _, path := range findAttributeFiles(workingDir, gitDir) {
|
for _, file := range findAttributeFiles(workingDir, gitDir) {
|
||||||
paths = append(paths, attrPaths(path, workingDir)...)
|
paths = append(paths, attrPaths(mp, file.path, workingDir, file.readMacros)...)
|
||||||
}
|
}
|
||||||
|
|
||||||
return paths
|
return paths
|
||||||
}
|
}
|
||||||
|
|
||||||
func attrPaths(path, workingDir string) []AttributePath {
|
func attrPaths(mp *gitattr.MacroProcessor, path, workingDir string, readMacros bool) []AttributePath {
|
||||||
attributes, err := os.Open(path)
|
attributes, err := os.Open(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
@ -95,55 +103,45 @@ func attrPaths(path, workingDir string) []AttributePath {
|
|||||||
reldir := filepath.Dir(relfile)
|
reldir := filepath.Dir(relfile)
|
||||||
source := &AttributeSource{Path: relfile}
|
source := &AttributeSource{Path: relfile}
|
||||||
|
|
||||||
le := &lineEndingSplitter{}
|
lines, eol, err := gitattr.ParseLines(attributes)
|
||||||
scanner := bufio.NewScanner(attributes)
|
if err != nil {
|
||||||
scanner.Split(le.ScanLines)
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
for scanner.Scan() {
|
lines = mp.ProcessLines(lines, readMacros)
|
||||||
line := strings.TrimSpace(scanner.Text())
|
|
||||||
|
|
||||||
if strings.HasPrefix(line, "#") {
|
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
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
hasFilter := strings.Contains(line, "filter=lfs")
|
pattern := line.Pattern.String()
|
||||||
|
if len(reldir) > 0 {
|
||||||
// Check for filter=lfs (signifying that LFS is tracking
|
pattern = filepath.Join(reldir, pattern)
|
||||||
// this file) or "lockable", which indicates that the
|
|
||||||
// file is lockable (and may or may not be tracked by
|
|
||||||
// Git LFS).
|
|
||||||
if hasFilter ||
|
|
||||||
strings.Contains(line, FilterDisableAttrib) ||
|
|
||||||
strings.HasSuffix(line, "lockable") {
|
|
||||||
|
|
||||||
fields := strings.Fields(line)
|
|
||||||
pattern := fields[0]
|
|
||||||
if len(reldir) > 0 {
|
|
||||||
pattern = filepath.Join(reldir, pattern)
|
|
||||||
}
|
|
||||||
// Find lockable flag in any position after pattern to avoid
|
|
||||||
// edge case of matching "lockable" to a file pattern
|
|
||||||
lockable := false
|
|
||||||
tracked := true
|
|
||||||
for _, f := range fields[1:] {
|
|
||||||
if f == LockableAttrib {
|
|
||||||
lockable = true
|
|
||||||
}
|
|
||||||
if !hasFilter ||
|
|
||||||
strings.HasPrefix(f, FilterDisableAttrib) {
|
|
||||||
tracked = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
paths = append(paths, AttributePath{
|
|
||||||
Path: pattern,
|
|
||||||
Source: source,
|
|
||||||
Lockable: lockable,
|
|
||||||
Tracked: tracked,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
paths = append(paths, AttributePath{
|
||||||
|
Path: pattern,
|
||||||
|
Source: source,
|
||||||
|
Lockable: lockable,
|
||||||
|
Tracked: tracked,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
source.LineEnding = le.LineEnding()
|
source.LineEnding = eol
|
||||||
|
|
||||||
return paths
|
return paths
|
||||||
}
|
}
|
||||||
@ -154,7 +152,7 @@ func attrPaths(path, workingDir string) []AttributePath {
|
|||||||
// workingDir is the root of the working copy
|
// workingDir is the root of the working copy
|
||||||
// gitDir is the root of the git repo
|
// gitDir is the root of the git repo
|
||||||
func GetAttributeFilter(workingDir, gitDir string) *filepathfilter.Filter {
|
func GetAttributeFilter(workingDir, gitDir string) *filepathfilter.Filter {
|
||||||
paths := GetAttributePaths(workingDir, gitDir)
|
paths := GetAttributePaths(gitattr.NewMacroProcessor(), workingDir, gitDir)
|
||||||
patterns := make([]filepathfilter.Pattern, 0, len(paths))
|
patterns := make([]filepathfilter.Pattern, 0, len(paths))
|
||||||
|
|
||||||
for _, path := range paths {
|
for _, path := range paths {
|
||||||
@ -166,53 +164,12 @@ func GetAttributeFilter(workingDir, gitDir string) *filepathfilter.Filter {
|
|||||||
return filepathfilter.NewFromPatterns(patterns, nil)
|
return filepathfilter.NewFromPatterns(patterns, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// copies bufio.ScanLines(), counting LF vs CRLF in a file
|
func findAttributeFiles(workingDir, gitDir string) []attrFile {
|
||||||
type lineEndingSplitter struct {
|
var paths []attrFile
|
||||||
LFCount int
|
|
||||||
CRLFCount int
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *lineEndingSplitter) LineEnding() string {
|
|
||||||
if s.CRLFCount > s.LFCount {
|
|
||||||
return "\r\n"
|
|
||||||
} else if s.LFCount == 0 {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return "\n"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *lineEndingSplitter) ScanLines(data []byte, atEOF bool) (advance int, token []byte, err error) {
|
|
||||||
if atEOF && len(data) == 0 {
|
|
||||||
return 0, nil, nil
|
|
||||||
}
|
|
||||||
if i := bytes.IndexByte(data, '\n'); i >= 0 {
|
|
||||||
// We have a full newline-terminated line.
|
|
||||||
return i + 1, s.dropCR(data[0:i]), nil
|
|
||||||
}
|
|
||||||
// If we're at EOF, we have a final, non-terminated line. Return it.
|
|
||||||
if atEOF {
|
|
||||||
return len(data), data, nil
|
|
||||||
}
|
|
||||||
// Request more data.
|
|
||||||
return 0, nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// dropCR drops a terminal \r from the data.
|
|
||||||
func (s *lineEndingSplitter) dropCR(data []byte) []byte {
|
|
||||||
if len(data) > 0 && data[len(data)-1] == '\r' {
|
|
||||||
s.CRLFCount++
|
|
||||||
return data[0 : len(data)-1]
|
|
||||||
}
|
|
||||||
s.LFCount++
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
func findAttributeFiles(workingDir, gitDir string) []string {
|
|
||||||
var paths []string
|
|
||||||
|
|
||||||
repoAttributes := filepath.Join(gitDir, "info", "attributes")
|
repoAttributes := filepath.Join(gitDir, "info", "attributes")
|
||||||
if info, err := os.Stat(repoAttributes); err == nil && !info.IsDir() {
|
if info, err := os.Stat(repoAttributes); err == nil && !info.IsDir() {
|
||||||
paths = append(paths, repoAttributes)
|
paths = append(paths, attrFile{path: repoAttributes, readMacros: true})
|
||||||
}
|
}
|
||||||
|
|
||||||
tools.FastWalkGitRepo(workingDir, func(parentDir string, info os.FileInfo, err error) {
|
tools.FastWalkGitRepo(workingDir, func(parentDir string, info os.FileInfo, err error) {
|
||||||
@ -224,7 +181,11 @@ func findAttributeFiles(workingDir, gitDir string) []string {
|
|||||||
if info.IsDir() || info.Name() != ".gitattributes" {
|
if info.IsDir() || info.Name() != ".gitattributes" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
paths = append(paths, filepath.Join(parentDir, info.Name()))
|
|
||||||
|
paths = append(paths, attrFile{
|
||||||
|
path: filepath.Join(parentDir, info.Name()),
|
||||||
|
readMacros: parentDir == workingDir,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// reverse the order of the files so more specific entries are found first
|
// reverse the order of the files so more specific entries are found first
|
||||||
|
@ -2,6 +2,7 @@ package gitattr
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"bytes"
|
||||||
"io"
|
"io"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@ -10,6 +11,8 @@ import (
|
|||||||
"github.com/git-lfs/wildmatch"
|
"github.com/git-lfs/wildmatch"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const attrPrefix = "[attr]"
|
||||||
|
|
||||||
// Line carries a single line from a repository's .gitattributes file, affecting
|
// Line carries a single line from a repository's .gitattributes file, affecting
|
||||||
// a single pattern and applying zero or more attributes.
|
// a single pattern and applying zero or more attributes.
|
||||||
type Line struct {
|
type Line struct {
|
||||||
@ -21,6 +24,12 @@ type Line struct {
|
|||||||
// repository, while /path/to/.gitattributes affects all blobs that are
|
// repository, while /path/to/.gitattributes affects all blobs that are
|
||||||
// direct or indirect children of /path/to.
|
// direct or indirect children of /path/to.
|
||||||
Pattern *wildmatch.Wildmatch
|
Pattern *wildmatch.Wildmatch
|
||||||
|
// Macro is the name of a macro that, when matched, indicates that all
|
||||||
|
// of the below attributes (Attrs) should be applied to that tree
|
||||||
|
// entry.
|
||||||
|
//
|
||||||
|
// A given entry will have exactly one of Pattern or Macro set.
|
||||||
|
Macro string
|
||||||
// Attrs is the list of attributes to be applied when the above pattern
|
// Attrs is the list of attributes to be applied when the above pattern
|
||||||
// matches a given filename.
|
// matches a given filename.
|
||||||
//
|
//
|
||||||
@ -50,12 +59,14 @@ type Attr struct {
|
|||||||
//
|
//
|
||||||
// If an error was encountered, it will be returned and the []*Line should be
|
// If an error was encountered, it will be returned and the []*Line should be
|
||||||
// considered unusable.
|
// considered unusable.
|
||||||
func ParseLines(r io.Reader) ([]*Line, error) {
|
func ParseLines(r io.Reader) ([]*Line, string, error) {
|
||||||
var lines []*Line
|
var lines []*Line
|
||||||
|
|
||||||
scanner := bufio.NewScanner(r)
|
splitter := &lineEndingSplitter{}
|
||||||
for scanner.Scan() {
|
|
||||||
|
|
||||||
|
scanner := bufio.NewScanner(r)
|
||||||
|
scanner.Split(splitter.ScanLines)
|
||||||
|
for scanner.Scan() {
|
||||||
text := strings.TrimSpace(scanner.Text())
|
text := strings.TrimSpace(scanner.Text())
|
||||||
if len(text) == 0 {
|
if len(text) == 0 {
|
||||||
continue
|
continue
|
||||||
@ -63,6 +74,7 @@ func ParseLines(r io.Reader) ([]*Line, error) {
|
|||||||
|
|
||||||
var pattern string
|
var pattern string
|
||||||
var applied string
|
var applied string
|
||||||
|
var macro string
|
||||||
|
|
||||||
switch text[0] {
|
switch text[0] {
|
||||||
case '#':
|
case '#':
|
||||||
@ -71,17 +83,21 @@ func ParseLines(r io.Reader) ([]*Line, error) {
|
|||||||
var err error
|
var err error
|
||||||
last := strings.LastIndex(text, "\"")
|
last := strings.LastIndex(text, "\"")
|
||||||
if last == 0 {
|
if last == 0 {
|
||||||
return nil, errors.Errorf("git/gitattr: unbalanced quote: %s", text)
|
return nil, "", errors.Errorf("git/gitattr: unbalanced quote: %s", text)
|
||||||
}
|
}
|
||||||
pattern, err = strconv.Unquote(text[:last+1])
|
pattern, err = strconv.Unquote(text[:last+1])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrapf(err, "git/gitattr")
|
return nil, "", errors.Wrapf(err, "git/gitattr")
|
||||||
}
|
}
|
||||||
applied = strings.TrimSpace(text[last+1:])
|
applied = strings.TrimSpace(text[last+1:])
|
||||||
default:
|
default:
|
||||||
splits := strings.SplitN(text, " ", 2)
|
splits := strings.SplitN(text, " ", 2)
|
||||||
|
|
||||||
pattern = splits[0]
|
if strings.HasPrefix(splits[0], attrPrefix) {
|
||||||
|
macro = splits[0][len(attrPrefix):]
|
||||||
|
} else {
|
||||||
|
pattern = splits[0]
|
||||||
|
}
|
||||||
if len(splits) == 2 {
|
if len(splits) == 2 {
|
||||||
applied = splits[1]
|
applied = splits[1]
|
||||||
}
|
}
|
||||||
@ -113,16 +129,63 @@ func ParseLines(r io.Reader) ([]*Line, error) {
|
|||||||
attrs = append(attrs, &attr)
|
attrs = append(attrs, &attr)
|
||||||
}
|
}
|
||||||
|
|
||||||
lines = append(lines, &Line{
|
var matchPattern *wildmatch.Wildmatch
|
||||||
Pattern: wildmatch.NewWildmatch(pattern,
|
if pattern != "" {
|
||||||
|
matchPattern = wildmatch.NewWildmatch(pattern,
|
||||||
wildmatch.Basename, wildmatch.SystemCase,
|
wildmatch.Basename, wildmatch.SystemCase,
|
||||||
),
|
)
|
||||||
Attrs: attrs,
|
}
|
||||||
|
|
||||||
|
lines = append(lines, &Line{
|
||||||
|
Macro: macro,
|
||||||
|
Pattern: matchPattern,
|
||||||
|
Attrs: attrs,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := scanner.Err(); err != nil {
|
if err := scanner.Err(); err != nil {
|
||||||
return nil, err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
return lines, nil
|
return lines, splitter.LineEnding(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// copies bufio.ScanLines(), counting LF vs CRLF in a file
|
||||||
|
type lineEndingSplitter struct {
|
||||||
|
LFCount int
|
||||||
|
CRLFCount int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *lineEndingSplitter) LineEnding() string {
|
||||||
|
if s.CRLFCount > s.LFCount {
|
||||||
|
return "\r\n"
|
||||||
|
} else if s.LFCount == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return "\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *lineEndingSplitter) ScanLines(data []byte, atEOF bool) (advance int, token []byte, err error) {
|
||||||
|
if atEOF && len(data) == 0 {
|
||||||
|
return 0, nil, nil
|
||||||
|
}
|
||||||
|
if i := bytes.IndexByte(data, '\n'); i >= 0 {
|
||||||
|
// We have a full newline-terminated line.
|
||||||
|
return i + 1, s.dropCR(data[0:i]), nil
|
||||||
|
}
|
||||||
|
// If we're at EOF, we have a final, non-terminated line. Return it.
|
||||||
|
if atEOF {
|
||||||
|
return len(data), data, nil
|
||||||
|
}
|
||||||
|
// Request more data.
|
||||||
|
return 0, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// dropCR drops a terminal \r from the data.
|
||||||
|
func (s *lineEndingSplitter) dropCR(data []byte) []byte {
|
||||||
|
if len(data) > 0 && data[len(data)-1] == '\r' {
|
||||||
|
s.CRLFCount++
|
||||||
|
return data[0 : len(data)-1]
|
||||||
|
}
|
||||||
|
s.LFCount++
|
||||||
|
return data
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestParseLines(t *testing.T) {
|
func TestParseLines(t *testing.T) {
|
||||||
lines, err := ParseLines(strings.NewReader("*.dat filter=lfs"))
|
lines, _, err := ParseLines(strings.NewReader("*.dat filter=lfs"))
|
||||||
|
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Len(t, lines, 1)
|
assert.Len(t, lines, 1)
|
||||||
@ -21,7 +21,7 @@ func TestParseLines(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestParseLinesManyAttrs(t *testing.T) {
|
func TestParseLinesManyAttrs(t *testing.T) {
|
||||||
lines, err := ParseLines(strings.NewReader(
|
lines, _, err := ParseLines(strings.NewReader(
|
||||||
"*.dat filter=lfs diff=lfs merge=lfs -text crlf"))
|
"*.dat filter=lfs diff=lfs merge=lfs -text crlf"))
|
||||||
|
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
@ -38,7 +38,7 @@ func TestParseLinesManyAttrs(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestParseLinesManyLines(t *testing.T) {
|
func TestParseLinesManyLines(t *testing.T) {
|
||||||
lines, err := ParseLines(strings.NewReader(strings.Join([]string{
|
lines, _, err := ParseLines(strings.NewReader(strings.Join([]string{
|
||||||
"*.dat filter=lfs diff=lfs merge=lfs -text",
|
"*.dat filter=lfs diff=lfs merge=lfs -text",
|
||||||
"*.jpg filter=lfs diff=lfs merge=lfs -text",
|
"*.jpg filter=lfs diff=lfs merge=lfs -text",
|
||||||
"# *.pdf filter=lfs diff=lfs merge=lfs -text",
|
"# *.pdf filter=lfs diff=lfs merge=lfs -text",
|
||||||
@ -76,7 +76,7 @@ func TestParseLinesManyLines(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestParseLinesUnset(t *testing.T) {
|
func TestParseLinesUnset(t *testing.T) {
|
||||||
lines, err := ParseLines(strings.NewReader("*.dat -filter"))
|
lines, _, err := ParseLines(strings.NewReader("*.dat -filter"))
|
||||||
|
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Len(t, lines, 1)
|
assert.Len(t, lines, 1)
|
||||||
@ -88,7 +88,7 @@ func TestParseLinesUnset(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestParseLinesUnspecified(t *testing.T) {
|
func TestParseLinesUnspecified(t *testing.T) {
|
||||||
lines, err := ParseLines(strings.NewReader("*.dat !filter"))
|
lines, _, err := ParseLines(strings.NewReader("*.dat !filter"))
|
||||||
|
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Len(t, lines, 1)
|
assert.Len(t, lines, 1)
|
||||||
@ -100,7 +100,7 @@ func TestParseLinesUnspecified(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestParseLinesQuotedPattern(t *testing.T) {
|
func TestParseLinesQuotedPattern(t *testing.T) {
|
||||||
lines, err := ParseLines(strings.NewReader(
|
lines, _, err := ParseLines(strings.NewReader(
|
||||||
"\"space *.dat\" filter=lfs"))
|
"\"space *.dat\" filter=lfs"))
|
||||||
|
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
@ -113,7 +113,7 @@ func TestParseLinesQuotedPattern(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestParseLinesCommented(t *testing.T) {
|
func TestParseLinesCommented(t *testing.T) {
|
||||||
lines, err := ParseLines(strings.NewReader(
|
lines, _, err := ParseLines(strings.NewReader(
|
||||||
"# \"space *.dat\" filter=lfs"))
|
"# \"space *.dat\" filter=lfs"))
|
||||||
|
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
@ -122,7 +122,7 @@ func TestParseLinesCommented(t *testing.T) {
|
|||||||
|
|
||||||
func TestParseLinesUnbalancedQuotes(t *testing.T) {
|
func TestParseLinesUnbalancedQuotes(t *testing.T) {
|
||||||
const text = "\"space *.dat filter=lfs"
|
const text = "\"space *.dat filter=lfs"
|
||||||
lines, err := ParseLines(strings.NewReader(text))
|
lines, _, err := ParseLines(strings.NewReader(text))
|
||||||
|
|
||||||
assert.Empty(t, lines)
|
assert.Empty(t, lines)
|
||||||
assert.EqualError(t, err, fmt.Sprintf(
|
assert.EqualError(t, err, fmt.Sprintf(
|
||||||
@ -130,7 +130,7 @@ func TestParseLinesUnbalancedQuotes(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestParseLinesWithNoAttributes(t *testing.T) {
|
func TestParseLinesWithNoAttributes(t *testing.T) {
|
||||||
lines, err := ParseLines(strings.NewReader("*.dat"))
|
lines, _, err := ParseLines(strings.NewReader("*.dat"))
|
||||||
|
|
||||||
assert.Len(t, lines, 1)
|
assert.Len(t, lines, 1)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
@ -138,3 +138,31 @@ func TestParseLinesWithNoAttributes(t *testing.T) {
|
|||||||
assert.Equal(t, lines[0].Pattern.String(), "*.dat")
|
assert.Equal(t, lines[0].Pattern.String(), "*.dat")
|
||||||
assert.Empty(t, lines[0].Attrs)
|
assert.Empty(t, lines[0].Attrs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParseLinesWithMacros(t *testing.T) {
|
||||||
|
lines, _, err := ParseLines(strings.NewReader(strings.Join([]string{
|
||||||
|
"[attr]lfs filter=lfs diff=lfs merge=lfs -text",
|
||||||
|
"*.dat lfs",
|
||||||
|
"*.txt text"}, "\n")))
|
||||||
|
|
||||||
|
assert.Len(t, lines, 3)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, lines[0].Macro, "lfs")
|
||||||
|
assert.Nil(t, lines[0].Pattern)
|
||||||
|
assert.Len(t, lines[0].Attrs, 4)
|
||||||
|
assert.Equal(t, lines[0].Attrs[0], &Attr{K: "filter", V: "lfs"})
|
||||||
|
assert.Equal(t, lines[0].Attrs[1], &Attr{K: "diff", V: "lfs"})
|
||||||
|
assert.Equal(t, lines[0].Attrs[2], &Attr{K: "merge", V: "lfs"})
|
||||||
|
assert.Equal(t, lines[0].Attrs[3], &Attr{K: "text", V: "false"})
|
||||||
|
|
||||||
|
assert.Equal(t, lines[1].Macro, "")
|
||||||
|
assert.Equal(t, lines[1].Pattern.String(), "*.dat")
|
||||||
|
assert.Len(t, lines[1].Attrs, 1)
|
||||||
|
assert.Equal(t, lines[1].Attrs[0], &Attr{K: "lfs", V: "true"})
|
||||||
|
|
||||||
|
assert.Equal(t, lines[2].Macro, "")
|
||||||
|
assert.Equal(t, lines[2].Pattern.String(), "*.txt")
|
||||||
|
assert.Len(t, lines[2].Attrs, 1)
|
||||||
|
assert.Equal(t, lines[2].Attrs[0], &Attr{K: "text", V: "true"})
|
||||||
|
}
|
||||||
|
56
git/gitattr/macro.go
Normal file
56
git/gitattr/macro.go
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
package gitattr
|
||||||
|
|
||||||
|
type MacroProcessor struct {
|
||||||
|
macros map[string][]*Attr
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMacroProcessor returns a new MacroProcessor object for parsing macros.
|
||||||
|
func NewMacroProcessor() *MacroProcessor {
|
||||||
|
macros := make(map[string][]*Attr)
|
||||||
|
|
||||||
|
// This is built into Git.
|
||||||
|
macros["binary"] = []*Attr{
|
||||||
|
&Attr{K: "diff", V: "false"},
|
||||||
|
&Attr{K: "merge", V: "false"},
|
||||||
|
&Attr{K: "text", V: "false"},
|
||||||
|
}
|
||||||
|
|
||||||
|
return &MacroProcessor{
|
||||||
|
macros: macros,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProcessLines reads the specified lines, returning a new set of lines which
|
||||||
|
// all have a valid pattern. If readMacros is true, it additionally loads any
|
||||||
|
// macro lines as it reads them.
|
||||||
|
func (mp *MacroProcessor) ProcessLines(lines []*Line, readMacros bool) []*Line {
|
||||||
|
result := make([]*Line, 0, len(lines))
|
||||||
|
for _, line := range lines {
|
||||||
|
if line.Pattern != nil {
|
||||||
|
resultLine := Line{
|
||||||
|
Pattern: line.Pattern,
|
||||||
|
Attrs: make([]*Attr, 0, len(line.Attrs)),
|
||||||
|
}
|
||||||
|
for _, attr := range line.Attrs {
|
||||||
|
macros := mp.macros[attr.K]
|
||||||
|
if attr.V == "true" && macros != nil {
|
||||||
|
resultLine.Attrs = append(
|
||||||
|
resultLine.Attrs,
|
||||||
|
macros...,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Git copies through aliases as well as
|
||||||
|
// expanding them.
|
||||||
|
resultLine.Attrs = append(
|
||||||
|
resultLine.Attrs,
|
||||||
|
attr,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
result = append(result, &resultLine)
|
||||||
|
} else if readMacros {
|
||||||
|
mp.macros[line.Macro] = line.Attrs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
126
git/gitattr/macro_test.go
Normal file
126
git/gitattr/macro_test.go
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
package gitattr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestProcessLinesWithMacros(t *testing.T) {
|
||||||
|
lines, _, err := ParseLines(strings.NewReader(strings.Join([]string{
|
||||||
|
"[attr]lfs filter=lfs diff=lfs merge=lfs -text",
|
||||||
|
"*.dat lfs",
|
||||||
|
"*.txt text"}, "\n")))
|
||||||
|
|
||||||
|
assert.Len(t, lines, 3)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
mp := NewMacroProcessor()
|
||||||
|
lines = mp.ProcessLines(lines, true)
|
||||||
|
|
||||||
|
assert.Len(t, lines, 2)
|
||||||
|
|
||||||
|
assert.Equal(t, lines[0].Macro, "")
|
||||||
|
assert.Equal(t, lines[0].Pattern.String(), "*.dat")
|
||||||
|
assert.Len(t, lines[0].Attrs, 5)
|
||||||
|
assert.Equal(t, lines[0].Attrs[0], &Attr{K: "filter", V: "lfs"})
|
||||||
|
assert.Equal(t, lines[0].Attrs[1], &Attr{K: "diff", V: "lfs"})
|
||||||
|
assert.Equal(t, lines[0].Attrs[2], &Attr{K: "merge", V: "lfs"})
|
||||||
|
assert.Equal(t, lines[0].Attrs[3], &Attr{K: "text", V: "false"})
|
||||||
|
assert.Equal(t, lines[0].Attrs[4], &Attr{K: "lfs", V: "true"})
|
||||||
|
|
||||||
|
assert.Equal(t, lines[1].Macro, "")
|
||||||
|
assert.Equal(t, lines[1].Pattern.String(), "*.txt")
|
||||||
|
assert.Len(t, lines[1].Attrs, 1)
|
||||||
|
assert.Equal(t, lines[1].Attrs[0], &Attr{K: "text", V: "true"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProcessLinesWithMacrosDisabled(t *testing.T) {
|
||||||
|
lines, _, err := ParseLines(strings.NewReader(strings.Join([]string{
|
||||||
|
"[attr]lfs filter=lfs diff=lfs merge=lfs -text",
|
||||||
|
"*.dat lfs",
|
||||||
|
"*.txt text"}, "\n")))
|
||||||
|
|
||||||
|
assert.Len(t, lines, 3)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
mp := NewMacroProcessor()
|
||||||
|
lines = mp.ProcessLines(lines, false)
|
||||||
|
|
||||||
|
assert.Len(t, lines, 2)
|
||||||
|
|
||||||
|
assert.Equal(t, lines[0].Macro, "")
|
||||||
|
assert.Equal(t, lines[0].Pattern.String(), "*.dat")
|
||||||
|
assert.Len(t, lines[0].Attrs, 1)
|
||||||
|
assert.Equal(t, lines[0].Attrs[0], &Attr{K: "lfs", V: "true"})
|
||||||
|
|
||||||
|
assert.Equal(t, lines[1].Macro, "")
|
||||||
|
assert.Equal(t, lines[1].Pattern.String(), "*.txt")
|
||||||
|
assert.Len(t, lines[1].Attrs, 1)
|
||||||
|
assert.Equal(t, lines[1].Attrs[0], &Attr{K: "text", V: "true"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProcessLinesWithBinaryMacros(t *testing.T) {
|
||||||
|
lines, _, err := ParseLines(strings.NewReader(strings.Join([]string{
|
||||||
|
"*.dat binary",
|
||||||
|
"*.txt text"}, "\n")))
|
||||||
|
|
||||||
|
assert.Len(t, lines, 2)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
mp := NewMacroProcessor()
|
||||||
|
lines = mp.ProcessLines(lines, true)
|
||||||
|
|
||||||
|
assert.Len(t, lines, 2)
|
||||||
|
|
||||||
|
assert.Equal(t, lines[0].Macro, "")
|
||||||
|
assert.Equal(t, lines[0].Pattern.String(), "*.dat")
|
||||||
|
assert.Len(t, lines[0].Attrs, 4)
|
||||||
|
assert.Equal(t, lines[0].Attrs[0], &Attr{K: "diff", V: "false"})
|
||||||
|
assert.Equal(t, lines[0].Attrs[1], &Attr{K: "merge", V: "false"})
|
||||||
|
assert.Equal(t, lines[0].Attrs[2], &Attr{K: "text", V: "false"})
|
||||||
|
assert.Equal(t, lines[0].Attrs[3], &Attr{K: "binary", V: "true"})
|
||||||
|
|
||||||
|
assert.Equal(t, lines[1].Macro, "")
|
||||||
|
assert.Equal(t, lines[1].Pattern.String(), "*.txt")
|
||||||
|
assert.Len(t, lines[1].Attrs, 1)
|
||||||
|
assert.Equal(t, lines[1].Attrs[0], &Attr{K: "text", V: "true"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProcessLinesIsStateful(t *testing.T) {
|
||||||
|
lines, _, err := ParseLines(strings.NewReader(strings.Join([]string{
|
||||||
|
"[attr]lfs filter=lfs diff=lfs merge=lfs -text",
|
||||||
|
"*.txt text"}, "\n")))
|
||||||
|
|
||||||
|
assert.Len(t, lines, 2)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
mp := NewMacroProcessor()
|
||||||
|
lines = mp.ProcessLines(lines, true)
|
||||||
|
|
||||||
|
assert.Len(t, lines, 1)
|
||||||
|
|
||||||
|
assert.Equal(t, lines[0].Macro, "")
|
||||||
|
assert.Equal(t, lines[0].Pattern.String(), "*.txt")
|
||||||
|
assert.Len(t, lines[0].Attrs, 1)
|
||||||
|
assert.Equal(t, lines[0].Attrs[0], &Attr{K: "text", V: "true"})
|
||||||
|
|
||||||
|
lines2, _, err := ParseLines(strings.NewReader("*.dat lfs\n"))
|
||||||
|
|
||||||
|
assert.Len(t, lines2, 1)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
lines2 = mp.ProcessLines(lines2, false)
|
||||||
|
|
||||||
|
assert.Len(t, lines2, 1)
|
||||||
|
|
||||||
|
assert.Equal(t, lines2[0].Macro, "")
|
||||||
|
assert.Equal(t, lines2[0].Pattern.String(), "*.dat")
|
||||||
|
assert.Len(t, lines2[0].Attrs, 5)
|
||||||
|
assert.Equal(t, lines2[0].Attrs[0], &Attr{K: "filter", V: "lfs"})
|
||||||
|
assert.Equal(t, lines2[0].Attrs[1], &Attr{K: "diff", V: "lfs"})
|
||||||
|
assert.Equal(t, lines2[0].Attrs[2], &Attr{K: "merge", V: "lfs"})
|
||||||
|
assert.Equal(t, lines2[0].Attrs[3], &Attr{K: "text", V: "false"})
|
||||||
|
assert.Equal(t, lines2[0].Attrs[4], &Attr{K: "lfs", V: "true"})
|
||||||
|
}
|
@ -20,7 +20,7 @@ type Tree struct {
|
|||||||
// will be propagated up accordingly.
|
// will be propagated up accordingly.
|
||||||
func New(db *gitobj.ObjectDatabase, t *gitobj.Tree) (*Tree, error) {
|
func New(db *gitobj.ObjectDatabase, t *gitobj.Tree) (*Tree, error) {
|
||||||
children := make(map[string]*Tree)
|
children := make(map[string]*Tree)
|
||||||
lines, err := linesInTree(db, t)
|
lines, _, err := linesInTree(db, t)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -59,7 +59,7 @@ func New(db *gitobj.ObjectDatabase, t *gitobj.Tree) (*Tree, error) {
|
|||||||
// linesInTree parses a given tree's .gitattributes and returns a slice of lines
|
// linesInTree parses a given tree's .gitattributes and returns a slice of lines
|
||||||
// in that .gitattributes, or an error. If no .gitattributes blob was found,
|
// in that .gitattributes, or an error. If no .gitattributes blob was found,
|
||||||
// return nil.
|
// return nil.
|
||||||
func linesInTree(db *gitobj.ObjectDatabase, t *gitobj.Tree) ([]*Line, error) {
|
func linesInTree(db *gitobj.ObjectDatabase, t *gitobj.Tree) ([]*Line, string, error) {
|
||||||
var at int = -1
|
var at int = -1
|
||||||
for i, e := range t.Entries {
|
for i, e := range t.Entries {
|
||||||
if e.Name == ".gitattributes" {
|
if e.Name == ".gitattributes" {
|
||||||
@ -69,12 +69,12 @@ func linesInTree(db *gitobj.ObjectDatabase, t *gitobj.Tree) ([]*Line, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if at < 0 {
|
if at < 0 {
|
||||||
return nil, nil
|
return nil, "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
blob, err := db.Blob(t.Entries[at].Oid)
|
blob, err := db.Blob(t.Entries[at].Oid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
defer blob.Close()
|
defer blob.Close()
|
||||||
|
|
||||||
|
2
go.mod
2
go.mod
@ -5,7 +5,7 @@ require (
|
|||||||
github.com/alexbrainman/sspi v0.0.0-20180125232955-4729b3d4d858
|
github.com/alexbrainman/sspi v0.0.0-20180125232955-4729b3d4d858
|
||||||
github.com/git-lfs/gitobj v1.1.0
|
github.com/git-lfs/gitobj v1.1.0
|
||||||
github.com/git-lfs/go-netrc v0.0.0-20180525200031-e0e9ca483a18
|
github.com/git-lfs/go-netrc v0.0.0-20180525200031-e0e9ca483a18
|
||||||
github.com/git-lfs/wildmatch v1.0.0
|
github.com/git-lfs/wildmatch v1.0.1
|
||||||
github.com/inconshreveable/mousetrap v1.0.0 // indirect
|
github.com/inconshreveable/mousetrap v1.0.0 // indirect
|
||||||
github.com/kr/pty v0.0.0-20150511174710-5cf931ef8f76
|
github.com/kr/pty v0.0.0-20150511174710-5cf931ef8f76
|
||||||
github.com/mattn/go-isatty v0.0.4
|
github.com/mattn/go-isatty v0.0.4
|
||||||
|
4
go.sum
4
go.sum
@ -8,8 +8,8 @@ github.com/git-lfs/gitobj v1.1.0 h1:XRUyk5nKYTWiO8U4cokO5QeoNUNBL8LKS+jXxXZdCTA=
|
|||||||
github.com/git-lfs/gitobj v1.1.0/go.mod h1:EdPNGHVxXe1jTuNXzZT1+CdJCuASoDSLPQuvNOo9nGM=
|
github.com/git-lfs/gitobj v1.1.0/go.mod h1:EdPNGHVxXe1jTuNXzZT1+CdJCuASoDSLPQuvNOo9nGM=
|
||||||
github.com/git-lfs/go-netrc v0.0.0-20180525200031-e0e9ca483a18 h1:7Th0eBA4rT8WJNiM1vppjaIv9W5WJinhpbCJvRJxloI=
|
github.com/git-lfs/go-netrc v0.0.0-20180525200031-e0e9ca483a18 h1:7Th0eBA4rT8WJNiM1vppjaIv9W5WJinhpbCJvRJxloI=
|
||||||
github.com/git-lfs/go-netrc v0.0.0-20180525200031-e0e9ca483a18/go.mod h1:70O4NAtvWn1jW8V8V+OKrJJYcxDLTmIozfi2fmSz5SI=
|
github.com/git-lfs/go-netrc v0.0.0-20180525200031-e0e9ca483a18/go.mod h1:70O4NAtvWn1jW8V8V+OKrJJYcxDLTmIozfi2fmSz5SI=
|
||||||
github.com/git-lfs/wildmatch v1.0.0 h1:TKsxqSrEXWj73N4xGcN/ISal8/JJOiAcOv9LH6Zprxw=
|
github.com/git-lfs/wildmatch v1.0.1 h1:VVoPY+tqog+r8KYfi0kBvlNnGUkToqdWEPFA143EpS8=
|
||||||
github.com/git-lfs/wildmatch v1.0.0/go.mod h1:SdHAGnApDpnFYQ0vAxbniWR0sn7yLJ3QXo9RRfhn2ew=
|
github.com/git-lfs/wildmatch v1.0.1/go.mod h1:SdHAGnApDpnFYQ0vAxbniWR0sn7yLJ3QXo9RRfhn2ew=
|
||||||
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
|
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
|
||||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||||
github.com/kr/pty v0.0.0-20150511174710-5cf931ef8f76 h1:i5TIRQpbCg4aJMUtVHIhkQnSw++Z405Z5pzqHqeNkdU=
|
github.com/kr/pty v0.0.0-20150511174710-5cf931ef8f76 h1:i5TIRQpbCg4aJMUtVHIhkQnSw++Z405Z5pzqHqeNkdU=
|
||||||
|
@ -10,6 +10,7 @@ import (
|
|||||||
"github.com/git-lfs/git-lfs/errors"
|
"github.com/git-lfs/git-lfs/errors"
|
||||||
"github.com/git-lfs/git-lfs/filepathfilter"
|
"github.com/git-lfs/git-lfs/filepathfilter"
|
||||||
"github.com/git-lfs/git-lfs/git"
|
"github.com/git-lfs/git-lfs/git"
|
||||||
|
"github.com/git-lfs/git-lfs/git/gitattr"
|
||||||
"github.com/git-lfs/git-lfs/tools"
|
"github.com/git-lfs/git-lfs/tools"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -39,7 +40,7 @@ func (c *Client) ensureLockablesLoaded() {
|
|||||||
// Internal function to repopulate lockable patterns
|
// Internal function to repopulate lockable patterns
|
||||||
// You must have locked the c.lockableMutex in the caller
|
// You must have locked the c.lockableMutex in the caller
|
||||||
func (c *Client) refreshLockablePatterns() {
|
func (c *Client) refreshLockablePatterns() {
|
||||||
paths := git.GetAttributePaths(c.LocalWorkingDir, c.LocalGitDir)
|
paths := git.GetAttributePaths(gitattr.NewMacroProcessor(), c.LocalWorkingDir, c.LocalGitDir)
|
||||||
// Always make non-nil even if empty
|
// Always make non-nil even if empty
|
||||||
c.lockablePatterns = make([]string, 0, len(paths))
|
c.lockablePatterns = make([]string, 0, len(paths))
|
||||||
for _, p := range paths {
|
for _, p := range paths {
|
||||||
|
86
t/t-attributes.sh
Executable file
86
t/t-attributes.sh
Executable file
@ -0,0 +1,86 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
. "$(dirname "$0")/testlib.sh"
|
||||||
|
|
||||||
|
begin_test "macros"
|
||||||
|
(
|
||||||
|
set -e
|
||||||
|
|
||||||
|
reponame="$(basename "$0" ".sh")"
|
||||||
|
clone_repo "$reponame" repo
|
||||||
|
|
||||||
|
mkdir dir
|
||||||
|
printf '[attr]lfs filter=lfs diff=lfs merge=lfs -text\n*.dat lfs\n' \
|
||||||
|
> .gitattributes
|
||||||
|
printf '[attr]lfs2 filter=lfs diff=lfs merge=lfs -text\n*.bin lfs2\n' \
|
||||||
|
> dir/.gitattributes
|
||||||
|
git add .gitattributes dir
|
||||||
|
git commit -m 'initial import'
|
||||||
|
|
||||||
|
contents="some data"
|
||||||
|
printf "$contents" > foo.dat
|
||||||
|
git add *.dat
|
||||||
|
git commit -m 'foo.dat'
|
||||||
|
assert_local_object "$(calc_oid "$contents")" 9
|
||||||
|
|
||||||
|
contents2="other data"
|
||||||
|
printf "$contents2" > dir/foo.bin
|
||||||
|
git add dir
|
||||||
|
git commit -m 'foo.bin'
|
||||||
|
refute_local_object "$(calc_oid "$contents2")"
|
||||||
|
|
||||||
|
git lfs track '*.dat' 2>&1 | tee track.log
|
||||||
|
grep '"*.dat" already supported' track.log
|
||||||
|
|
||||||
|
git lfs track 'dir/*.bin' 2>&1 | tee track.log
|
||||||
|
! grep '"dir/*.bin" already supported' track.log
|
||||||
|
)
|
||||||
|
end_test
|
||||||
|
|
||||||
|
begin_test "macros with HOME"
|
||||||
|
(
|
||||||
|
set -e
|
||||||
|
|
||||||
|
reponame="$(basename "$0" ".sh")-home"
|
||||||
|
clone_repo "$reponame" repo-home
|
||||||
|
|
||||||
|
mkdir -p "$HOME/.config/git"
|
||||||
|
printf '[attr]lfs filter=lfs diff=lfs merge=lfs -text\n*.dat lfs\n' \
|
||||||
|
> "$HOME/.config/git/attributes"
|
||||||
|
|
||||||
|
contents="some data"
|
||||||
|
printf "$contents" > foo.dat
|
||||||
|
git add *.dat
|
||||||
|
git commit -m 'foo.dat'
|
||||||
|
assert_local_object "$(calc_oid "$contents")" 9
|
||||||
|
|
||||||
|
git lfs track 2>&1 | tee track.log
|
||||||
|
grep '*.dat' track.log
|
||||||
|
)
|
||||||
|
end_test
|
||||||
|
|
||||||
|
begin_test "macros with HOME split"
|
||||||
|
(
|
||||||
|
set -e
|
||||||
|
|
||||||
|
reponame="$(basename "$0" ".sh")-home-split"
|
||||||
|
clone_repo "$reponame" repo-home-split
|
||||||
|
|
||||||
|
mkdir -p "$HOME/.config/git"
|
||||||
|
printf '[attr]lfs filter=lfs diff=lfs merge=lfs -text\n' \
|
||||||
|
> "$HOME/.config/git/attributes"
|
||||||
|
|
||||||
|
printf '*.dat lfs\n' > .gitattributes
|
||||||
|
git add .gitattributes
|
||||||
|
git commit -m 'initial import'
|
||||||
|
|
||||||
|
contents="some data"
|
||||||
|
printf "$contents" > foo.dat
|
||||||
|
git add *.dat
|
||||||
|
git commit -m 'foo.dat'
|
||||||
|
assert_local_object "$(calc_oid "$contents")" 9
|
||||||
|
|
||||||
|
git lfs track '*.dat' 2>&1 | tee track.log
|
||||||
|
grep '"*.dat" already supported' track.log
|
||||||
|
)
|
||||||
|
end_test
|
@ -47,7 +47,7 @@ begin_test "track"
|
|||||||
grep "*.jpg" .gitattributes
|
grep "*.jpg" .gitattributes
|
||||||
|
|
||||||
echo "*.gif -filter -text" >> a/b/.gitattributes
|
echo "*.gif -filter -text" >> a/b/.gitattributes
|
||||||
echo "*.mov -filter=lfs -text" >> a/b/.gitattributes
|
echo "*.mov -filter -text" >> a/b/.gitattributes
|
||||||
|
|
||||||
git lfs track | tee track.log
|
git lfs track | tee track.log
|
||||||
tail -n 3 track.log | head -n 1 | grep "Listing excluded patterns"
|
tail -n 3 track.log | head -n 1 | grep "Listing excluded patterns"
|
||||||
|
@ -127,7 +127,10 @@ var (
|
|||||||
u.HomeDir = os.Getenv("HOME")
|
u.HomeDir = os.Getenv("HOME")
|
||||||
return u, nil
|
return u, nil
|
||||||
}
|
}
|
||||||
lookupUser func(who string) (*user.User, error) = user.Lookup
|
lookupUser func(who string) (*user.User, error) = user.Lookup
|
||||||
|
lookupConfigHome func() string = func() string {
|
||||||
|
return os.Getenv("XDG_CONFIG_HOME")
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// ExpandPath returns a copy of path with any references to the current user's
|
// ExpandPath returns a copy of path with any references to the current user's
|
||||||
@ -179,6 +182,22 @@ func ExpandPath(path string, expand bool) (string, error) {
|
|||||||
return filepath.Join(homedir, path[len(username)+1:]), nil
|
return filepath.Join(homedir, path[len(username)+1:]), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ExpandConfigPath returns a copy of path expanded as with ExpandPath. If the
|
||||||
|
// path is empty, the default path is looked up inside $XDG_CONFIG_HOME, or
|
||||||
|
// ~/.config if that is not set.
|
||||||
|
func ExpandConfigPath(path, defaultPath string) (string, error) {
|
||||||
|
if path != "" {
|
||||||
|
return ExpandPath(path, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
configHome := lookupConfigHome()
|
||||||
|
if configHome != "" {
|
||||||
|
return filepath.Join(configHome, defaultPath), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return ExpandPath(fmt.Sprintf("~/.config/%s", defaultPath), false)
|
||||||
|
}
|
||||||
|
|
||||||
// VerifyFileHash reads a file and verifies whether the SHA is correct
|
// VerifyFileHash reads a file and verifies whether the SHA is correct
|
||||||
// Returns an error if there is a problem
|
// Returns an error if there is a problem
|
||||||
func VerifyFileHash(oid, path string) error {
|
func VerifyFileHash(oid, path string) error {
|
||||||
|
@ -121,6 +121,73 @@ func TestExpandPath(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ExpandConfigPathTestCase struct {
|
||||||
|
Path string
|
||||||
|
DefaultPath string
|
||||||
|
|
||||||
|
Want string
|
||||||
|
WantErr string
|
||||||
|
|
||||||
|
currentUser func() (*user.User, error)
|
||||||
|
lookupConfigHome func() string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ExpandConfigPathTestCase) Assert(t *testing.T) {
|
||||||
|
if c.currentUser != nil {
|
||||||
|
oldCurrentUser := currentUser
|
||||||
|
currentUser = c.currentUser
|
||||||
|
defer func() { currentUser = oldCurrentUser }()
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.lookupConfigHome != nil {
|
||||||
|
oldLookupConfigHome := lookupConfigHome
|
||||||
|
lookupConfigHome = c.lookupConfigHome
|
||||||
|
defer func() { lookupConfigHome = oldLookupConfigHome }()
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := ExpandConfigPath(c.Path, c.DefaultPath)
|
||||||
|
if err != nil || len(c.WantErr) > 0 {
|
||||||
|
assert.EqualError(t, err, c.WantErr)
|
||||||
|
}
|
||||||
|
assert.Equal(t, filepath.ToSlash(c.Want), filepath.ToSlash(got))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExpandConfigPath(t *testing.T) {
|
||||||
|
for desc, c := range map[string]*ExpandConfigPathTestCase{
|
||||||
|
"unexpanded full path": {
|
||||||
|
Path: "/path/to/attributes",
|
||||||
|
Want: "/path/to/attributes",
|
||||||
|
},
|
||||||
|
"expanded full path": {
|
||||||
|
Path: "~/path/to/attributes",
|
||||||
|
Want: "/home/pat/path/to/attributes",
|
||||||
|
currentUser: func() (*user.User, error) {
|
||||||
|
return &user.User{
|
||||||
|
HomeDir: "/home/pat",
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"expanded default path": {
|
||||||
|
DefaultPath: "git/attributes",
|
||||||
|
Want: "/home/pat/.config/git/attributes",
|
||||||
|
currentUser: func() (*user.User, error) {
|
||||||
|
return &user.User{
|
||||||
|
HomeDir: "/home/pat",
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"XDG_CONFIG_HOME set": {
|
||||||
|
DefaultPath: "git/attributes",
|
||||||
|
Want: "/home/pat/configpath/git/attributes",
|
||||||
|
lookupConfigHome: func() string {
|
||||||
|
return "/home/pat/configpath"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(desc, c.Assert)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestFastWalkBasic(t *testing.T) {
|
func TestFastWalkBasic(t *testing.T) {
|
||||||
rootDir, err := ioutil.TempDir(os.TempDir(), "GitLfsTestFastWalkBasic")
|
rootDir, err := ioutil.TempDir(os.TempDir(), "GitLfsTestFastWalkBasic")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
2
vendor/github.com/git-lfs/wildmatch/wildmatch.go
generated
vendored
2
vendor/github.com/git-lfs/wildmatch/wildmatch.go
generated
vendored
@ -85,7 +85,7 @@ func NewWildmatch(p string, opts ...opt) *Wildmatch {
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
// escapes is a constant string containing all escapable characters
|
// escapes is a constant string containing all escapable characters
|
||||||
escapes = "\\[]*?"
|
escapes = "\\[]*?#"
|
||||||
)
|
)
|
||||||
|
|
||||||
// slashEscape converts paths "p" to POSIX-compliant path, independent of which
|
// slashEscape converts paths "p" to POSIX-compliant path, independent of which
|
||||||
|
2
vendor/modules.txt
vendored
2
vendor/modules.txt
vendored
@ -13,7 +13,7 @@ github.com/git-lfs/gitobj/pack
|
|||||||
github.com/git-lfs/gitobj/storage
|
github.com/git-lfs/gitobj/storage
|
||||||
# github.com/git-lfs/go-netrc v0.0.0-20180525200031-e0e9ca483a18
|
# github.com/git-lfs/go-netrc v0.0.0-20180525200031-e0e9ca483a18
|
||||||
github.com/git-lfs/go-netrc/netrc
|
github.com/git-lfs/go-netrc/netrc
|
||||||
# github.com/git-lfs/wildmatch v1.0.0
|
# github.com/git-lfs/wildmatch v1.0.1
|
||||||
github.com/git-lfs/wildmatch
|
github.com/git-lfs/wildmatch
|
||||||
# github.com/inconshreveable/mousetrap v1.0.0
|
# github.com/inconshreveable/mousetrap v1.0.0
|
||||||
github.com/inconshreveable/mousetrap
|
github.com/inconshreveable/mousetrap
|
||||||
|
Loading…
Reference in New Issue
Block a user