aboutsummaryrefslogtreecommitdiffstats
path: root/plumbing/format
diff options
context:
space:
mode:
authorMáximo Cuadros <mcuadros@gmail.com>2018-02-26 00:26:37 +0100
committerMáximo Cuadros <mcuadros@gmail.com>2018-02-26 00:26:37 +0100
commit9fb58fc0561855882b1741dbb8b6cbaf6e889351 (patch)
tree803da8be65d8c78e458d084714efd9b969f6da47 /plumbing/format
parent66f08b836000cb79d2a5b76d3e15e1f81dc77e0b (diff)
downloadgo-git-9fb58fc0561855882b1741dbb8b6cbaf6e889351.tar.gz
plumbing: format index, Index.Add and Index.Glob methods
Signed-off-by: Máximo Cuadros <mcuadros@gmail.com>
Diffstat (limited to 'plumbing/format')
-rw-r--r--plumbing/format/index/index.go32
-rw-r--r--plumbing/format/index/index_test.go37
-rw-r--r--plumbing/format/index/match.go186
3 files changed, 255 insertions, 0 deletions
diff --git a/plumbing/format/index/index.go b/plumbing/format/index/index.go
index 9de4230..fc7b8cd 100644
--- a/plumbing/format/index/index.go
+++ b/plumbing/format/index/index.go
@@ -4,6 +4,7 @@ import (
"bytes"
"errors"
"fmt"
+ "path/filepath"
"time"
"gopkg.in/src-d/go-git.v4/plumbing"
@@ -51,8 +52,20 @@ type Index struct {
ResolveUndo *ResolveUndo
}
+// Add creates a new Entry and returns it. The caller should first check that
+// another entry with the same path does not exist.
+func (i *Index) Add(path string) *Entry {
+ e := &Entry{
+ Name: filepath.ToSlash(path),
+ }
+
+ i.Entries = append(i.Entries, e)
+ return e
+}
+
// Entry returns the entry that match the given path, if any.
func (i *Index) Entry(path string) (*Entry, error) {
+ path = filepath.ToSlash(path)
for _, e := range i.Entries {
if e.Name == path {
return e, nil
@@ -64,6 +77,7 @@ func (i *Index) Entry(path string) (*Entry, error) {
// Remove remove the entry that match the give path and returns deleted entry.
func (i *Index) Remove(path string) (*Entry, error) {
+ path = filepath.ToSlash(path)
for index, e := range i.Entries {
if e.Name == path {
i.Entries = append(i.Entries[:index], i.Entries[index+1:]...)
@@ -74,6 +88,24 @@ func (i *Index) Remove(path string) (*Entry, error) {
return nil, ErrEntryNotFound
}
+// Glob returns the all entries matching pattern or nil if there is no matching
+// entry. The syntax of patterns is the same as in filepath.Glob.
+func (i *Index) Glob(pattern string) (matches []*Entry, err error) {
+ pattern = filepath.ToSlash(pattern)
+ for _, e := range i.Entries {
+ m, err := match(pattern, e.Name)
+ if err != nil {
+ return nil, err
+ }
+
+ if m {
+ matches = append(matches, e)
+ }
+ }
+
+ return
+}
+
// String is equivalent to `git ls-files --stage --debug`
func (i *Index) String() string {
buf := bytes.NewBuffer(nil)
diff --git a/plumbing/format/index/index_test.go b/plumbing/format/index/index_test.go
index cad5f9c..ecf3c0d 100644
--- a/plumbing/format/index/index_test.go
+++ b/plumbing/format/index/index_test.go
@@ -1,9 +1,22 @@
package index
import (
+ "path/filepath"
+
. "gopkg.in/check.v1"
)
+func (s *IndexSuite) TestIndexAdd(c *C) {
+ idx := &Index{}
+ e := idx.Add("foo")
+ e.Size = 42
+
+ e, err := idx.Entry("foo")
+ c.Assert(err, IsNil)
+ c.Assert(e.Name, Equals, "foo")
+ c.Assert(e.Size, Equals, uint32(42))
+}
+
func (s *IndexSuite) TestIndexEntry(c *C) {
idx := &Index{
Entries: []*Entry{
@@ -37,3 +50,27 @@ func (s *IndexSuite) TestIndexRemove(c *C) {
c.Assert(e, IsNil)
c.Assert(err, Equals, ErrEntryNotFound)
}
+
+func (s *IndexSuite) TestIndexGlob(c *C) {
+ idx := &Index{
+ Entries: []*Entry{
+ {Name: "foo/bar/bar", Size: 42},
+ {Name: "foo/baz/qux", Size: 42},
+ {Name: "fux", Size: 82},
+ },
+ }
+
+ m, err := idx.Glob(filepath.Join("foo", "b*"))
+ c.Assert(err, IsNil)
+ c.Assert(m, HasLen, 2)
+ c.Assert(m[0].Name, Equals, "foo/bar/bar")
+ c.Assert(m[1].Name, Equals, "foo/baz/qux")
+
+ m, err = idx.Glob("f*")
+ c.Assert(err, IsNil)
+ c.Assert(m, HasLen, 3)
+
+ m, err = idx.Glob("f*/baz/q*")
+ c.Assert(err, IsNil)
+ c.Assert(m, HasLen, 1)
+}
diff --git a/plumbing/format/index/match.go b/plumbing/format/index/match.go
new file mode 100644
index 0000000..2891d7d
--- /dev/null
+++ b/plumbing/format/index/match.go
@@ -0,0 +1,186 @@
+package index
+
+import (
+ "path/filepath"
+ "runtime"
+ "unicode/utf8"
+)
+
+// match is filepath.Match with support to match fullpath and not only filenames
+// code from:
+// https://github.com/golang/go/blob/39852bf4cce6927e01d0136c7843f65a801738cb/src/path/filepath/match.go#L44-L224
+func match(pattern, name string) (matched bool, err error) {
+Pattern:
+ for len(pattern) > 0 {
+ var star bool
+ var chunk string
+ star, chunk, pattern = scanChunk(pattern)
+
+ // Look for match at current position.
+ t, ok, err := matchChunk(chunk, name)
+ // if we're the last chunk, make sure we've exhausted the name
+ // otherwise we'll give a false result even if we could still match
+ // using the star
+ if ok && (len(t) == 0 || len(pattern) > 0) {
+ name = t
+ continue
+ }
+ if err != nil {
+ return false, err
+ }
+ if star {
+ // Look for match skipping i+1 bytes.
+ // Cannot skip /.
+ for i := 0; i < len(name); i++ {
+ t, ok, err := matchChunk(chunk, name[i+1:])
+ if ok {
+ // if we're the last chunk, make sure we exhausted the name
+ if len(pattern) == 0 && len(t) > 0 {
+ continue
+ }
+ name = t
+ continue Pattern
+ }
+ if err != nil {
+ return false, err
+ }
+ }
+ }
+ return false, nil
+ }
+ return len(name) == 0, nil
+}
+
+// scanChunk gets the next segment of pattern, which is a non-star string
+// possibly preceded by a star.
+func scanChunk(pattern string) (star bool, chunk, rest string) {
+ for len(pattern) > 0 && pattern[0] == '*' {
+ pattern = pattern[1:]
+ star = true
+ }
+ inrange := false
+ var i int
+Scan:
+ for i = 0; i < len(pattern); i++ {
+ switch pattern[i] {
+ case '\\':
+ if runtime.GOOS != "windows" {
+ // error check handled in matchChunk: bad pattern.
+ if i+1 < len(pattern) {
+ i++
+ }
+ }
+ case '[':
+ inrange = true
+ case ']':
+ inrange = false
+ case '*':
+ if !inrange {
+ break Scan
+ }
+ }
+ }
+ return star, pattern[0:i], pattern[i:]
+}
+
+// matchChunk checks whether chunk matches the beginning of s.
+// If so, it returns the remainder of s (after the match).
+// Chunk is all single-character operators: literals, char classes, and ?.
+func matchChunk(chunk, s string) (rest string, ok bool, err error) {
+ for len(chunk) > 0 {
+ if len(s) == 0 {
+ return
+ }
+ switch chunk[0] {
+ case '[':
+ // character class
+ r, n := utf8.DecodeRuneInString(s)
+ s = s[n:]
+ chunk = chunk[1:]
+ // We can't end right after '[', we're expecting at least
+ // a closing bracket and possibly a caret.
+ if len(chunk) == 0 {
+ err = filepath.ErrBadPattern
+ return
+ }
+ // possibly negated
+ negated := chunk[0] == '^'
+ if negated {
+ chunk = chunk[1:]
+ }
+ // parse all ranges
+ match := false
+ nrange := 0
+ for {
+ if len(chunk) > 0 && chunk[0] == ']' && nrange > 0 {
+ chunk = chunk[1:]
+ break
+ }
+ var lo, hi rune
+ if lo, chunk, err = getEsc(chunk); err != nil {
+ return
+ }
+ hi = lo
+ if chunk[0] == '-' {
+ if hi, chunk, err = getEsc(chunk[1:]); err != nil {
+ return
+ }
+ }
+ if lo <= r && r <= hi {
+ match = true
+ }
+ nrange++
+ }
+ if match == negated {
+ return
+ }
+
+ case '?':
+ _, n := utf8.DecodeRuneInString(s)
+ s = s[n:]
+ chunk = chunk[1:]
+
+ case '\\':
+ if runtime.GOOS != "windows" {
+ chunk = chunk[1:]
+ if len(chunk) == 0 {
+ err = filepath.ErrBadPattern
+ return
+ }
+ }
+ fallthrough
+
+ default:
+ if chunk[0] != s[0] {
+ return
+ }
+ s = s[1:]
+ chunk = chunk[1:]
+ }
+ }
+ return s, true, nil
+}
+
+// getEsc gets a possibly-escaped character from chunk, for a character class.
+func getEsc(chunk string) (r rune, nchunk string, err error) {
+ if len(chunk) == 0 || chunk[0] == '-' || chunk[0] == ']' {
+ err = filepath.ErrBadPattern
+ return
+ }
+ if chunk[0] == '\\' && runtime.GOOS != "windows" {
+ chunk = chunk[1:]
+ if len(chunk) == 0 {
+ err = filepath.ErrBadPattern
+ return
+ }
+ }
+ r, n := utf8.DecodeRuneInString(chunk)
+ if r == utf8.RuneError && n == 1 {
+ err = filepath.ErrBadPattern
+ }
+ nchunk = chunk[n:]
+ if len(nchunk) == 0 {
+ err = filepath.ErrBadPattern
+ }
+ return
+}