From 9fb58fc0561855882b1741dbb8b6cbaf6e889351 Mon Sep 17 00:00:00 2001 From: Máximo Cuadros Date: Mon, 26 Feb 2018 00:26:37 +0100 Subject: plumbing: format index, Index.Add and Index.Glob methods MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Máximo Cuadros --- plumbing/format/index/index.go | 32 +++++++ plumbing/format/index/index_test.go | 37 +++++++ plumbing/format/index/match.go | 186 ++++++++++++++++++++++++++++++++++++ 3 files changed, 255 insertions(+) create mode 100644 plumbing/format/index/match.go (limited to 'plumbing/format') 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 +} -- cgit