aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--plumbing/format/commitgraph/commitgraph.go6
-rw-r--r--plumbing/format/commitgraph/commitgraph_test.go41
-rw-r--r--plumbing/format/commitgraph/encoder.go34
-rw-r--r--plumbing/format/commitgraph/file.go18
-rw-r--r--plumbing/format/commitgraph/memory.go25
-rw-r--r--plumbing/format/gitattributes/attributes.go214
-rw-r--r--plumbing/format/gitattributes/attributes_test.go67
-rw-r--r--plumbing/format/gitattributes/dir.go126
-rw-r--r--plumbing/format/gitattributes/dir_test.go199
-rw-r--r--plumbing/format/gitattributes/matcher.go78
-rw-r--r--plumbing/format/gitattributes/matcher_test.go29
-rw-r--r--plumbing/format/gitattributes/pattern.go101
-rw-r--r--plumbing/format/gitattributes/pattern_test.go229
-rw-r--r--plumbing/object/commit.go7
-rw-r--r--plumbing/object/commit_test.go23
-rw-r--r--plumbing/object/patch.go4
-rw-r--r--plumbing/object/tag.go7
-rw-r--r--plumbing/object/tag_test.go24
18 files changed, 1165 insertions, 67 deletions
diff --git a/plumbing/format/commitgraph/commitgraph.go b/plumbing/format/commitgraph/commitgraph.go
index 9bf7149..e43cd89 100644
--- a/plumbing/format/commitgraph/commitgraph.go
+++ b/plumbing/format/commitgraph/commitgraph.go
@@ -6,9 +6,9 @@ import (
"gopkg.in/src-d/go-git.v4/plumbing"
)
-// Node is a reduced representation of Commit as presented in the commit graph
+// CommitData is a reduced representation of Commit as presented in the commit graph
// file. It is merely useful as an optimization for walking the commit graphs.
-type Node struct {
+type CommitData struct {
// TreeHash is the hash of the root tree of the commit.
TreeHash plumbing.Hash
// ParentIndexes are the indexes of the parent commits of the commit.
@@ -29,7 +29,7 @@ type Index interface {
GetIndexByHash(h plumbing.Hash) (int, error)
// GetNodeByIndex gets the commit node from the commit graph using index
// obtained from child node, if available
- GetNodeByIndex(i int) (*Node, error)
+ GetCommitDataByIndex(i int) (*CommitData, error)
// Hashes returns all the hashes that are available in the index
Hashes() []plumbing.Hash
}
diff --git a/plumbing/format/commitgraph/commitgraph_test.go b/plumbing/format/commitgraph/commitgraph_test.go
index b984142..0e38707 100644
--- a/plumbing/format/commitgraph/commitgraph_test.go
+++ b/plumbing/format/commitgraph/commitgraph_test.go
@@ -32,40 +32,40 @@ func testDecodeHelper(c *C, path string) {
// Root commit
nodeIndex, err := index.GetIndexByHash(plumbing.NewHash("347c91919944a68e9413581a1bc15519550a3afe"))
c.Assert(err, IsNil)
- node, err := index.GetNodeByIndex(nodeIndex)
+ commitData, err := index.GetCommitDataByIndex(nodeIndex)
c.Assert(err, IsNil)
- c.Assert(len(node.ParentIndexes), Equals, 0)
- c.Assert(len(node.ParentHashes), Equals, 0)
+ c.Assert(len(commitData.ParentIndexes), Equals, 0)
+ c.Assert(len(commitData.ParentHashes), Equals, 0)
// Regular commit
nodeIndex, err = index.GetIndexByHash(plumbing.NewHash("e713b52d7e13807e87a002e812041f248db3f643"))
c.Assert(err, IsNil)
- node, err = index.GetNodeByIndex(nodeIndex)
+ commitData, err = index.GetCommitDataByIndex(nodeIndex)
c.Assert(err, IsNil)
- c.Assert(len(node.ParentIndexes), Equals, 1)
- c.Assert(len(node.ParentHashes), Equals, 1)
- c.Assert(node.ParentHashes[0].String(), Equals, "347c91919944a68e9413581a1bc15519550a3afe")
+ c.Assert(len(commitData.ParentIndexes), Equals, 1)
+ c.Assert(len(commitData.ParentHashes), Equals, 1)
+ c.Assert(commitData.ParentHashes[0].String(), Equals, "347c91919944a68e9413581a1bc15519550a3afe")
// Merge commit
nodeIndex, err = index.GetIndexByHash(plumbing.NewHash("b29328491a0682c259bcce28741eac71f3499f7d"))
c.Assert(err, IsNil)
- node, err = index.GetNodeByIndex(nodeIndex)
+ commitData, err = index.GetCommitDataByIndex(nodeIndex)
c.Assert(err, IsNil)
- c.Assert(len(node.ParentIndexes), Equals, 2)
- c.Assert(len(node.ParentHashes), Equals, 2)
- c.Assert(node.ParentHashes[0].String(), Equals, "e713b52d7e13807e87a002e812041f248db3f643")
- c.Assert(node.ParentHashes[1].String(), Equals, "03d2c021ff68954cf3ef0a36825e194a4b98f981")
+ c.Assert(len(commitData.ParentIndexes), Equals, 2)
+ c.Assert(len(commitData.ParentHashes), Equals, 2)
+ c.Assert(commitData.ParentHashes[0].String(), Equals, "e713b52d7e13807e87a002e812041f248db3f643")
+ c.Assert(commitData.ParentHashes[1].String(), Equals, "03d2c021ff68954cf3ef0a36825e194a4b98f981")
// Octopus merge commit
nodeIndex, err = index.GetIndexByHash(plumbing.NewHash("6f6c5d2be7852c782be1dd13e36496dd7ad39560"))
c.Assert(err, IsNil)
- node, err = index.GetNodeByIndex(nodeIndex)
+ commitData, err = index.GetCommitDataByIndex(nodeIndex)
c.Assert(err, IsNil)
- c.Assert(len(node.ParentIndexes), Equals, 3)
- c.Assert(len(node.ParentHashes), Equals, 3)
- c.Assert(node.ParentHashes[0].String(), Equals, "ce275064ad67d51e99f026084e20827901a8361c")
- c.Assert(node.ParentHashes[1].String(), Equals, "bb13916df33ed23004c3ce9ed3b8487528e655c1")
- c.Assert(node.ParentHashes[2].String(), Equals, "a45273fe2d63300e1962a9e26a6b15c276cd7082")
+ c.Assert(len(commitData.ParentIndexes), Equals, 3)
+ c.Assert(len(commitData.ParentHashes), Equals, 3)
+ c.Assert(commitData.ParentHashes[0].String(), Equals, "ce275064ad67d51e99f026084e20827901a8361c")
+ c.Assert(commitData.ParentHashes[1].String(), Equals, "bb13916df33ed23004c3ce9ed3b8487528e655c1")
+ c.Assert(commitData.ParentHashes[2].String(), Equals, "a45273fe2d63300e1962a9e26a6b15c276cd7082")
// Check all hashes
hashes := index.Hashes()
@@ -114,10 +114,9 @@ func (s *CommitgraphSuite) TestReencodeInMemory(c *C) {
c.Assert(err, IsNil)
memoryIndex := commitgraph.NewMemoryIndex()
for i, hash := range index.Hashes() {
- node, err := index.GetNodeByIndex(i)
- c.Assert(err, IsNil)
- err = memoryIndex.Add(hash, node)
+ commitData, err := index.GetCommitDataByIndex(i)
c.Assert(err, IsNil)
+ memoryIndex.Add(hash, commitData)
}
reader.Close()
diff --git a/plumbing/format/commitgraph/encoder.go b/plumbing/format/commitgraph/encoder.go
index 501b09e..648153f 100644
--- a/plumbing/format/commitgraph/encoder.go
+++ b/plumbing/format/commitgraph/encoder.go
@@ -29,13 +29,13 @@ func (e *Encoder) Encode(idx Index) error {
hashes := idx.Hashes()
// Sort the inout and prepare helper structures we'll need for encoding
- hashToIndex, fanout, largeEdgesCount := e.prepare(idx, hashes)
+ hashToIndex, fanout, extraEdgesCount := e.prepare(idx, hashes)
chunkSignatures := [][]byte{oidFanoutSignature, oidLookupSignature, commitDataSignature}
chunkSizes := []uint64{4 * 256, uint64(len(hashes)) * 20, uint64(len(hashes)) * 36}
- if largeEdgesCount > 0 {
- chunkSignatures = append(chunkSignatures, largeEdgeListSignature)
- chunkSizes = append(chunkSizes, uint64(largeEdgesCount)*4)
+ if extraEdgesCount > 0 {
+ chunkSignatures = append(chunkSignatures, extraEdgeListSignature)
+ chunkSizes = append(chunkSizes, uint64(extraEdgesCount)*4)
}
if err = e.encodeFileHeader(len(chunkSignatures)); err != nil {
@@ -50,8 +50,8 @@ func (e *Encoder) Encode(idx Index) error {
if err = e.encodeOidLookup(hashes); err != nil {
return err
}
- if largeEdges, err := e.encodeCommitData(hashes, hashToIndex, idx); err == nil {
- if err = e.encodeLargeEdges(largeEdges); err != nil {
+ if extraEdges, err := e.encodeCommitData(hashes, hashToIndex, idx); err == nil {
+ if err = e.encodeExtraEdges(extraEdges); err != nil {
return err
}
}
@@ -61,7 +61,7 @@ func (e *Encoder) Encode(idx Index) error {
return e.encodeChecksum()
}
-func (e *Encoder) prepare(idx Index, hashes []plumbing.Hash) (hashToIndex map[plumbing.Hash]uint32, fanout []uint32, largeEdgesCount uint32) {
+func (e *Encoder) prepare(idx Index, hashes []plumbing.Hash) (hashToIndex map[plumbing.Hash]uint32, fanout []uint32, extraEdgesCount uint32) {
// Sort the hashes and build our index
plumbing.HashesSort(hashes)
hashToIndex = make(map[plumbing.Hash]uint32)
@@ -76,11 +76,11 @@ func (e *Encoder) prepare(idx Index, hashes []plumbing.Hash) (hashToIndex map[pl
fanout[i] += fanout[i-1]
}
- // Find out if we will need large edge table
+ // Find out if we will need extra edge table
for i := 0; i < len(hashes); i++ {
- v, _ := idx.GetNodeByIndex(i)
+ v, _ := idx.GetCommitDataByIndex(i)
if len(v.ParentHashes) > 2 {
- largeEdgesCount += uint32(len(v.ParentHashes) - 1)
+ extraEdgesCount += uint32(len(v.ParentHashes) - 1)
break
}
}
@@ -131,10 +131,10 @@ func (e *Encoder) encodeOidLookup(hashes []plumbing.Hash) (err error) {
return
}
-func (e *Encoder) encodeCommitData(hashes []plumbing.Hash, hashToIndex map[plumbing.Hash]uint32, idx Index) (largeEdges []uint32, err error) {
+func (e *Encoder) encodeCommitData(hashes []plumbing.Hash, hashToIndex map[plumbing.Hash]uint32, idx Index) (extraEdges []uint32, err error) {
for _, hash := range hashes {
origIndex, _ := idx.GetIndexByHash(hash)
- commitData, _ := idx.GetNodeByIndex(origIndex)
+ commitData, _ := idx.GetCommitDataByIndex(origIndex)
if _, err = e.Write(commitData.TreeHash[:]); err != nil {
return
}
@@ -151,11 +151,11 @@ func (e *Encoder) encodeCommitData(hashes []plumbing.Hash, hashToIndex map[plumb
parent2 = hashToIndex[commitData.ParentHashes[1]]
} else if len(commitData.ParentHashes) > 2 {
parent1 = hashToIndex[commitData.ParentHashes[0]]
- parent2 = uint32(len(largeEdges)) | parentOctopusUsed
+ parent2 = uint32(len(extraEdges)) | parentOctopusUsed
for _, parentHash := range commitData.ParentHashes[1:] {
- largeEdges = append(largeEdges, hashToIndex[parentHash])
+ extraEdges = append(extraEdges, hashToIndex[parentHash])
}
- largeEdges[len(largeEdges)-1] |= parentLast
+ extraEdges[len(extraEdges)-1] |= parentLast
}
if err = binary.WriteUint32(e, parent1); err == nil {
@@ -174,8 +174,8 @@ func (e *Encoder) encodeCommitData(hashes []plumbing.Hash, hashToIndex map[plumb
return
}
-func (e *Encoder) encodeLargeEdges(largeEdges []uint32) (err error) {
- for _, parent := range largeEdges {
+func (e *Encoder) encodeExtraEdges(extraEdges []uint32) (err error) {
+ for _, parent := range extraEdges {
if err = binary.WriteUint32(e, parent); err != nil {
return
}
diff --git a/plumbing/format/commitgraph/file.go b/plumbing/format/commitgraph/file.go
index dce6243..175d279 100644
--- a/plumbing/format/commitgraph/file.go
+++ b/plumbing/format/commitgraph/file.go
@@ -14,11 +14,11 @@ import (
var (
// ErrUnsupportedVersion is returned by OpenFileIndex when the commit graph
// file version is not supported.
- ErrUnsupportedVersion = errors.New("Unsuported version")
+ ErrUnsupportedVersion = errors.New("Unsupported version")
// ErrUnsupportedHash is returned by OpenFileIndex when the commit graph
// hash function is not supported. Currently only SHA-1 is defined and
// supported
- ErrUnsupportedHash = errors.New("Unsuported hash algorithm")
+ ErrUnsupportedHash = errors.New("Unsupported hash algorithm")
// ErrMalformedCommitGraphFile is returned by OpenFileIndex when the commit
// graph file is corrupted.
ErrMalformedCommitGraphFile = errors.New("Malformed commit graph file")
@@ -27,7 +27,7 @@ var (
oidFanoutSignature = []byte{'O', 'I', 'D', 'F'}
oidLookupSignature = []byte{'O', 'I', 'D', 'L'}
commitDataSignature = []byte{'C', 'D', 'A', 'T'}
- largeEdgeListSignature = []byte{'E', 'D', 'G', 'E'}
+ extraEdgeListSignature = []byte{'E', 'D', 'G', 'E'}
lastSignature = []byte{0, 0, 0, 0}
parentNone = uint32(0x70000000)
@@ -42,7 +42,7 @@ type fileIndex struct {
oidFanoutOffset int64
oidLookupOffset int64
commitDataOffset int64
- largeEdgeListOffset int64
+ extraEdgeListOffset int64
}
// OpenFileIndex opens a serialized commit graph file in the format described at
@@ -106,8 +106,8 @@ func (fi *fileIndex) readChunkHeaders() error {
fi.oidLookupOffset = int64(chunkOffset)
} else if bytes.Equal(chunkID, commitDataSignature) {
fi.commitDataOffset = int64(chunkOffset)
- } else if bytes.Equal(chunkID, largeEdgeListSignature) {
- fi.largeEdgeListOffset = int64(chunkOffset)
+ } else if bytes.Equal(chunkID, extraEdgeListSignature) {
+ fi.extraEdgeListOffset = int64(chunkOffset)
} else if bytes.Equal(chunkID, lastSignature) {
break
}
@@ -165,7 +165,7 @@ func (fi *fileIndex) GetIndexByHash(h plumbing.Hash) (int, error) {
return 0, plumbing.ErrObjectNotFound
}
-func (fi *fileIndex) GetNodeByIndex(idx int) (*Node, error) {
+func (fi *fileIndex) GetCommitDataByIndex(idx int) (*CommitData, error) {
if idx >= fi.fanout[0xff] {
return nil, plumbing.ErrObjectNotFound
}
@@ -194,7 +194,7 @@ func (fi *fileIndex) GetNodeByIndex(idx int) (*Node, error) {
if parent2&parentOctopusUsed == parentOctopusUsed {
// Octopus merge
parentIndexes = []int{int(parent1 & parentOctopusMask)}
- offset := fi.largeEdgeListOffset + 4*int64(parent2&parentOctopusMask)
+ offset := fi.extraEdgeListOffset + 4*int64(parent2&parentOctopusMask)
buf := make([]byte, 4)
for {
_, err := fi.reader.ReadAt(buf, offset)
@@ -220,7 +220,7 @@ func (fi *fileIndex) GetNodeByIndex(idx int) (*Node, error) {
return nil, err
}
- return &Node{
+ return &CommitData{
TreeHash: treeHash,
ParentIndexes: parentIndexes,
ParentHashes: parentHashes,
diff --git a/plumbing/format/commitgraph/memory.go b/plumbing/format/commitgraph/memory.go
index 316bc6d..f084b85 100644
--- a/plumbing/format/commitgraph/memory.go
+++ b/plumbing/format/commitgraph/memory.go
@@ -5,7 +5,7 @@ import (
)
type MemoryIndex struct {
- commitData []*Node
+ commitData []*CommitData
indexMap map[plumbing.Hash]int
}
@@ -26,28 +26,28 @@ func (mi *MemoryIndex) GetIndexByHash(h plumbing.Hash) (int, error) {
return 0, plumbing.ErrObjectNotFound
}
-// GetNodeByIndex gets the commit node from the commit graph using index
+// GetCommitDataByIndex gets the commit node from the commit graph using index
// obtained from child node, if available
-func (mi *MemoryIndex) GetNodeByIndex(i int) (*Node, error) {
+func (mi *MemoryIndex) GetCommitDataByIndex(i int) (*CommitData, error) {
if int(i) >= len(mi.commitData) {
return nil, plumbing.ErrObjectNotFound
}
- node := mi.commitData[i]
+ commitData := mi.commitData[i]
// Map parent hashes to parent indexes
- if node.ParentIndexes == nil {
- parentIndexes := make([]int, len(node.ParentHashes))
- for i, parentHash := range node.ParentHashes {
+ if commitData.ParentIndexes == nil {
+ parentIndexes := make([]int, len(commitData.ParentHashes))
+ for i, parentHash := range commitData.ParentHashes {
var err error
if parentIndexes[i], err = mi.GetIndexByHash(parentHash); err != nil {
return nil, err
}
}
- node.ParentIndexes = parentIndexes
+ commitData.ParentIndexes = parentIndexes
}
- return node, nil
+ return commitData, nil
}
// Hashes returns all the hashes that are available in the index
@@ -60,12 +60,11 @@ func (mi *MemoryIndex) Hashes() []plumbing.Hash {
}
// Add adds new node to the memory index
-func (mi *MemoryIndex) Add(hash plumbing.Hash, node *Node) error {
+func (mi *MemoryIndex) Add(hash plumbing.Hash, commitData *CommitData) {
// The parent indexes are calculated lazily in GetNodeByIndex
// which allows adding nodes out of order as long as all parents
// are eventually resolved
- node.ParentIndexes = nil
+ commitData.ParentIndexes = nil
mi.indexMap[hash] = len(mi.commitData)
- mi.commitData = append(mi.commitData, node)
- return nil
+ mi.commitData = append(mi.commitData, commitData)
}
diff --git a/plumbing/format/gitattributes/attributes.go b/plumbing/format/gitattributes/attributes.go
new file mode 100644
index 0000000..d13c2a9
--- /dev/null
+++ b/plumbing/format/gitattributes/attributes.go
@@ -0,0 +1,214 @@
+package gitattributes
+
+import (
+ "errors"
+ "io"
+ "io/ioutil"
+ "strings"
+)
+
+const (
+ commentPrefix = "#"
+ eol = "\n"
+ macroPrefix = "[attr]"
+)
+
+var (
+ ErrMacroNotAllowed = errors.New("macro not allowed")
+ ErrInvalidAttributeName = errors.New("Invalid attribute name")
+)
+
+type MatchAttribute struct {
+ Name string
+ Pattern Pattern
+ Attributes []Attribute
+}
+
+type attributeState byte
+
+const (
+ attributeUnknown attributeState = 0
+ attributeSet attributeState = 1
+ attributeUnspecified attributeState = '!'
+ attributeUnset attributeState = '-'
+ attributeSetValue attributeState = '='
+)
+
+type Attribute interface {
+ Name() string
+ IsSet() bool
+ IsUnset() bool
+ IsUnspecified() bool
+ IsValueSet() bool
+ Value() string
+ String() string
+}
+
+type attribute struct {
+ name string
+ state attributeState
+ value string
+}
+
+func (a attribute) Name() string {
+ return a.name
+}
+
+func (a attribute) IsSet() bool {
+ return a.state == attributeSet
+}
+
+func (a attribute) IsUnset() bool {
+ return a.state == attributeUnset
+}
+
+func (a attribute) IsUnspecified() bool {
+ return a.state == attributeUnspecified
+}
+
+func (a attribute) IsValueSet() bool {
+ return a.state == attributeSetValue
+}
+
+func (a attribute) Value() string {
+ return a.value
+}
+
+func (a attribute) String() string {
+ switch a.state {
+ case attributeSet:
+ return a.name + ": set"
+ case attributeUnset:
+ return a.name + ": unset"
+ case attributeUnspecified:
+ return a.name + ": unspecified"
+ default:
+ return a.name + ": " + a.value
+ }
+}
+
+// ReadAttributes reads patterns and attributes from the gitattributes format.
+func ReadAttributes(r io.Reader, domain []string, allowMacro bool) (attributes []MatchAttribute, err error) {
+ data, err := ioutil.ReadAll(r)
+ if err != nil {
+ return nil, err
+ }
+
+ for _, line := range strings.Split(string(data), eol) {
+ attribute, err := ParseAttributesLine(line, domain, allowMacro)
+ if err != nil {
+ return attributes, err
+ }
+ if len(attribute.Name) == 0 {
+ continue
+ }
+
+ attributes = append(attributes, attribute)
+ }
+
+ return attributes, nil
+}
+
+// ParseAttributesLine parses a gitattribute line, extracting path pattern and
+// attributes.
+func ParseAttributesLine(line string, domain []string, allowMacro bool) (m MatchAttribute, err error) {
+ line = strings.TrimSpace(line)
+
+ if strings.HasPrefix(line, commentPrefix) || len(line) == 0 {
+ return
+ }
+
+ name, unquoted := unquote(line)
+ attrs := strings.Fields(unquoted)
+ if len(name) == 0 {
+ name = attrs[0]
+ attrs = attrs[1:]
+ }
+
+ var macro bool
+ macro, name, err = checkMacro(name, allowMacro)
+ if err != nil {
+ return
+ }
+
+ m.Name = name
+ m.Attributes = make([]Attribute, 0, len(attrs))
+
+ for _, attrName := range attrs {
+ attr := attribute{
+ name: attrName,
+ state: attributeSet,
+ }
+
+ // ! and - prefixes
+ state := attributeState(attr.name[0])
+ if state == attributeUnspecified || state == attributeUnset {
+ attr.state = state
+ attr.name = attr.name[1:]
+ }
+
+ kv := strings.SplitN(attrName, "=", 2)
+ if len(kv) == 2 {
+ attr.name = kv[0]
+ attr.value = kv[1]
+ attr.state = attributeSetValue
+ }
+
+ if !validAttributeName(attr.name) {
+ return m, ErrInvalidAttributeName
+ }
+ m.Attributes = append(m.Attributes, attr)
+ }
+
+ if !macro {
+ m.Pattern = ParsePattern(name, domain)
+ }
+ return
+}
+
+func checkMacro(name string, allowMacro bool) (macro bool, macroName string, err error) {
+ if !strings.HasPrefix(name, macroPrefix) {
+ return false, name, nil
+ }
+ if !allowMacro {
+ return true, name, ErrMacroNotAllowed
+ }
+
+ macroName = name[len(macroPrefix):]
+ if !validAttributeName(macroName) {
+ return true, name, ErrInvalidAttributeName
+ }
+ return true, macroName, nil
+}
+
+func validAttributeName(name string) bool {
+ if len(name) == 0 || name[0] == '-' {
+ return false
+ }
+
+ for _, ch := range name {
+ if !(ch == '-' || ch == '.' || ch == '_' ||
+ ('0' <= ch && ch <= '9') ||
+ ('a' <= ch && ch <= 'z') ||
+ ('A' <= ch && ch <= 'Z')) {
+ return false
+ }
+ }
+ return true
+}
+
+func unquote(str string) (string, string) {
+ if str[0] != '"' {
+ return "", str
+ }
+
+ for i := 1; i < len(str); i++ {
+ switch str[i] {
+ case '\\':
+ i++
+ case '"':
+ return str[1:i], str[i+1:]
+ }
+ }
+ return "", str
+}
diff --git a/plumbing/format/gitattributes/attributes_test.go b/plumbing/format/gitattributes/attributes_test.go
new file mode 100644
index 0000000..aea70ba
--- /dev/null
+++ b/plumbing/format/gitattributes/attributes_test.go
@@ -0,0 +1,67 @@
+package gitattributes
+
+import (
+ "strings"
+
+ . "gopkg.in/check.v1"
+)
+
+type AttributesSuite struct{}
+
+var _ = Suite(&AttributesSuite{})
+
+func (s *AttributesSuite) TestAttributes_ReadAttributes(c *C) {
+ lines := []string{
+ "[attr]sub -a",
+ "[attr]add a",
+ "* sub a",
+ "* !a foo=bar -b c",
+ }
+
+ mas, err := ReadAttributes(strings.NewReader(strings.Join(lines, "\n")), nil, true)
+ c.Assert(err, IsNil)
+ c.Assert(len(mas), Equals, 4)
+
+ c.Assert(mas[0].Name, Equals, "sub")
+ c.Assert(mas[0].Pattern, IsNil)
+ c.Assert(mas[0].Attributes[0].IsUnset(), Equals, true)
+
+ c.Assert(mas[1].Name, Equals, "add")
+ c.Assert(mas[1].Pattern, IsNil)
+ c.Assert(mas[1].Attributes[0].IsSet(), Equals, true)
+
+ c.Assert(mas[2].Name, Equals, "*")
+ c.Assert(mas[2].Pattern, NotNil)
+ c.Assert(mas[2].Attributes[0].IsSet(), Equals, true)
+
+ c.Assert(mas[3].Name, Equals, "*")
+ c.Assert(mas[3].Pattern, NotNil)
+ c.Assert(mas[3].Attributes[0].IsUnspecified(), Equals, true)
+ c.Assert(mas[3].Attributes[1].IsValueSet(), Equals, true)
+ c.Assert(mas[3].Attributes[1].Value(), Equals, "bar")
+ c.Assert(mas[3].Attributes[2].IsUnset(), Equals, true)
+ c.Assert(mas[3].Attributes[3].IsSet(), Equals, true)
+ c.Assert(mas[3].Attributes[0].String(), Equals, "a: unspecified")
+ c.Assert(mas[3].Attributes[1].String(), Equals, "foo: bar")
+ c.Assert(mas[3].Attributes[2].String(), Equals, "b: unset")
+ c.Assert(mas[3].Attributes[3].String(), Equals, "c: set")
+}
+
+func (s *AttributesSuite) TestAttributes_ReadAttributesDisallowMacro(c *C) {
+ lines := []string{
+ "[attr]sub -a",
+ "* a add",
+ }
+
+ _, err := ReadAttributes(strings.NewReader(strings.Join(lines, "\n")), nil, false)
+ c.Assert(err, Equals, ErrMacroNotAllowed)
+}
+
+func (s *AttributesSuite) TestAttributes_ReadAttributesInvalidName(c *C) {
+ lines := []string{
+ "[attr]foo!bar -a",
+ }
+
+ _, err := ReadAttributes(strings.NewReader(strings.Join(lines, "\n")), nil, true)
+ c.Assert(err, Equals, ErrInvalidAttributeName)
+}
diff --git a/plumbing/format/gitattributes/dir.go b/plumbing/format/gitattributes/dir.go
new file mode 100644
index 0000000..d5c1e6a
--- /dev/null
+++ b/plumbing/format/gitattributes/dir.go
@@ -0,0 +1,126 @@
+package gitattributes
+
+import (
+ "os"
+ "os/user"
+
+ "gopkg.in/src-d/go-billy.v4"
+ "gopkg.in/src-d/go-git.v4/plumbing/format/config"
+ gioutil "gopkg.in/src-d/go-git.v4/utils/ioutil"
+)
+
+const (
+ coreSection = "core"
+ attributesfile = "attributesfile"
+ gitDir = ".git"
+ gitattributesFile = ".gitattributes"
+ gitconfigFile = ".gitconfig"
+ systemFile = "/etc/gitconfig"
+)
+
+func ReadAttributesFile(fs billy.Filesystem, path []string, attributesFile string, allowMacro bool) ([]MatchAttribute, error) {
+ f, err := fs.Open(fs.Join(append(path, attributesFile)...))
+ if os.IsNotExist(err) {
+ return nil, nil
+ }
+ if err != nil {
+ return nil, err
+ }
+
+ return ReadAttributes(f, path, allowMacro)
+}
+
+// ReadPatterns reads gitattributes patterns recursively through the directory
+// structure. The result is in ascending order of priority (last higher).
+//
+// The .gitattribute file in the root directory will allow custom macro
+// definitions. Custom macro definitions in other directories .gitattributes
+// will return an error.
+func ReadPatterns(fs billy.Filesystem, path []string) (attributes []MatchAttribute, err error) {
+ attributes, err = ReadAttributesFile(fs, path, gitattributesFile, true)
+ if err != nil {
+ return
+ }
+
+ attrs, err := walkDirectory(fs, path)
+ return append(attributes, attrs...), err
+}
+
+func walkDirectory(fs billy.Filesystem, root []string) (attributes []MatchAttribute, err error) {
+ fis, err := fs.ReadDir(fs.Join(root...))
+ if err != nil {
+ return attributes, err
+ }
+
+ for _, fi := range fis {
+ if !fi.IsDir() || fi.Name() == ".git" {
+ continue
+ }
+
+ path := append(root, fi.Name())
+
+ dirAttributes, err := ReadAttributesFile(fs, path, gitattributesFile, false)
+ if err != nil {
+ return attributes, err
+ }
+
+ subAttributes, err := walkDirectory(fs, path)
+ if err != nil {
+ return attributes, err
+ }
+
+ attributes = append(attributes, append(dirAttributes, subAttributes...)...)
+ }
+
+ return
+}
+
+func loadPatterns(fs billy.Filesystem, path string) ([]MatchAttribute, error) {
+ f, err := fs.Open(path)
+ if os.IsNotExist(err) {
+ return nil, nil
+ }
+ if err != nil {
+ return nil, err
+ }
+ defer gioutil.CheckClose(f, &err)
+
+ raw := config.New()
+ if err = config.NewDecoder(f).Decode(raw); err != nil {
+ return nil, nil
+ }
+
+ path = raw.Section(coreSection).Options.Get(attributesfile)
+ if path == "" {
+ return nil, nil
+ }
+
+ return ReadAttributesFile(fs, nil, path, true)
+}
+
+// LoadGlobalPatterns loads gitattributes patterns and attributes from the
+// gitattributes file declared in a user's ~/.gitconfig file. If the
+// ~/.gitconfig file does not exist the function will return nil. If the
+// core.attributesFile property is not declared, the function will return nil.
+// If the file pointed to by the core.attributesfile property does not exist,
+// the function will return nil. The function assumes fs is rooted at the root
+// filesystem.
+func LoadGlobalPatterns(fs billy.Filesystem) (attributes []MatchAttribute, err error) {
+ usr, err := user.Current()
+ if err != nil {
+ return
+ }
+
+ return loadPatterns(fs, fs.Join(usr.HomeDir, gitconfigFile))
+}
+
+// LoadSystemPatterns loads gitattributes patterns and attributes from the
+// gitattributes file declared in a system's /etc/gitconfig file. If the
+// /etc/gitconfig file does not exist the function will return nil. If the
+// core.attributesfile property is not declared, the function will return nil.
+// If the file pointed to by the core.attributesfile property does not exist,
+// the function will return nil. The function assumes fs is rooted at the root
+// filesystem.
+func LoadSystemPatterns(fs billy.Filesystem) (attributes []MatchAttribute, err error) {
+ return loadPatterns(fs, systemFile)
+}
diff --git a/plumbing/format/gitattributes/dir_test.go b/plumbing/format/gitattributes/dir_test.go
new file mode 100644
index 0000000..34b915d
--- /dev/null
+++ b/plumbing/format/gitattributes/dir_test.go
@@ -0,0 +1,199 @@
+package gitattributes
+
+import (
+ "os"
+ "os/user"
+ "strconv"
+
+ . "gopkg.in/check.v1"
+ "gopkg.in/src-d/go-billy.v4"
+ "gopkg.in/src-d/go-billy.v4/memfs"
+)
+
+type MatcherSuite struct {
+ GFS billy.Filesystem // git repository root
+ RFS billy.Filesystem // root that contains user home
+ MCFS billy.Filesystem // root that contains user home, but missing ~/.gitattributes
+ MEFS billy.Filesystem // root that contains user home, but missing attributesfile entry
+ MIFS billy.Filesystem // root that contains user home, but missing .gitattributes
+
+ SFS billy.Filesystem // root that contains /etc/gitattributes
+}
+
+var _ = Suite(&MatcherSuite{})
+
+func (s *MatcherSuite) SetUpTest(c *C) {
+ // setup root that contains user home
+ usr, err := user.Current()
+ c.Assert(err, IsNil)
+
+ gitAttributesGlobal := func(fs billy.Filesystem, filename string) {
+ f, err := fs.Create(filename)
+ c.Assert(err, IsNil)
+ _, err = f.Write([]byte("# IntelliJ\n"))
+ c.Assert(err, IsNil)
+ _, err = f.Write([]byte(".idea/** text\n"))
+ c.Assert(err, IsNil)
+ _, err = f.Write([]byte("*.iml -text\n"))
+ c.Assert(err, IsNil)
+ err = f.Close()
+ c.Assert(err, IsNil)
+ }
+
+ // setup generic git repository root
+ fs := memfs.New()
+ f, err := fs.Create(".gitattributes")
+ c.Assert(err, IsNil)
+ _, err = f.Write([]byte("vendor/g*/** foo=bar\n"))
+ c.Assert(err, IsNil)
+ err = f.Close()
+ c.Assert(err, IsNil)
+
+ err = fs.MkdirAll("vendor", os.ModePerm)
+ c.Assert(err, IsNil)
+ f, err = fs.Create("vendor/.gitattributes")
+ c.Assert(err, IsNil)
+ _, err = f.Write([]byte("github.com/** -foo\n"))
+ c.Assert(err, IsNil)
+ err = f.Close()
+ c.Assert(err, IsNil)
+
+ fs.MkdirAll("another", os.ModePerm)
+ fs.MkdirAll("vendor/github.com", os.ModePerm)
+ fs.MkdirAll("vendor/gopkg.in", os.ModePerm)
+
+ gitAttributesGlobal(fs, fs.Join(usr.HomeDir, ".gitattributes_global"))
+
+ s.GFS = fs
+
+ fs = memfs.New()
+ err = fs.MkdirAll(usr.HomeDir, os.ModePerm)
+ c.Assert(err, IsNil)
+
+ f, err = fs.Create(fs.Join(usr.HomeDir, gitconfigFile))
+ c.Assert(err, IsNil)
+ _, err = f.Write([]byte("[core]\n"))
+ c.Assert(err, IsNil)
+ _, err = f.Write([]byte(" attributesfile = " + strconv.Quote(fs.Join(usr.HomeDir, ".gitattributes_global")) + "\n"))
+ c.Assert(err, IsNil)
+ err = f.Close()
+ c.Assert(err, IsNil)
+
+ gitAttributesGlobal(fs, fs.Join(usr.HomeDir, ".gitattributes_global"))
+
+ s.RFS = fs
+
+ // root that contains user home, but missing ~/.gitconfig
+ fs = memfs.New()
+ gitAttributesGlobal(fs, fs.Join(usr.HomeDir, ".gitattributes_global"))
+
+ s.MCFS = fs
+
+ // setup root that contains user home, but missing attributesfile entry
+ fs = memfs.New()
+ err = fs.MkdirAll(usr.HomeDir, os.ModePerm)
+ c.Assert(err, IsNil)
+
+ f, err = fs.Create(fs.Join(usr.HomeDir, gitconfigFile))
+ c.Assert(err, IsNil)
+ _, err = f.Write([]byte("[core]\n"))
+ c.Assert(err, IsNil)
+ err = f.Close()
+ c.Assert(err, IsNil)
+
+ gitAttributesGlobal(fs, fs.Join(usr.HomeDir, ".gitattributes_global"))
+
+ s.MEFS = fs
+
+ // setup root that contains user home, but missing .gitattributes
+ fs = memfs.New()
+ err = fs.MkdirAll(usr.HomeDir, os.ModePerm)
+ c.Assert(err, IsNil)
+
+ f, err = fs.Create(fs.Join(usr.HomeDir, gitconfigFile))
+ c.Assert(err, IsNil)
+ _, err = f.Write([]byte("[core]\n"))
+ c.Assert(err, IsNil)
+ _, err = f.Write([]byte(" attributesfile = " + strconv.Quote(fs.Join(usr.HomeDir, ".gitattributes_global")) + "\n"))
+ c.Assert(err, IsNil)
+ err = f.Close()
+ c.Assert(err, IsNil)
+
+ s.MIFS = fs
+
+ // setup root that contains user home
+ fs = memfs.New()
+ err = fs.MkdirAll("etc", os.ModePerm)
+ c.Assert(err, IsNil)
+
+ f, err = fs.Create(systemFile)
+ c.Assert(err, IsNil)
+ _, err = f.Write([]byte("[core]\n"))
+ c.Assert(err, IsNil)
+ _, err = f.Write([]byte(" attributesfile = /etc/gitattributes_global\n"))
+ c.Assert(err, IsNil)
+ err = f.Close()
+ c.Assert(err, IsNil)
+
+ gitAttributesGlobal(fs, "/etc/gitattributes_global")
+
+ s.SFS = fs
+}
+
+func (s *MatcherSuite) TestDir_ReadPatterns(c *C) {
+ ps, err := ReadPatterns(s.GFS, nil)
+ c.Assert(err, IsNil)
+ c.Assert(ps, HasLen, 2)
+
+ m := NewMatcher(ps)
+ results, _ := m.Match([]string{"vendor", "gopkg.in", "file"}, nil)
+ c.Assert(results["foo"].Value(), Equals, "bar")
+
+ results, _ = m.Match([]string{"vendor", "github.com", "file"}, nil)
+ c.Assert(results["foo"].IsUnset(), Equals, false)
+}
+
+func (s *MatcherSuite) TestDir_LoadGlobalPatterns(c *C) {
+ ps, err := LoadGlobalPatterns(s.RFS)
+ c.Assert(err, IsNil)
+ c.Assert(ps, HasLen, 2)
+
+ m := NewMatcher(ps)
+
+ results, _ := m.Match([]string{"go-git.v4.iml"}, nil)
+ c.Assert(results["text"].IsUnset(), Equals, true)
+
+ results, _ = m.Match([]string{".idea", "file"}, nil)
+ c.Assert(results["text"].IsSet(), Equals, true)
+}
+
+func (s *MatcherSuite) TestDir_LoadGlobalPatternsMissingGitconfig(c *C) {
+ ps, err := LoadGlobalPatterns(s.MCFS)
+ c.Assert(err, IsNil)
+ c.Assert(ps, HasLen, 0)
+}
+
+func (s *MatcherSuite) TestDir_LoadGlobalPatternsMissingAttributesfile(c *C) {
+ ps, err := LoadGlobalPatterns(s.MEFS)
+ c.Assert(err, IsNil)
+ c.Assert(ps, HasLen, 0)
+}
+
+func (s *MatcherSuite) TestDir_LoadGlobalPatternsMissingGitattributes(c *C) {
+ ps, err := LoadGlobalPatterns(s.MIFS)
+ c.Assert(err, IsNil)
+ c.Assert(ps, HasLen, 0)
+}
+
+func (s *MatcherSuite) TestDir_LoadSystemPatterns(c *C) {
+ ps, err := LoadSystemPatterns(s.SFS)
+ c.Assert(err, IsNil)
+ c.Assert(ps, HasLen, 2)
+
+ m := NewMatcher(ps)
+ results, _ := m.Match([]string{"go-git.v4.iml"}, nil)
+ c.Assert(results["text"].IsUnset(), Equals, true)
+
+ results, _ = m.Match([]string{".idea", "file"}, nil)
+ c.Assert(results["text"].IsSet(), Equals, true)
+}
diff --git a/plumbing/format/gitattributes/matcher.go b/plumbing/format/gitattributes/matcher.go
new file mode 100644
index 0000000..df12864
--- /dev/null
+++ b/plumbing/format/gitattributes/matcher.go
@@ -0,0 +1,78 @@
+package gitattributes
+
+// Matcher defines a global multi-pattern matcher for gitattributes patterns
+type Matcher interface {
+ // Match matches patterns in the order of priorities.
+ Match(path []string, attributes []string) (map[string]Attribute, bool)
+}
+
+type MatcherOptions struct{}
+
+// NewMatcher constructs a new matcher. Patterns must be given in the order of
+// increasing priority. That is the most generic settings files first, then the
+// content of the repo .gitattributes, then content of .gitattributes down the
+// path.
+func NewMatcher(stack []MatchAttribute) Matcher {
+ m := &matcher{stack: stack}
+ m.init()
+
+ return m
+}
+
+type matcher struct {
+ stack []MatchAttribute
+ macros map[string]MatchAttribute
+}
+
+func (m *matcher) init() {
+ m.macros = make(map[string]MatchAttribute)
+
+ for _, attr := range m.stack {
+ if attr.Pattern == nil {
+ m.macros[attr.Name] = attr
+ }
+ }
+}
+
+// Match matches path against the patterns in gitattributes files and returns
+// the attributes associated with the path.
+//
+// Specific attributes can be specified otherwise all attributes are returned.
+//
+// Matched is true if any path was matched to a rule, even if the results map
+// is empty.
+func (m *matcher) Match(path []string, attributes []string) (results map[string]Attribute, matched bool) {
+ results = make(map[string]Attribute, len(attributes))
+
+ n := len(m.stack)
+ for i := n - 1; i >= 0; i-- {
+ if len(attributes) > 0 && len(attributes) == len(results) {
+ return
+ }
+
+ pattern := m.stack[i].Pattern
+ if pattern == nil {
+ continue
+ }
+
+ if match := pattern.Match(path); match {
+ matched = true
+ for _, attr := range m.stack[i].Attributes {
+ if attr.IsSet() {
+ m.expandMacro(attr.Name(), results)
+ }
+ results[attr.Name()] = attr
+ }
+ }
+ }
+ return
+}
+
+func (m *matcher) expandMacro(name string, results map[string]Attribute) bool {
+ if macro, ok := m.macros[name]; ok {
+ for _, attr := range macro.Attributes {
+ results[attr.Name()] = attr
+ }
+ }
+ return false
+}
diff --git a/plumbing/format/gitattributes/matcher_test.go b/plumbing/format/gitattributes/matcher_test.go
new file mode 100644
index 0000000..edb71a1
--- /dev/null
+++ b/plumbing/format/gitattributes/matcher_test.go
@@ -0,0 +1,29 @@
+package gitattributes
+
+import (
+ "strings"
+
+ . "gopkg.in/check.v1"
+)
+
+func (s *MatcherSuite) TestMatcher_Match(c *C) {
+ lines := []string{
+ "[attr]binary -diff -merge -text",
+ "**/middle/v[uo]l?ano binary text eol=crlf",
+ "volcano -eol",
+ "foobar diff merge text eol=lf foo=bar",
+ }
+
+ ma, err := ReadAttributes(strings.NewReader(strings.Join(lines, "\n")), nil, true)
+ c.Assert(err, IsNil)
+
+ m := NewMatcher(ma)
+ results, matched := m.Match([]string{"head", "middle", "vulkano"}, nil)
+
+ c.Assert(matched, Equals, true)
+ c.Assert(results["binary"].IsSet(), Equals, true)
+ c.Assert(results["diff"].IsUnset(), Equals, true)
+ c.Assert(results["merge"].IsUnset(), Equals, true)
+ c.Assert(results["text"].IsSet(), Equals, true)
+ c.Assert(results["eol"].Value(), Equals, "crlf")
+}
diff --git a/plumbing/format/gitattributes/pattern.go b/plumbing/format/gitattributes/pattern.go
new file mode 100644
index 0000000..c5ca0c7
--- /dev/null
+++ b/plumbing/format/gitattributes/pattern.go
@@ -0,0 +1,101 @@
+package gitattributes
+
+import (
+ "path/filepath"
+ "strings"
+)
+
+const (
+ patternDirSep = "/"
+ zeroToManyDirs = "**"
+)
+
+// Pattern defines a gitattributes pattern.
+type Pattern interface {
+ // Match matches the given path to the pattern.
+ Match(path []string) bool
+}
+
+type pattern struct {
+ domain []string
+ pattern []string
+}
+
+// ParsePattern parses a gitattributes pattern string into the Pattern
+// structure.
+func ParsePattern(p string, domain []string) Pattern {
+ return &pattern{
+ domain: domain,
+ pattern: strings.Split(p, patternDirSep),
+ }
+}
+
+func (p *pattern) Match(path []string) bool {
+ if len(path) <= len(p.domain) {
+ return false
+ }
+ for i, e := range p.domain {
+ if path[i] != e {
+ return false
+ }
+ }
+
+ if len(p.pattern) == 1 {
+ // for a simple rule, .gitattribute matching rules differs from
+ // .gitignore and only the last part of the path is considered.
+ path = path[len(path)-1:]
+ } else {
+ path = path[len(p.domain):]
+ }
+
+ pattern := p.pattern
+ var match, doublestar bool
+ var err error
+ for _, part := range path {
+ // skip empty
+ if pattern[0] == "" {
+ pattern = pattern[1:]
+ }
+
+ // eat doublestar
+ if pattern[0] == zeroToManyDirs {
+ pattern = pattern[1:]
+ if len(pattern) == 0 {
+ return true
+ }
+ doublestar = true
+ }
+
+ switch true {
+ case strings.Contains(pattern[0], "**"):
+ return false
+
+ // keep going down the path until we hit a match
+ case doublestar:
+ match, err = filepath.Match(pattern[0], part)
+ if err != nil {
+ return false
+ }
+
+ if match {
+ doublestar = false
+ pattern = pattern[1:]
+ }
+
+ default:
+ match, err = filepath.Match(pattern[0], part)
+ if err != nil {
+ return false
+ }
+ if !match {
+ return false
+ }
+ pattern = pattern[1:]
+ }
+ }
+
+ if len(pattern) > 0 {
+ return false
+ }
+ return match
+}
diff --git a/plumbing/format/gitattributes/pattern_test.go b/plumbing/format/gitattributes/pattern_test.go
new file mode 100644
index 0000000..f95be6e
--- /dev/null
+++ b/plumbing/format/gitattributes/pattern_test.go
@@ -0,0 +1,229 @@
+package gitattributes
+
+import (
+ "testing"
+
+ . "gopkg.in/check.v1"
+)
+
+func Test(t *testing.T) { TestingT(t) }
+
+type PatternSuite struct{}
+
+var _ = Suite(&PatternSuite{})
+
+func (s *PatternSuite) TestMatch_domainLonger_mismatch(c *C) {
+ p := ParsePattern("value", []string{"head", "middle", "tail"})
+ r := p.Match([]string{"head", "middle"})
+ c.Assert(r, Equals, false)
+}
+
+func (s *PatternSuite) TestMatch_domainSameLength_mismatch(c *C) {
+ p := ParsePattern("value", []string{"head", "middle", "tail"})
+ r := p.Match([]string{"head", "middle", "tail"})
+ c.Assert(r, Equals, false)
+}
+
+func (s *PatternSuite) TestMatch_domainMismatch_mismatch(c *C) {
+ p := ParsePattern("value", []string{"head", "middle", "tail"})
+ r := p.Match([]string{"head", "middle", "_tail_", "value"})
+ c.Assert(r, Equals, false)
+}
+
+func (s *PatternSuite) TestSimpleMatch_match(c *C) {
+ p := ParsePattern("vul?ano", nil)
+ r := p.Match([]string{"value", "vulkano"})
+ c.Assert(r, Equals, true)
+}
+
+func (s *PatternSuite) TestSimpleMatch_withDomain(c *C) {
+ p := ParsePattern("middle/tail", []string{"value", "volcano"})
+ r := p.Match([]string{"value", "volcano", "middle", "tail"})
+ c.Assert(r, Equals, true)
+}
+
+func (s *PatternSuite) TestSimpleMatch_onlyMatchInDomain_mismatch(c *C) {
+ p := ParsePattern("value/volcano", []string{"value", "volcano"})
+ r := p.Match([]string{"value", "volcano", "tail"})
+ c.Assert(r, Equals, false)
+}
+
+func (s *PatternSuite) TestSimpleMatch_atStart(c *C) {
+ p := ParsePattern("value", nil)
+ r := p.Match([]string{"value", "tail"})
+ c.Assert(r, Equals, false)
+}
+
+func (s *PatternSuite) TestSimpleMatch_inTheMiddle(c *C) {
+ p := ParsePattern("value", nil)
+ r := p.Match([]string{"head", "value", "tail"})
+ c.Assert(r, Equals, false)
+}
+
+func (s *PatternSuite) TestSimpleMatch_atEnd(c *C) {
+ p := ParsePattern("value", nil)
+ r := p.Match([]string{"head", "value"})
+ c.Assert(r, Equals, true)
+}
+
+func (s *PatternSuite) TestSimpleMatch_mismatch(c *C) {
+ p := ParsePattern("value", nil)
+ r := p.Match([]string{"head", "val", "tail"})
+ c.Assert(r, Equals, false)
+}
+
+func (s *PatternSuite) TestSimpleMatch_valueLonger_mismatch(c *C) {
+ p := ParsePattern("tai", nil)
+ r := p.Match([]string{"head", "value", "tail"})
+ c.Assert(r, Equals, false)
+}
+
+func (s *PatternSuite) TestSimpleMatch_withAsterisk(c *C) {
+ p := ParsePattern("t*l", nil)
+ r := p.Match([]string{"value", "vulkano", "tail"})
+ c.Assert(r, Equals, true)
+}
+
+func (s *PatternSuite) TestSimpleMatch_withQuestionMark(c *C) {
+ p := ParsePattern("ta?l", nil)
+ r := p.Match([]string{"value", "vulkano", "tail"})
+ c.Assert(r, Equals, true)
+}
+
+func (s *PatternSuite) TestSimpleMatch_magicChars(c *C) {
+ p := ParsePattern("v[ou]l[kc]ano", nil)
+ r := p.Match([]string{"value", "volcano"})
+ c.Assert(r, Equals, true)
+}
+
+func (s *PatternSuite) TestSimpleMatch_wrongPattern_mismatch(c *C) {
+ p := ParsePattern("v[ou]l[", nil)
+ r := p.Match([]string{"value", "vol["})
+ c.Assert(r, Equals, false)
+}
+
+func (s *PatternSuite) TestGlobMatch_fromRootWithSlash(c *C) {
+ p := ParsePattern("/value/vul?ano/tail", nil)
+ r := p.Match([]string{"value", "vulkano", "tail"})
+ c.Assert(r, Equals, true)
+}
+
+func (s *PatternSuite) TestGlobMatch_withDomain(c *C) {
+ p := ParsePattern("middle/tail", []string{"value", "volcano"})
+ r := p.Match([]string{"value", "volcano", "middle", "tail"})
+ c.Assert(r, Equals, true)
+}
+
+func (s *PatternSuite) TestGlobMatch_onlyMatchInDomain_mismatch(c *C) {
+ p := ParsePattern("volcano/tail", []string{"value", "volcano"})
+ r := p.Match([]string{"value", "volcano", "tail"})
+ c.Assert(r, Equals, false)
+}
+
+func (s *PatternSuite) TestGlobMatch_fromRootWithoutSlash(c *C) {
+ p := ParsePattern("value/vul?ano/tail", nil)
+ r := p.Match([]string{"value", "vulkano", "tail"})
+ c.Assert(r, Equals, true)
+}
+
+func (s *PatternSuite) TestGlobMatch_fromRoot_mismatch(c *C) {
+ p := ParsePattern("value/vulkano", nil)
+ r := p.Match([]string{"value", "volcano"})
+ c.Assert(r, Equals, false)
+}
+
+func (s *PatternSuite) TestGlobMatch_fromRoot_tooShort_mismatch(c *C) {
+ p := ParsePattern("value/vul?ano", nil)
+ r := p.Match([]string{"value"})
+ c.Assert(r, Equals, false)
+}
+
+func (s *PatternSuite) TestGlobMatch_fromRoot_notAtRoot_mismatch(c *C) {
+ p := ParsePattern("/value/volcano", nil)
+ r := p.Match([]string{"value", "value", "volcano"})
+ c.Assert(r, Equals, false)
+}
+
+func (s *PatternSuite) TestGlobMatch_leadingAsterisks_atStart(c *C) {
+ p := ParsePattern("**/*lue/vol?ano/ta?l", nil)
+ r := p.Match([]string{"value", "volcano", "tail"})
+ c.Assert(r, Equals, true)
+}
+
+func (s *PatternSuite) TestGlobMatch_leadingAsterisks_notAtStart(c *C) {
+ p := ParsePattern("**/*lue/vol?ano/tail", nil)
+ r := p.Match([]string{"head", "value", "volcano", "tail"})
+ c.Assert(r, Equals, true)
+}
+
+func (s *PatternSuite) TestGlobMatch_leadingAsterisks_mismatch(c *C) {
+ p := ParsePattern("**/*lue/vol?ano/tail", nil)
+ r := p.Match([]string{"head", "value", "Volcano", "tail"})
+ c.Assert(r, Equals, false)
+}
+
+func (s *PatternSuite) TestGlobMatch_tailingAsterisks(c *C) {
+ p := ParsePattern("/*lue/vol?ano/**", nil)
+ r := p.Match([]string{"value", "volcano", "tail", "moretail"})
+ c.Assert(r, Equals, true)
+}
+
+func (s *PatternSuite) TestGlobMatch_tailingAsterisks_single(c *C) {
+ p := ParsePattern("/*lue/**", nil)
+ r := p.Match([]string{"value", "volcano"})
+ c.Assert(r, Equals, true)
+}
+
+func (s *PatternSuite) TestGlobMatch_tailingAsterisks_exactMatch(c *C) {
+ p := ParsePattern("/*lue/vol?ano/**", nil)
+ r := p.Match([]string{"value", "volcano"})
+ c.Assert(r, Equals, false)
+}
+
+func (s *PatternSuite) TestGlobMatch_middleAsterisks_emptyMatch(c *C) {
+ p := ParsePattern("/*lue/**/vol?ano", nil)
+ r := p.Match([]string{"value", "volcano"})
+ c.Assert(r, Equals, true)
+}
+
+func (s *PatternSuite) TestGlobMatch_middleAsterisks_oneMatch(c *C) {
+ p := ParsePattern("/*lue/**/vol?ano", nil)
+ r := p.Match([]string{"value", "middle", "volcano"})
+ c.Assert(r, Equals, true)
+}
+
+func (s *PatternSuite) TestGlobMatch_middleAsterisks_multiMatch(c *C) {
+ p := ParsePattern("/*lue/**/vol?ano", nil)
+ r := p.Match([]string{"value", "middle1", "middle2", "volcano"})
+ c.Assert(r, Equals, true)
+}
+
+func (s *PatternSuite) TestGlobMatch_wrongDoubleAsterisk_mismatch(c *C) {
+ p := ParsePattern("/*lue/**foo/vol?ano/tail", nil)
+ r := p.Match([]string{"value", "foo", "volcano", "tail"})
+ c.Assert(r, Equals, false)
+}
+
+func (s *PatternSuite) TestGlobMatch_magicChars(c *C) {
+ p := ParsePattern("**/head/v[ou]l[kc]ano", nil)
+ r := p.Match([]string{"value", "head", "volcano"})
+ c.Assert(r, Equals, true)
+}
+
+func (s *PatternSuite) TestGlobMatch_wrongPattern_noTraversal_mismatch(c *C) {
+ p := ParsePattern("**/head/v[ou]l[", nil)
+ r := p.Match([]string{"value", "head", "vol["})
+ c.Assert(r, Equals, false)
+}
+
+func (s *PatternSuite) TestGlobMatch_wrongPattern_onTraversal_mismatch(c *C) {
+ p := ParsePattern("/value/**/v[ou]l[", nil)
+ r := p.Match([]string{"value", "head", "vol["})
+ c.Assert(r, Equals, false)
+}
+
+func (s *PatternSuite) TestGlobMatch_issue_923(c *C) {
+ p := ParsePattern("**/android/**/GeneratedPluginRegistrant.java", nil)
+ r := p.Match([]string{"packages", "flutter_tools", "lib", "src", "android", "gradle.dart"})
+ c.Assert(r, Equals, false)
+}
diff --git a/plumbing/object/commit.go b/plumbing/object/commit.go
index 511242d..6b50934 100644
--- a/plumbing/object/commit.go
+++ b/plumbing/object/commit.go
@@ -235,6 +235,11 @@ func (b *Commit) Encode(o plumbing.EncodedObject) error {
return b.encode(o, true)
}
+// EncodeWithoutSignature export a Commit into a plumbing.EncodedObject without the signature (correspond to the payload of the PGP signature).
+func (b *Commit) EncodeWithoutSignature(o plumbing.EncodedObject) error {
+ return b.encode(o, false)
+}
+
func (b *Commit) encode(o plumbing.EncodedObject, includeSig bool) (err error) {
o.SetType(plumbing.CommitObject)
w, err := o.Writer()
@@ -349,7 +354,7 @@ func (c *Commit) Verify(armoredKeyRing string) (*openpgp.Entity, error) {
encoded := &plumbing.MemoryObject{}
// Encode commit components, excluding signature and get a reader object.
- if err := c.encode(encoded, false); err != nil {
+ if err := c.EncodeWithoutSignature(encoded); err != nil {
return nil, err
}
er, err := encoded.Reader()
diff --git a/plumbing/object/commit_test.go b/plumbing/object/commit_test.go
index c9acf42..957e7d6 100644
--- a/plumbing/object/commit_test.go
+++ b/plumbing/object/commit_test.go
@@ -4,14 +4,15 @@ import (
"bytes"
"context"
"io"
+ "io/ioutil"
"strings"
"time"
+ fixtures "gopkg.in/src-d/go-git-fixtures.v3"
"gopkg.in/src-d/go-git.v4/plumbing"
"gopkg.in/src-d/go-git.v4/plumbing/cache"
. "gopkg.in/check.v1"
- "gopkg.in/src-d/go-git-fixtures.v3"
"gopkg.in/src-d/go-git.v4/storage/filesystem"
)
@@ -495,3 +496,23 @@ func (s *SuiteCommit) TestMalformedHeader(c *C) {
err = decoded.Decode(encoded)
c.Assert(err, IsNil)
}
+
+func (s *SuiteCommit) TestEncodeWithoutSignature(c *C) {
+ //Similar to TestString since no signature
+ encoded := &plumbing.MemoryObject{}
+ err := s.Commit.EncodeWithoutSignature(encoded)
+ c.Assert(err, IsNil)
+ er, err := encoded.Reader()
+ c.Assert(err, IsNil)
+ payload, err := ioutil.ReadAll(er)
+ c.Assert(err, IsNil)
+
+ c.Assert(string(payload), Equals, ""+
+ "tree eba74343e2f15d62adedfd8c883ee0262b5c8021\n"+
+ "parent 35e85108805c84807bc66a02d91535e1e24b38b9\n"+
+ "parent a5b8b09e2f8fcb0bb99d3ccb0958157b40890d69\n"+
+ "author Máximo Cuadros Ortiz <mcuadros@gmail.com> 1427802494 +0200\n"+
+ "committer Máximo Cuadros Ortiz <mcuadros@gmail.com> 1427802494 +0200\n"+
+ "\n"+
+ "Merge branch 'master' of github.com:tyba/git-fixture\n")
+}
diff --git a/plumbing/object/patch.go b/plumbing/object/patch.go
index 068589e..1efd0b1 100644
--- a/plumbing/object/patch.go
+++ b/plumbing/object/patch.go
@@ -321,6 +321,10 @@ func getFileStatsFromFilePatches(filePatches []fdiff.FilePatch) FileStats {
for _, chunk := range fp.Chunks() {
s := chunk.Content()
+ if len(s) == 0 {
+ continue
+ }
+
switch chunk.Type() {
case fdiff.Add:
cs.Addition += strings.Count(s, "\n")
diff --git a/plumbing/object/tag.go b/plumbing/object/tag.go
index bc03477..9ee5509 100644
--- a/plumbing/object/tag.go
+++ b/plumbing/object/tag.go
@@ -171,6 +171,11 @@ func (t *Tag) Encode(o plumbing.EncodedObject) error {
return t.encode(o, true)
}
+// EncodeWithoutSignature export a Tag into a plumbing.EncodedObject without the signature (correspond to the payload of the PGP signature).
+func (t *Tag) EncodeWithoutSignature(o plumbing.EncodedObject) error {
+ return t.encode(o, false)
+}
+
func (t *Tag) encode(o plumbing.EncodedObject, includeSig bool) (err error) {
o.SetType(plumbing.TagObject)
w, err := o.Writer()
@@ -291,7 +296,7 @@ func (t *Tag) Verify(armoredKeyRing string) (*openpgp.Entity, error) {
encoded := &plumbing.MemoryObject{}
// Encode tag components, excluding signature and get a reader object.
- if err := t.encode(encoded, false); err != nil {
+ if err := t.EncodeWithoutSignature(encoded); err != nil {
return nil, err
}
er, err := encoded.Reader()
diff --git a/plumbing/object/tag_test.go b/plumbing/object/tag_test.go
index 0ef7136..addec8d 100644
--- a/plumbing/object/tag_test.go
+++ b/plumbing/object/tag_test.go
@@ -3,16 +3,17 @@ package object
import (
"fmt"
"io"
+ "io/ioutil"
"strings"
"time"
+ fixtures "gopkg.in/src-d/go-git-fixtures.v3"
"gopkg.in/src-d/go-git.v4/plumbing"
"gopkg.in/src-d/go-git.v4/plumbing/cache"
"gopkg.in/src-d/go-git.v4/storage/filesystem"
"gopkg.in/src-d/go-git.v4/storage/memory"
. "gopkg.in/check.v1"
- "gopkg.in/src-d/go-git-fixtures.v3"
)
type TagSuite struct {
@@ -447,3 +448,24 @@ HdzbB2ak/HxIeCqmHVlmUqa+WfTMUJcsgOm3/ZFPCSoL6l0bz9Z1XVbiyD03
_, err = tag.Verify(armoredKeyRing)
c.Assert(err, IsNil)
}
+
+func (s *TagSuite) TestEncodeWithoutSignature(c *C) {
+ //Similar to TestString since no signature
+ encoded := &plumbing.MemoryObject{}
+ tag := s.tag(c, plumbing.NewHash("b742a2a9fa0afcfa9a6fad080980fbc26b007c69"))
+ err := tag.EncodeWithoutSignature(encoded)
+ c.Assert(err, IsNil)
+ er, err := encoded.Reader()
+ c.Assert(err, IsNil)
+ payload, err := ioutil.ReadAll(er)
+ c.Assert(err, IsNil)
+
+ c.Assert(string(payload), Equals, ""+
+ "object f7b877701fbf855b44c0a9e86f3fdce2c298b07f\n"+
+ "type commit\n"+
+ "tag annotated-tag\n"+
+ "tagger Máximo Cuadros <mcuadros@gmail.com> 1474485215 +0200\n"+
+ "\n"+
+ "example annotated tag\n",
+ )
+}