From 66f08b836000cb79d2a5b76d3e15e1f81dc77e0b Mon Sep 17 00:00:00 2001 From: Máximo Cuadros Date: Thu, 1 Feb 2018 17:10:42 +0100 Subject: new methods Worktree.[AddGlob|AddDirectory] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Máximo Cuadros --- worktree_commit.go | 1 - worktree_status.go | 218 ++++++++++++++++++++++++++++++++++++++++++++--------- worktree_test.go | 160 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 344 insertions(+), 35 deletions(-) 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..4141381 100644 --- a/worktree_status.go +++ b/worktree_status.go @@ -6,6 +6,7 @@ import ( "io" "os" + "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 +19,16 @@ 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 + // file in the worktree.ErrNotDirectory + ErrGlobNoMatches = errors.New("glob pattern did not match any files") + // ErrNotDirectory in an AddDirectory if the path is not a directory. + ErrNotDirectory = errors.New("path is not a directory") +) // Status returns the working tree status. func (w *Worktree) Status() (Status, error) { @@ -243,30 +251,170 @@ 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. func (w *Worktree) Add(path string) (plumbing.Hash, error) { 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 + } + + added, h, err := w.doAdd(idx, s, path) + if err != nil { + return h, err + } + + if !added { + return h, nil + } + + return h, w.r.Storer.SetIndex(idx) +} + +// AddDirectory adds the files contents of a directory and all his +// sub-directories recursively in the worktree to the index. If any of the +// file is already staged in the index no error is returned. +func (w *Worktree) AddDirectory(path string) error { + fi, err := w.Filesystem.Lstat(path) + if err != nil { + return err + } + + if !fi.IsDir() { + return ErrNotDirectory + } + + s, err := w.Status() + if err != nil { + return err + } + + idx, err := w.r.Storer.Index() + if err != nil { + return err + } + + added, err := w.doAddDirectory(idx, s, path) + if err != nil { + return err + } + + if !added { + return nil + } + + return w.r.Storer.SetIndex(idx) +} + +func (w *Worktree) doAddDirectory(idx *index.Index, s Status, path string) (added bool, err error) { + files, err := w.Filesystem.ReadDir(path) + if err != nil { + return false, err + } + + for _, file := range files { + name := w.Filesystem.Join(path, file.Name()) + + var a bool + if file.IsDir() { + a, err = w.doAddDirectory(idx, s, name) + } else { + a, _, err = w.doAdd(idx, s, name) + } + + if err != nil { + return + } + + if !added && a { + added = true + } + } + + return + +} + +// AddGlob given a glob pattern adds all the matching files content and all his +// sub-directories recursively in the worktree to the index. If any of the +// file is already staged in the index no error is returned. +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.doAdd(idx, s, file) + } + + if err != nil { + return err + } + + if !saveIndex && added { + saveIndex = true + } + } + + if saveIndex { + return w.r.Storer.SetIndex(idx) + } + + return nil +} + +// doAdd 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) doAdd(idx *index.Index, s Status, path string) (added bool, h plumbing.Hash, err error) { + h, err = w.copyFileToStorage(path) if err != nil { if os.IsNotExist(err) { - h, err = w.deleteFromIndex(path) + added = true + h, err = w.deleteFromIndex(idx, path) } - return h, err + + return } if s.File(path).Worktree == Unmodified { - return h, nil + return false, h, nil } - if err := w.addOrUpdateFileToIndex(path, h); err != nil { - return h, err + if err := w.addOrUpdateFileToIndex(idx, path, h); err != nil { + return false, h, err } - return h, err + return true, h, err } func (w *Worktree) copyFileToStorage(path string) (hash plumbing.Hash, err error) { @@ -324,28 +472,17 @@ 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 { @@ -378,26 +515,30 @@ 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) + idx, err := w.r.Storer.Index() if err != nil { return plumbing.ZeroHash, err } - return hash, w.deleteFromFilesystem(path) -} - -func (w *Worktree) deleteFromIndex(path string) (plumbing.Hash, error) { - idx, err := w.r.Storer.Index() + hash, err := w.deleteFromIndex(idx, path) if err != nil { return plumbing.ZeroHash, err } + if err := w.deleteFromFilesystem(path); err != nil { + return hash, err + } + + return hash, w.r.Storer.SetIndex(idx) +} + +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 { @@ -420,7 +561,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 +575,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..8f30e82 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,133 @@ 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) + + err = w.AddDirectory("qux") + 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/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) TestAddDirectoryErrorNotDirectory(c *C) { + r, _ := Init(memory.NewStorage(), memfs.New()) + w, _ := r.Worktree() + + err := util.WriteFile(w.Filesystem, "foo", []byte("FOO"), 0755) + c.Assert(err, IsNil) + + err = w.AddDirectory("foo") + c.Assert(err, Equals, ErrNotDirectory) +} + +func (s *WorktreeSuite) TestAddDirectoryErrorNotFound(c *C) { + r, _ := Init(memory.NewStorage(), memfs.New()) + w, _ := r.Worktree() + + err := w.AddDirectory("foo") + c.Assert(err, NotNil) +} + +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("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) { -- cgit 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 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 From 6d23b50e27312f3ba3e839153c2c0db5237c827d Mon Sep 17 00:00:00 2001 From: Máximo Cuadros Date: Mon, 26 Feb 2018 00:26:41 +0100 Subject: new methods Worktree.[AddGlob|RemoveBlob] and recursive Worktree.[Add|Remove] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Máximo Cuadros --- worktree_status.go | 192 +++++++++++++++++++++++++++++++++++------------------ worktree_test.go | 139 ++++++++++++++++++++++++++++++++++---- 2 files changed, 254 insertions(+), 77 deletions(-) diff --git a/worktree_status.go b/worktree_status.go index 4141381..2cac78e 100644 --- a/worktree_status.go +++ b/worktree_status.go @@ -5,6 +5,8 @@ import ( "errors" "io" "os" + "path" + "path/filepath" "gopkg.in/src-d/go-billy.v4/util" "gopkg.in/src-d/go-git.v4/plumbing" @@ -24,10 +26,8 @@ var ( // the worktree. ErrDestinationExists = errors.New("destination exists") // ErrGlobNoMatches in an AddGlob if the glob pattern does not match any - // file in the worktree.ErrNotDirectory + // files in the worktree. ErrGlobNoMatches = errors.New("glob pattern did not match any files") - // ErrNotDirectory in an AddDirectory if the path is not a directory. - ErrNotDirectory = errors.New("path is not a directory") ) // Status returns the working tree status. @@ -252,8 +252,12 @@ 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. If a file deleted -// from the Workspace is given, the file is removed from the index. +// 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 @@ -264,67 +268,41 @@ func (w *Worktree) Add(path string) (plumbing.Hash, error) { return plumbing.ZeroHash, err } - added, h, err := w.doAdd(idx, s, path) - if err != nil { - return h, err - } - - if !added { - return h, nil - } + var h plumbing.Hash + var added bool - return h, w.r.Storer.SetIndex(idx) -} - -// AddDirectory adds the files contents of a directory and all his -// sub-directories recursively in the worktree to the index. If any of the -// file is already staged in the index no error is returned. -func (w *Worktree) AddDirectory(path string) error { fi, err := w.Filesystem.Lstat(path) - if err != nil { - return err - } - - if !fi.IsDir() { - return ErrNotDirectory - } - - s, err := w.Status() - if err != nil { - return err - } - - idx, err := w.r.Storer.Index() - if err != nil { - return err + if err != nil || !fi.IsDir() { + added, h, err = w.doAddFile(idx, s, path) + } else { + added, err = w.doAddDirectory(idx, s, path) } - added, err := w.doAddDirectory(idx, s, path) if err != nil { - return err + return h, err } if !added { - return nil + return h, nil } - return w.r.Storer.SetIndex(idx) + return h, w.r.Storer.SetIndex(idx) } -func (w *Worktree) doAddDirectory(idx *index.Index, s Status, path string) (added bool, err error) { - files, err := w.Filesystem.ReadDir(path) +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 := w.Filesystem.Join(path, file.Name()) + name := path.Join(directory, file.Name()) var a bool if file.IsDir() { a, err = w.doAddDirectory(idx, s, name) } else { - a, _, err = w.doAdd(idx, s, name) + a, _, err = w.doAddFile(idx, s, name) } if err != nil { @@ -337,12 +315,11 @@ func (w *Worktree) doAddDirectory(idx *index.Index, s Status, path string) (adde } return - } -// AddGlob given a glob pattern adds all the matching files content and all his -// sub-directories recursively in the worktree to the index. If any of the -// file is already staged in the index no error is returned. +// 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 { @@ -374,7 +351,7 @@ func (w *Worktree) AddGlob(pattern string) error { if fi.IsDir() { added, err = w.doAddDirectory(idx, s, file) } else { - added, _, err = w.doAdd(idx, s, file) + added, _, err = w.doAddFile(idx, s, file) } if err != nil { @@ -393,9 +370,13 @@ func (w *Worktree) AddGlob(pattern string) error { return nil } -// doAdd create a new blob from path and update the index, added is true if +// 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) doAdd(idx *index.Index, s Status, path string) (added bool, h plumbing.Hash, err error) { +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) { @@ -406,10 +387,6 @@ func (w *Worktree) doAdd(idx *index.Index, s Status, path string) (added bool, h return } - if s.File(path).Worktree == Unmodified { - return false, h, nil - } - if err := w.addOrUpdateFileToIndex(idx, path, h); err != nil { return false, h, err } @@ -486,10 +463,7 @@ func (w *Worktree) addOrUpdateFileToIndex(idx *index.Index, filename string, h p } 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 { @@ -515,21 +489,79 @@ 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) { + // TODO(mcuadros): remove plumbing.Hash from signature at v5. idx, err := w.r.Storer.Index() if err != nil { return plumbing.ZeroHash, err } - hash, err := w.deleteFromIndex(idx, 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 plumbing.ZeroHash, err + return h, err } - if err := w.deleteFromFilesystem(path); err != nil { - return hash, err + return h, w.r.Storer.SetIndex(idx) +} + +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 } - return hash, w.r.Storer.SetIndex(idx) + 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) { @@ -550,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 } diff --git a/worktree_test.go b/worktree_test.go index 8f30e82..cb2e5e2 100644 --- a/worktree_test.go +++ b/worktree_test.go @@ -1196,8 +1196,9 @@ func (s *WorktreeSuite) TestAddDirectory(c *C) { err = util.WriteFile(w.Filesystem, "qux/baz/bar", []byte("BAR"), 0755) c.Assert(err, IsNil) - err = w.AddDirectory("qux") + 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) @@ -1224,23 +1225,13 @@ func (s *WorktreeSuite) TestAddDirectory(c *C) { c.Assert(file.Worktree, Equals, Unmodified) } -func (s *WorktreeSuite) TestAddDirectoryErrorNotDirectory(c *C) { - r, _ := Init(memory.NewStorage(), memfs.New()) - w, _ := r.Worktree() - - err := util.WriteFile(w.Filesystem, "foo", []byte("FOO"), 0755) - c.Assert(err, IsNil) - - err = w.AddDirectory("foo") - c.Assert(err, Equals, ErrNotDirectory) -} - func (s *WorktreeSuite) TestAddDirectoryErrorNotFound(c *C) { r, _ := Init(memory.NewStorage(), memfs.New()) w, _ := r.Worktree() - err := w.AddDirectory("foo") + h, err := w.Add("foo") c.Assert(err, NotNil) + c.Assert(h.IsZero(), Equals, true) } func (s *WorktreeSuite) TestAddGlob(c *C) { @@ -1264,7 +1255,7 @@ func (s *WorktreeSuite) TestAddGlob(c *C) { err = util.WriteFile(w.Filesystem, "qux/bar/baz", []byte("BAZ"), 0755) c.Assert(err, IsNil) - err = w.AddGlob("qux/b*") + err = w.AddGlob(w.Filesystem.Join("qux", "b*")) c.Assert(err, IsNil) idx, err = w.r.Storer.Index() @@ -1339,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{ @@ -1362,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{ -- cgit