aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMáximo Cuadros <mcuadros@gmail.com>2018-02-28 19:18:36 +0100
committerGitHub <noreply@github.com>2018-02-28 19:18:36 +0100
commit4397264e391b45a8eac147cc7373189d55c640cc (patch)
tree6198853fc7ca31ac16a0cbfa683828568452a6dd
parentdefd0b861ca79845c8f06f7c826c769012404bbd (diff)
parent6d23b50e27312f3ba3e839153c2c0db5237c827d (diff)
downloadgo-git-4397264e391b45a8eac147cc7373189d55c640cc.tar.gz
Merge pull request #739 from mcuadros/add
new methods Worktree.[AddGlob|RemoveBlob] and recursive Worktree.[Add|Remove]
-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
-rw-r--r--worktree_commit.go1
-rw-r--r--worktree_status.go290
-rw-r--r--worktree_test.go271
6 files changed, 779 insertions, 38 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
+}
diff --git a/worktree_commit.go b/worktree_commit.go
index 3145c8a..5fa63ab 100644
--- a/worktree_commit.go
+++ b/worktree_commit.go
@@ -63,7 +63,6 @@ func (w *Worktree) autoAddModifiedAndDeleted() error {
if _, err := w.Add(path); err != nil {
return err
}
-
}
return nil
diff --git a/worktree_status.go b/worktree_status.go
index 36f48eb..2cac78e 100644
--- a/worktree_status.go
+++ b/worktree_status.go
@@ -5,7 +5,10 @@ import (
"errors"
"io"
"os"
+ "path"
+ "path/filepath"
+ "gopkg.in/src-d/go-billy.v4/util"
"gopkg.in/src-d/go-git.v4/plumbing"
"gopkg.in/src-d/go-git.v4/plumbing/filemode"
"gopkg.in/src-d/go-git.v4/plumbing/format/gitignore"
@@ -18,9 +21,14 @@ import (
"gopkg.in/src-d/go-git.v4/utils/merkletrie/noder"
)
-// ErrDestinationExists in an Move operation means that the target exists on
-// the worktree.
-var ErrDestinationExists = errors.New("destination exists")
+var (
+ // ErrDestinationExists in an Move operation means that the target exists on
+ // the worktree.
+ ErrDestinationExists = errors.New("destination exists")
+ // ErrGlobNoMatches in an AddGlob if the glob pattern does not match any
+ // files in the worktree.
+ ErrGlobNoMatches = errors.New("glob pattern did not match any files")
+)
// Status returns the working tree status.
func (w *Worktree) Status() (Status, error) {
@@ -243,30 +251,147 @@ func diffTreeIsEquals(a, b noder.Hasher) bool {
}
// Add adds the file contents of a file in the worktree to the index. if the
-// file is already staged in the index no error is returned.
+// file is already staged in the index no error is returned. If a file deleted
+// from the Workspace is given, the file is removed from the index. If a
+// directory given, adds the files and all his sub-directories recursively in
+// the worktree to the index. If any of the files is already staged in the index
+// no error is returned. When path is a file, the blob.Hash is returned.
func (w *Worktree) Add(path string) (plumbing.Hash, error) {
+ // TODO(mcuadros): remove plumbing.Hash from signature at v5.
s, err := w.Status()
if err != nil {
return plumbing.ZeroHash, err
}
- h, err := w.copyFileToStorage(path)
+ idx, err := w.r.Storer.Index()
+ if err != nil {
+ return plumbing.ZeroHash, err
+ }
+
+ var h plumbing.Hash
+ var added bool
+
+ fi, err := w.Filesystem.Lstat(path)
+ if err != nil || !fi.IsDir() {
+ added, h, err = w.doAddFile(idx, s, path)
+ } else {
+ added, err = w.doAddDirectory(idx, s, path)
+ }
+
if err != nil {
- if os.IsNotExist(err) {
- h, err = w.deleteFromIndex(path)
- }
return h, err
}
- if s.File(path).Worktree == Unmodified {
+ if !added {
return h, nil
}
- if err := w.addOrUpdateFileToIndex(path, h); err != nil {
- return h, err
+ return h, w.r.Storer.SetIndex(idx)
+}
+
+func (w *Worktree) doAddDirectory(idx *index.Index, s Status, directory string) (added bool, err error) {
+ files, err := w.Filesystem.ReadDir(directory)
+ if err != nil {
+ return false, err
+ }
+
+ for _, file := range files {
+ name := path.Join(directory, file.Name())
+
+ var a bool
+ if file.IsDir() {
+ a, err = w.doAddDirectory(idx, s, name)
+ } else {
+ a, _, err = w.doAddFile(idx, s, name)
+ }
+
+ if err != nil {
+ return
+ }
+
+ if !added && a {
+ added = true
+ }
+ }
+
+ return
+}
+
+// AddGlob adds all paths, matching pattern, to the index. If pattern matches a
+// directory path, all directory contents are added to the index recursively. No
+// error is returned if all matching paths are already staged in index.
+func (w *Worktree) AddGlob(pattern string) error {
+ files, err := util.Glob(w.Filesystem, pattern)
+ if err != nil {
+ return err
+ }
+
+ if len(files) == 0 {
+ return ErrGlobNoMatches
+ }
+
+ s, err := w.Status()
+ if err != nil {
+ return err
+ }
+
+ idx, err := w.r.Storer.Index()
+ if err != nil {
+ return err
+ }
+
+ var saveIndex bool
+ for _, file := range files {
+ fi, err := w.Filesystem.Lstat(file)
+ if err != nil {
+ return err
+ }
+
+ var added bool
+ if fi.IsDir() {
+ added, err = w.doAddDirectory(idx, s, file)
+ } else {
+ added, _, err = w.doAddFile(idx, s, file)
+ }
+
+ if err != nil {
+ return err
+ }
+
+ if !saveIndex && added {
+ saveIndex = true
+ }
}
- return h, err
+ if saveIndex {
+ return w.r.Storer.SetIndex(idx)
+ }
+
+ return nil
+}
+
+// doAddFile create a new blob from path and update the index, added is true if
+// the file added is different from the index.
+func (w *Worktree) doAddFile(idx *index.Index, s Status, path string) (added bool, h plumbing.Hash, err error) {
+ if s.File(path).Worktree == Unmodified {
+ return false, h, nil
+ }
+
+ h, err = w.copyFileToStorage(path)
+ if err != nil {
+ if os.IsNotExist(err) {
+ added = true
+ h, err = w.deleteFromIndex(idx, path)
+ }
+
+ return
+ }
+
+ if err := w.addOrUpdateFileToIndex(idx, path, h); err != nil {
+ return false, h, err
+ }
+
+ return true, h, err
}
func (w *Worktree) copyFileToStorage(path string) (hash plumbing.Hash, err error) {
@@ -324,35 +449,21 @@ func (w *Worktree) fillEncodedObjectFromSymlink(dst io.Writer, path string, fi o
return err
}
-func (w *Worktree) addOrUpdateFileToIndex(filename string, h plumbing.Hash) error {
- idx, err := w.r.Storer.Index()
- if err != nil {
- return err
- }
-
+func (w *Worktree) addOrUpdateFileToIndex(idx *index.Index, filename string, h plumbing.Hash) error {
e, err := idx.Entry(filename)
if err != nil && err != index.ErrEntryNotFound {
return err
}
if err == index.ErrEntryNotFound {
- if err := w.doAddFileToIndex(idx, filename, h); err != nil {
- return err
- }
- } else {
- if err := w.doUpdateFileToIndex(e, filename, h); err != nil {
- return err
- }
+ return w.doAddFileToIndex(idx, filename, h)
}
- return w.r.Storer.SetIndex(idx)
+ return w.doUpdateFileToIndex(e, filename, h)
}
func (w *Worktree) doAddFileToIndex(idx *index.Index, filename string, h plumbing.Hash) error {
- e := &index.Entry{Name: filename}
- idx.Entries = append(idx.Entries, e)
-
- return w.doUpdateFileToIndex(e, filename, h)
+ return w.doUpdateFileToIndex(idx.Add(filename), filename, h)
}
func (w *Worktree) doUpdateFileToIndex(e *index.Entry, filename string, h plumbing.Hash) error {
@@ -378,26 +489,88 @@ func (w *Worktree) doUpdateFileToIndex(e *index.Entry, filename string, h plumbi
// Remove removes files from the working tree and from the index.
func (w *Worktree) Remove(path string) (plumbing.Hash, error) {
- hash, err := w.deleteFromIndex(path)
+ // TODO(mcuadros): remove plumbing.Hash from signature at v5.
+ idx, err := w.r.Storer.Index()
if err != nil {
return plumbing.ZeroHash, err
}
- return hash, w.deleteFromFilesystem(path)
+ var h plumbing.Hash
+
+ fi, err := w.Filesystem.Lstat(path)
+ if err != nil || !fi.IsDir() {
+ h, err = w.doRemoveFile(idx, path)
+ } else {
+ _, err = w.doRemoveDirectory(idx, path)
+ }
+ if err != nil {
+ return h, err
+ }
+
+ return h, w.r.Storer.SetIndex(idx)
}
-func (w *Worktree) deleteFromIndex(path string) (plumbing.Hash, error) {
- idx, err := w.r.Storer.Index()
+func (w *Worktree) doRemoveDirectory(idx *index.Index, directory string) (removed bool, err error) {
+ files, err := w.Filesystem.ReadDir(directory)
+ if err != nil {
+ return false, err
+ }
+
+ for _, file := range files {
+ name := path.Join(directory, file.Name())
+
+ var r bool
+ if file.IsDir() {
+ r, err = w.doRemoveDirectory(idx, name)
+ } else {
+ _, err = w.doRemoveFile(idx, name)
+ if err == index.ErrEntryNotFound {
+ err = nil
+ }
+ }
+
+ if err != nil {
+ return
+ }
+
+ if !removed && r {
+ removed = true
+ }
+ }
+
+ err = w.removeEmptyDirectory(directory)
+ return
+}
+
+func (w *Worktree) removeEmptyDirectory(path string) error {
+ files, err := w.Filesystem.ReadDir(path)
+ if err != nil {
+ return err
+ }
+
+ if len(files) != 0 {
+ return nil
+ }
+
+ return w.Filesystem.Remove(path)
+}
+
+func (w *Worktree) doRemoveFile(idx *index.Index, path string) (plumbing.Hash, error) {
+ hash, err := w.deleteFromIndex(idx, path)
if err != nil {
return plumbing.ZeroHash, err
}
+ return hash, w.deleteFromFilesystem(path)
+}
+
+func (w *Worktree) deleteFromIndex(idx *index.Index, path string) (plumbing.Hash, error) {
e, err := idx.Remove(path)
if err != nil {
return plumbing.ZeroHash, err
}
- return e.Hash, w.r.Storer.SetIndex(idx)
+ return e.Hash, nil
}
func (w *Worktree) deleteFromFilesystem(path string) error {
@@ -409,9 +582,43 @@ func (w *Worktree) deleteFromFilesystem(path string) error {
return err
}
+// RemoveGlob removes all paths, matching pattern, from the index. If pattern
+// matches a directory path, all directory contents are removed from the index
+// recursively.
+func (w *Worktree) RemoveGlob(pattern string) error {
+ idx, err := w.r.Storer.Index()
+ if err != nil {
+ return err
+ }
+
+ entries, err := idx.Glob(pattern)
+ if err != nil {
+ return err
+ }
+
+ for _, e := range entries {
+ file := filepath.FromSlash(e.Name)
+ if _, err := w.Filesystem.Lstat(file); err != nil && !os.IsNotExist(err) {
+ return err
+ }
+
+ if _, err := w.doRemoveFile(idx, file); err != nil {
+ return err
+ }
+
+ dir, _ := filepath.Split(file)
+ if err := w.removeEmptyDirectory(dir); err != nil {
+ return err
+ }
+ }
+
+ return w.r.Storer.SetIndex(idx)
+}
+
// Move moves or rename a file in the worktree and the index, directories are
// not supported.
func (w *Worktree) Move(from, to string) (plumbing.Hash, error) {
+ // TODO(mcuadros): support directories and/or implement support for glob
if _, err := w.Filesystem.Lstat(from); err != nil {
return plumbing.ZeroHash, err
}
@@ -420,7 +627,12 @@ func (w *Worktree) Move(from, to string) (plumbing.Hash, error) {
return plumbing.ZeroHash, ErrDestinationExists
}
- hash, err := w.deleteFromIndex(from)
+ idx, err := w.r.Storer.Index()
+ if err != nil {
+ return plumbing.ZeroHash, err
+ }
+
+ hash, err := w.deleteFromIndex(idx, from)
if err != nil {
return plumbing.ZeroHash, err
}
@@ -429,5 +641,9 @@ func (w *Worktree) Move(from, to string) (plumbing.Hash, error) {
return hash, err
}
- return hash, w.addOrUpdateFileToIndex(to, hash)
+ if err := w.addOrUpdateFileToIndex(idx, to, hash); err != nil {
+ return hash, err
+ }
+
+ return hash, w.r.Storer.SetIndex(idx)
}
diff --git a/worktree_test.go b/worktree_test.go
index e51e89a..cb2e5e2 100644
--- a/worktree_test.go
+++ b/worktree_test.go
@@ -1115,6 +1115,40 @@ func (s *WorktreeSuite) TestAddUnmodified(c *C) {
c.Assert(err, IsNil)
}
+func (s *WorktreeSuite) TestAddRemoved(c *C) {
+ fs := memfs.New()
+ w := &Worktree{
+ r: s.Repository,
+ Filesystem: fs,
+ }
+
+ err := w.Checkout(&CheckoutOptions{Force: true})
+ c.Assert(err, IsNil)
+
+ idx, err := w.r.Storer.Index()
+ c.Assert(err, IsNil)
+ c.Assert(idx.Entries, HasLen, 9)
+
+ err = w.Filesystem.Remove("LICENSE")
+ c.Assert(err, IsNil)
+
+ hash, err := w.Add("LICENSE")
+ c.Assert(err, IsNil)
+ c.Assert(hash.String(), Equals, "c192bd6a24ea1ab01d78686e417c8bdc7c3d197f")
+
+ e, err := idx.Entry("LICENSE")
+ c.Assert(err, IsNil)
+ c.Assert(e.Hash, Equals, hash)
+ c.Assert(e.Mode, Equals, filemode.Regular)
+
+ status, err := w.Status()
+ c.Assert(err, IsNil)
+ c.Assert(status, HasLen, 1)
+
+ file := status.File("LICENSE")
+ c.Assert(file.Staging, Equals, Deleted)
+}
+
func (s *WorktreeSuite) TestAddSymlink(c *C) {
dir, err := ioutil.TempDir("", "checkout")
c.Assert(err, IsNil)
@@ -1141,7 +1175,124 @@ func (s *WorktreeSuite) TestAddSymlink(c *C) {
c.Assert(err, IsNil)
c.Assert(obj, NotNil)
c.Assert(obj.Size(), Equals, int64(3))
+}
+
+func (s *WorktreeSuite) TestAddDirectory(c *C) {
+ fs := memfs.New()
+ w := &Worktree{
+ r: s.Repository,
+ Filesystem: fs,
+ }
+
+ err := w.Checkout(&CheckoutOptions{Force: true})
+ c.Assert(err, IsNil)
+
+ idx, err := w.r.Storer.Index()
+ c.Assert(err, IsNil)
+ c.Assert(idx.Entries, HasLen, 9)
+
+ err = util.WriteFile(w.Filesystem, "qux/foo", []byte("FOO"), 0755)
+ c.Assert(err, IsNil)
+ err = util.WriteFile(w.Filesystem, "qux/baz/bar", []byte("BAR"), 0755)
+ c.Assert(err, IsNil)
+
+ h, err := w.Add("qux")
+ c.Assert(err, IsNil)
+ c.Assert(h.IsZero(), Equals, true)
+
+ idx, err = w.r.Storer.Index()
+ c.Assert(err, IsNil)
+ c.Assert(idx.Entries, HasLen, 11)
+
+ e, err := idx.Entry("qux/foo")
+ c.Assert(err, IsNil)
+ c.Assert(e.Mode, Equals, filemode.Executable)
+
+ e, err = idx.Entry("qux/baz/bar")
+ c.Assert(err, IsNil)
+ c.Assert(e.Mode, Equals, filemode.Executable)
+
+ status, err := w.Status()
+ c.Assert(err, IsNil)
+ c.Assert(status, HasLen, 2)
+
+ file := status.File("qux/foo")
+ c.Assert(file.Staging, Equals, Added)
+ c.Assert(file.Worktree, Equals, Unmodified)
+ file = status.File("qux/baz/bar")
+ c.Assert(file.Staging, Equals, Added)
+ c.Assert(file.Worktree, Equals, Unmodified)
+}
+
+func (s *WorktreeSuite) TestAddDirectoryErrorNotFound(c *C) {
+ r, _ := Init(memory.NewStorage(), memfs.New())
+ w, _ := r.Worktree()
+
+ h, err := w.Add("foo")
+ c.Assert(err, NotNil)
+ c.Assert(h.IsZero(), Equals, true)
+}
+
+func (s *WorktreeSuite) TestAddGlob(c *C) {
+ fs := memfs.New()
+ w := &Worktree{
+ r: s.Repository,
+ Filesystem: fs,
+ }
+
+ err := w.Checkout(&CheckoutOptions{Force: true})
+ c.Assert(err, IsNil)
+
+ idx, err := w.r.Storer.Index()
+ c.Assert(err, IsNil)
+ c.Assert(idx.Entries, HasLen, 9)
+
+ err = util.WriteFile(w.Filesystem, "qux/qux", []byte("QUX"), 0755)
+ c.Assert(err, IsNil)
+ err = util.WriteFile(w.Filesystem, "qux/baz", []byte("BAZ"), 0755)
+ c.Assert(err, IsNil)
+ err = util.WriteFile(w.Filesystem, "qux/bar/baz", []byte("BAZ"), 0755)
+ c.Assert(err, IsNil)
+
+ err = w.AddGlob(w.Filesystem.Join("qux", "b*"))
+ c.Assert(err, IsNil)
+
+ idx, err = w.r.Storer.Index()
+ c.Assert(err, IsNil)
+ c.Assert(idx.Entries, HasLen, 11)
+
+ e, err := idx.Entry("qux/baz")
+ c.Assert(err, IsNil)
+ c.Assert(e.Mode, Equals, filemode.Executable)
+
+ e, err = idx.Entry("qux/bar/baz")
+ c.Assert(err, IsNil)
+ c.Assert(e.Mode, Equals, filemode.Executable)
+
+ status, err := w.Status()
+ c.Assert(err, IsNil)
+ c.Assert(status, HasLen, 3)
+
+ file := status.File("qux/qux")
+ c.Assert(file.Staging, Equals, Untracked)
+ c.Assert(file.Worktree, Equals, Untracked)
+
+ file = status.File("qux/baz")
+ c.Assert(file.Staging, Equals, Added)
+ c.Assert(file.Worktree, Equals, Unmodified)
+
+ file = status.File("qux/bar/baz")
+ c.Assert(file.Staging, Equals, Added)
+ c.Assert(file.Worktree, Equals, Unmodified)
+}
+
+func (s *WorktreeSuite) TestAddGlobErrorNoMatches(c *C) {
+ r, _ := Init(memory.NewStorage(), memfs.New())
+ w, _ := r.Worktree()
+
+ err := w.AddGlob("foo")
+ c.Assert(err, Equals, ErrGlobNoMatches)
}
func (s *WorktreeSuite) TestRemove(c *C) {
@@ -1179,6 +1330,58 @@ func (s *WorktreeSuite) TestRemoveNotExistentEntry(c *C) {
c.Assert(err, NotNil)
}
+func (s *WorktreeSuite) TestRemoveDirectory(c *C) {
+ fs := memfs.New()
+ w := &Worktree{
+ r: s.Repository,
+ Filesystem: fs,
+ }
+
+ err := w.Checkout(&CheckoutOptions{Force: true})
+ c.Assert(err, IsNil)
+
+ hash, err := w.Remove("json")
+ c.Assert(hash.IsZero(), Equals, true)
+ c.Assert(err, IsNil)
+
+ status, err := w.Status()
+ c.Assert(err, IsNil)
+ c.Assert(status, HasLen, 2)
+ c.Assert(status.File("json/long.json").Staging, Equals, Deleted)
+ c.Assert(status.File("json/short.json").Staging, Equals, Deleted)
+
+ _, err = w.Filesystem.Stat("json")
+ c.Assert(os.IsNotExist(err), Equals, true)
+}
+
+func (s *WorktreeSuite) TestRemoveDirectoryUntracked(c *C) {
+ fs := memfs.New()
+ w := &Worktree{
+ r: s.Repository,
+ Filesystem: fs,
+ }
+
+ err := w.Checkout(&CheckoutOptions{Force: true})
+ c.Assert(err, IsNil)
+
+ err = util.WriteFile(w.Filesystem, "json/foo", []byte("FOO"), 0755)
+ c.Assert(err, IsNil)
+
+ hash, err := w.Remove("json")
+ c.Assert(hash.IsZero(), Equals, true)
+ c.Assert(err, IsNil)
+
+ status, err := w.Status()
+ c.Assert(err, IsNil)
+ c.Assert(status, HasLen, 3)
+ c.Assert(status.File("json/long.json").Staging, Equals, Deleted)
+ c.Assert(status.File("json/short.json").Staging, Equals, Deleted)
+ c.Assert(status.File("json/foo").Staging, Equals, Untracked)
+
+ _, err = w.Filesystem.Stat("json")
+ c.Assert(err, IsNil)
+}
+
func (s *WorktreeSuite) TestRemoveDeletedFromWorktree(c *C) {
fs := memfs.New()
w := &Worktree{
@@ -1202,6 +1405,74 @@ func (s *WorktreeSuite) TestRemoveDeletedFromWorktree(c *C) {
c.Assert(status.File("LICENSE").Staging, Equals, Deleted)
}
+func (s *WorktreeSuite) TestRemoveGlob(c *C) {
+ fs := memfs.New()
+ w := &Worktree{
+ r: s.Repository,
+ Filesystem: fs,
+ }
+
+ err := w.Checkout(&CheckoutOptions{Force: true})
+ c.Assert(err, IsNil)
+
+ err = w.RemoveGlob(w.Filesystem.Join("json", "l*"))
+ c.Assert(err, IsNil)
+
+ status, err := w.Status()
+ c.Assert(err, IsNil)
+ c.Assert(status, HasLen, 1)
+ c.Assert(status.File("json/long.json").Staging, Equals, Deleted)
+}
+
+func (s *WorktreeSuite) TestRemoveGlobDirectory(c *C) {
+ fs := memfs.New()
+ w := &Worktree{
+ r: s.Repository,
+ Filesystem: fs,
+ }
+
+ err := w.Checkout(&CheckoutOptions{Force: true})
+ c.Assert(err, IsNil)
+
+ err = w.RemoveGlob("js*")
+ c.Assert(err, IsNil)
+
+ status, err := w.Status()
+ c.Assert(err, IsNil)
+ c.Assert(status, HasLen, 2)
+ c.Assert(status.File("json/short.json").Staging, Equals, Deleted)
+ c.Assert(status.File("json/long.json").Staging, Equals, Deleted)
+
+ _, err = w.Filesystem.Stat("json")
+ c.Assert(os.IsNotExist(err), Equals, true)
+}
+
+func (s *WorktreeSuite) TestRemoveGlobDirectoryDeleted(c *C) {
+ fs := memfs.New()
+ w := &Worktree{
+ r: s.Repository,
+ Filesystem: fs,
+ }
+
+ err := w.Checkout(&CheckoutOptions{Force: true})
+ c.Assert(err, IsNil)
+
+ err = fs.Remove("json/short.json")
+ c.Assert(err, IsNil)
+
+ err = util.WriteFile(w.Filesystem, "json/foo", []byte("FOO"), 0755)
+ c.Assert(err, IsNil)
+
+ err = w.RemoveGlob("js*")
+ c.Assert(err, IsNil)
+
+ status, err := w.Status()
+ c.Assert(err, IsNil)
+ c.Assert(status, HasLen, 3)
+ c.Assert(status.File("json/short.json").Staging, Equals, Deleted)
+ c.Assert(status.File("json/long.json").Staging, Equals, Deleted)
+}
+
func (s *WorktreeSuite) TestMove(c *C) {
fs := memfs.New()
w := &Worktree{