diff options
Diffstat (limited to 'plumbing')
-rw-r--r-- | plumbing/format/commitgraph/commitgraph.go | 6 | ||||
-rw-r--r-- | plumbing/format/commitgraph/commitgraph_test.go | 41 | ||||
-rw-r--r-- | plumbing/format/commitgraph/encoder.go | 34 | ||||
-rw-r--r-- | plumbing/format/commitgraph/file.go | 18 | ||||
-rw-r--r-- | plumbing/format/commitgraph/memory.go | 25 | ||||
-rw-r--r-- | plumbing/format/gitattributes/attributes.go | 214 | ||||
-rw-r--r-- | plumbing/format/gitattributes/attributes_test.go | 67 | ||||
-rw-r--r-- | plumbing/format/gitattributes/dir.go | 126 | ||||
-rw-r--r-- | plumbing/format/gitattributes/dir_test.go | 199 | ||||
-rw-r--r-- | plumbing/format/gitattributes/matcher.go | 78 | ||||
-rw-r--r-- | plumbing/format/gitattributes/matcher_test.go | 29 | ||||
-rw-r--r-- | plumbing/format/gitattributes/pattern.go | 101 | ||||
-rw-r--r-- | plumbing/format/gitattributes/pattern_test.go | 229 | ||||
-rw-r--r-- | plumbing/object/commit.go | 7 | ||||
-rw-r--r-- | plumbing/object/commit_test.go | 23 | ||||
-rw-r--r-- | plumbing/object/patch.go | 4 | ||||
-rw-r--r-- | plumbing/object/tag.go | 7 | ||||
-rw-r--r-- | plumbing/object/tag_test.go | 24 |
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", + ) +} |