aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMáximo Cuadros <mcuadros@gmail.com>2017-04-12 15:18:41 +0200
committerGitHub <noreply@github.com>2017-04-12 15:18:41 +0200
commit932ced9f55f556de02610425cfa161c35c6a758b (patch)
tree9b5dd9ad1665fad8424dfbdc5bd93b531f714b09
parent9b45f468c61a0756dd19d09b64c2b1a88cc99ec5 (diff)
parent5bcf802213e801c4d52102612f007defa5d0397f (diff)
downloadgo-git-932ced9f55f556de02610425cfa161c35c6a758b.tar.gz
Merge pull request #339 from mcuadros/status
worktree, status and reset implementation based on merkletrie
-rw-r--r--_examples/checkout/main.go26
-rw-r--r--_examples/common_test.go2
-rw-r--r--options.go66
-rw-r--r--plumbing/format/index/index.go26
-rw-r--r--plumbing/object/difftree.go4
-rw-r--r--plumbing/object/tree.go8
-rw-r--r--plumbing/object/tree_test.go6
-rw-r--r--plumbing/object/treenoder.go26
-rw-r--r--repository.go20
-rw-r--r--status.go70
-rw-r--r--submodule.go6
-rw-r--r--submodule_test.go5
-rw-r--r--utils/merkletrie/filesystem/node.go144
-rw-r--r--utils/merkletrie/filesystem/node_test.go114
-rw-r--r--utils/merkletrie/index/node.go86
-rw-r--r--utils/merkletrie/index/node_test.go108
-rw-r--r--worktree.go443
-rw-r--r--worktree_status.go131
-rw-r--r--worktree_test.go289
19 files changed, 1288 insertions, 292 deletions
diff --git a/_examples/checkout/main.go b/_examples/checkout/main.go
index 24f926a..2c54550 100644
--- a/_examples/checkout/main.go
+++ b/_examples/checkout/main.go
@@ -11,25 +11,37 @@ import (
// Basic example of how to checkout a specific commit.
func main() {
- CheckArgs("<url>", "<directory>", "<commit-ref>")
- url, directory, commitRef := os.Args[1], os.Args[2], os.Args[3]
+ CheckArgs("<url>", "<directory>", "<commit>")
+ url, directory, commit := os.Args[1], os.Args[2], os.Args[3]
// Clone the given repository to the given directory
Info("git clone %s %s", url, directory)
-
r, err := git.PlainClone(directory, false, &git.CloneOptions{
URL: url,
})
CheckIfError(err)
- Info("git checkout %s", commitRef)
+ // ... retrieving the commit being pointed by HEAD
+ Info("git show-ref --head HEAD")
+ ref, err := r.Head()
+ CheckIfError(err)
+ fmt.Println(ref.Hash())
w, err := r.Worktree()
-
CheckIfError(err)
- CheckIfError(w.Checkout(plumbing.NewHash(commitRef)))
+ // ... checking out to commit
+ Info("git checkout %s", commit)
+ err = w.Checkout(&git.CheckoutOptions{
+ Hash: plumbing.NewHash(commit),
+ })
+ CheckIfError(err)
- fmt.Println("voila")
+ // ... retrieving the commit being pointed by HEAD, it's shows that the
+ // repository is poiting to the giving commit in detached mode
+ Info("git show-ref --head HEAD")
+ ref, err = r.Head()
+ CheckIfError(err)
+ fmt.Println(ref.Hash())
}
diff --git a/_examples/common_test.go b/_examples/common_test.go
index d812f2b..5543eaf 100644
--- a/_examples/common_test.go
+++ b/_examples/common_test.go
@@ -13,7 +13,7 @@ import (
var examplesTest = flag.Bool("examples", false, "run the examples tests")
-var defaultURL = "https://github.com/mcuadros/basic.git"
+var defaultURL = "https://github.com/git-fixtures/basic.git"
var args = map[string][]string{
"checkout": []string{defaultURL, tempFolder(), "35e85108805c84807bc66a02d91535e1e24b38b9"},
diff --git a/options.go b/options.go
index d033654..50fba10 100644
--- a/options.go
+++ b/options.go
@@ -178,6 +178,72 @@ type SubmoduleUpdateOptions struct {
RecurseSubmodules SubmoduleRescursivity
}
+// CheckoutOptions describes how a checkout 31operation should be performed.
+type CheckoutOptions struct {
+ // Hash to be checked out, if used HEAD will in detached mode. Branch and
+ // Hash are mutual exclusive.
+ Hash plumbing.Hash
+ // Branch to be checked out, if Branch and Hash are empty is set to `master`.
+ Branch plumbing.ReferenceName
+ // Force, if true when switching branches, proceed even if the index or the
+ // working tree differs from HEAD. This is used to throw away local changes
+ Force bool
+}
+
+// Validate validates the fields and sets the default values.
+func (o *CheckoutOptions) Validate() error {
+ if o.Branch == "" {
+ o.Branch = plumbing.Master
+ }
+
+ return nil
+}
+
+// ResetMode defines the mode of a reset operation.
+type ResetMode int8
+
+const (
+ // HardReset resets the index and working tree. Any changes to tracked files
+ // in the working tree are discarded.
+ HardReset ResetMode = iota
+ // MixedReset resets the index but not the working tree (i.e., the changed
+ // files are preserved but not marked for commit) and reports what has not
+ // been updated. This is the default action.
+ MixedReset
+ // MergeReset resets the index and updates the files in the working tree
+ // that are different between Commit and HEAD, but keeps those which are
+ // different between the index and working tree (i.e. which have changes
+ // which have not been added).
+ //
+ // If a file that is different between Commit and the index has unstaged
+ // changes, reset is aborted.
+ MergeReset
+)
+
+// ResetOptions describes how a reset operation should be performed.
+type ResetOptions struct {
+ // Commit, if commit is pressent set the current branch head (HEAD) to it.
+ Commit plumbing.Hash
+ // Mode, form resets the current branch head to Commit and possibly updates
+ // the index (resetting it to the tree of Commit) and the working tree
+ // depending on Mode. If empty MixedReset is used.
+ Mode ResetMode
+}
+
+// Validate validates the fields and sets the default values.
+func (o *ResetOptions) Validate(r *Repository) error {
+ if o.Commit == plumbing.ZeroHash {
+ ref, err := r.Head()
+ if err != nil {
+ return err
+ }
+
+ o.Commit = ref.Hash()
+ }
+
+ return nil
+}
+
// LogOptions describes how a log action should be performed.
type LogOptions struct {
// When the From option is set the log will only contain commits
diff --git a/plumbing/format/index/index.go b/plumbing/format/index/index.go
index ee50efd..61e7d66 100644
--- a/plumbing/format/index/index.go
+++ b/plumbing/format/index/index.go
@@ -2,8 +2,11 @@ package index
import (
"errors"
+ "fmt"
"time"
+ "bytes"
+
"gopkg.in/src-d/go-git.v4/plumbing"
"gopkg.in/src-d/go-git.v4/plumbing/filemode"
)
@@ -47,6 +50,16 @@ type Index struct {
ResolveUndo *ResolveUndo
}
+// String is equivalent to `git ls-files --stage --debug`
+func (i *Index) String() string {
+ buf := bytes.NewBuffer(nil)
+ for _, e := range i.Entries {
+ buf.WriteString(e.String())
+ }
+
+ return buf.String()
+}
+
// Entry represents a single file (or stage of a file) in the cache. An entry
// represents exactly one stage of a file. If a file path is unmerged then
// multiple Entry instances may appear for the same path name.
@@ -78,6 +91,19 @@ type Entry struct {
IntentToAdd bool
}
+func (e Entry) String() string {
+ buf := bytes.NewBuffer(nil)
+
+ fmt.Fprintf(buf, "%06o %s %d\t%s\n", e.Mode, e.Hash, e.Stage, e.Name)
+ fmt.Fprintf(buf, " ctime: %d:%d\n", e.CreatedAt.Unix(), e.CreatedAt.Nanosecond())
+ fmt.Fprintf(buf, " mtime: %d:%d\n", e.ModifiedAt.Unix(), e.ModifiedAt.Nanosecond())
+ fmt.Fprintf(buf, " dev: %d\tino: %d\n", e.Dev, e.Inode)
+ fmt.Fprintf(buf, " uid: %d\tgid: %d\n", e.UID, e.GID)
+ fmt.Fprintf(buf, " size: %d\tflags: %x\n", e.Size, 0)
+
+ return buf.String()
+}
+
// Tree contains pre-computed hashes for trees that can be derived from the
// index. It helps speed up tree object generation from index for a new commit.
type Tree struct {
diff --git a/plumbing/object/difftree.go b/plumbing/object/difftree.go
index 87a7153..ac58c4d 100644
--- a/plumbing/object/difftree.go
+++ b/plumbing/object/difftree.go
@@ -10,8 +10,8 @@ import (
// DiffTree compares the content and mode of the blobs found via two
// tree objects.
func DiffTree(a, b *Tree) (Changes, error) {
- from := newTreeNoder(a)
- to := newTreeNoder(b)
+ from := NewTreeRootNode(a)
+ to := NewTreeRootNode(b)
hashEqual := func(a, b noder.Hasher) bool {
return bytes.Equal(a.Hash(), b.Hash())
diff --git a/plumbing/object/tree.go b/plumbing/object/tree.go
index e70b5cd..d2265a8 100644
--- a/plumbing/object/tree.go
+++ b/plumbing/object/tree.go
@@ -67,7 +67,7 @@ type TreeEntry struct {
// File returns the hash of the file identified by the `path` argument.
// The path is interpreted as relative to the tree receiver.
func (t *Tree) File(path string) (*File, error) {
- e, err := t.findEntry(path)
+ e, err := t.FindEntry(path)
if err != nil {
return nil, ErrFileNotFound
}
@@ -86,7 +86,7 @@ func (t *Tree) File(path string) (*File, error) {
// Tree returns the tree identified by the `path` argument.
// The path is interpreted as relative to the tree receiver.
func (t *Tree) Tree(path string) (*Tree, error) {
- e, err := t.findEntry(path)
+ e, err := t.FindEntry(path)
if err != nil {
return nil, ErrDirectoryNotFound
}
@@ -109,7 +109,8 @@ func (t *Tree) TreeEntryFile(e *TreeEntry) (*File, error) {
return NewFile(e.Name, e.Mode, blob), nil
}
-func (t *Tree) findEntry(path string) (*TreeEntry, error) {
+// FindEntry search a TreeEntry in this tree or any subtree.
+func (t *Tree) FindEntry(path string) (*TreeEntry, error) {
pathParts := strings.Split(path, "/")
var tree *Tree
@@ -146,6 +147,7 @@ func (t *Tree) entry(baseName string) (*TreeEntry, error) {
if t.m == nil {
t.buildMap()
}
+
entry, ok := t.m[baseName]
if !ok {
return nil, errEntryNotFound
diff --git a/plumbing/object/tree_test.go b/plumbing/object/tree_test.go
index cf5ad5f..aa86517 100644
--- a/plumbing/object/tree_test.go
+++ b/plumbing/object/tree_test.go
@@ -107,6 +107,12 @@ func (s *TreeSuite) TestFiles(c *C) {
c.Assert(count, Equals, 9)
}
+func (s *TreeSuite) TestFindEntry(c *C) {
+ e, err := s.Tree.FindEntry("vendor/foo.go")
+ c.Assert(err, IsNil)
+ c.Assert(e.Name, Equals, "foo.go")
+}
+
// This plumbing.EncodedObject implementation has a reader that only returns 6
// bytes at a time, this should simulate the conditions when a read
// returns less bytes than asked, for example when reading a hash which
diff --git a/plumbing/object/treenoder.go b/plumbing/object/treenoder.go
index 4da8298..bd65abc 100644
--- a/plumbing/object/treenoder.go
+++ b/plumbing/object/treenoder.go
@@ -1,13 +1,5 @@
package object
-// A treenoder is a helper type that wraps git trees into merkletrie
-// noders.
-//
-// As a merkletrie noder doesn't understand the concept of modes (e.g.
-// file permissions), the treenoder includes the mode of the git tree in
-// the hash, so changes in the modes will be detected as modifications
-// to the file contents by the merkletrie difftree algorithm. This is
-// consistent with how the "git diff-tree" command works.
import (
"io"
@@ -16,6 +8,14 @@ import (
"gopkg.in/src-d/go-git.v4/utils/merkletrie/noder"
)
+// A treenoder is a helper type that wraps git trees into merkletrie
+// noders.
+//
+// As a merkletrie noder doesn't understand the concept of modes (e.g.
+// file permissions), the treenoder includes the mode of the git tree in
+// the hash, so changes in the modes will be detected as modifications
+// to the file contents by the merkletrie difftree algorithm. This is
+// consistent with how the "git diff-tree" command works.
type treeNoder struct {
parent *Tree // the root node is its own parent
name string // empty string for the root node
@@ -24,7 +24,8 @@ type treeNoder struct {
children []noder.Noder // memoized
}
-func newTreeNoder(t *Tree) *treeNoder {
+// NewTreeRootNode returns the root node of a Tree
+func NewTreeRootNode(t *Tree) noder.Noder {
if t == nil {
return &treeNoder{}
}
@@ -45,13 +46,6 @@ func (t *treeNoder) String() string {
return "treeNoder <" + t.name + ">"
}
-// The hash of a treeNoder is the result of concatenating the hash of
-// its contents and its mode; that way the difftree algorithm will
-// detect changes in the contents of files and also in their mode.
-//
-// Files with Regular and Deprecated file modes are considered the same
-// for the purpose of difftree, so Regular will be used as the mode for
-// Deprecated files here.
func (t *treeNoder) Hash() []byte {
if t.mode == filemode.Deprecated {
return append(t.hash[:], filemode.Regular.Bytes()...)
diff --git a/repository.go b/repository.go
index d9a1d7e..bb59afe 100644
--- a/repository.go
+++ b/repository.go
@@ -340,11 +340,11 @@ func (r *Repository) clone(o *CloneOptions) error {
return err
}
- if _, err := r.updateReferences(c.Fetch, o.ReferenceName, head); err != nil {
+ if _, err := r.updateReferences(c.Fetch, head); err != nil {
return err
}
- if err := r.updateWorktree(); err != nil {
+ if err := r.updateWorktree(head.Name()); err != nil {
return err
}
@@ -429,7 +429,7 @@ func (r *Repository) updateRemoteConfig(remote *Remote, o *CloneOptions,
}
func (r *Repository) updateReferences(spec []config.RefSpec,
- headName plumbing.ReferenceName, resolvedHead *plumbing.Reference) (updated bool, err error) {
+ resolvedHead *plumbing.Reference) (updated bool, err error) {
if !resolvedHead.IsBranch() {
// Detached HEAD mode
@@ -534,7 +534,7 @@ func (r *Repository) Pull(o *PullOptions) error {
return err
}
- refsUpdated, err := r.updateReferences(remote.c.Fetch, o.ReferenceName, head)
+ refsUpdated, err := r.updateReferences(remote.c.Fetch, head)
if err != nil {
return err
}
@@ -547,7 +547,7 @@ func (r *Repository) Pull(o *PullOptions) error {
return NoErrAlreadyUpToDate
}
- if err := r.updateWorktree(); err != nil {
+ if err := r.updateWorktree(head.Name()); err != nil {
return err
}
@@ -560,22 +560,24 @@ func (r *Repository) Pull(o *PullOptions) error {
return nil
}
-func (r *Repository) updateWorktree() error {
+func (r *Repository) updateWorktree(branch plumbing.ReferenceName) error {
if r.wt == nil {
return nil
}
- w, err := r.Worktree()
+ b, err := r.Reference(branch, true)
if err != nil {
return err
}
- h, err := r.Head()
+ w, err := r.Worktree()
if err != nil {
return err
}
- return w.Checkout(h.Hash())
+ return w.Reset(&ResetOptions{
+ Commit: b.Hash(),
+ })
}
// Fetch fetches changes from a remote repository.
diff --git a/status.go b/status.go
new file mode 100644
index 0000000..2517e50
--- /dev/null
+++ b/status.go
@@ -0,0 +1,70 @@
+package git
+
+import "fmt"
+import "bytes"
+
+// Status represents the current status of a Worktree.
+// The key of the map is the path of the file.
+type Status map[string]*FileStatus
+
+// File returns the FileStatus for a given path, if the FileStatus doesn't
+// exists a new FileStatus is added to the map using the path as key.
+func (s Status) File(path string) *FileStatus {
+ if _, ok := (s)[path]; !ok {
+ s[path] = &FileStatus{Worktree: Unmodified, Staging: Unmodified}
+ }
+
+ return s[path]
+}
+
+// IsClean returns true if all the files aren't in Unmodified status.
+func (s Status) IsClean() bool {
+ for _, status := range s {
+ if status.Worktree != Unmodified || status.Staging != Unmodified {
+ return false
+ }
+ }
+
+ return true
+}
+
+func (s Status) String() string {
+ buf := bytes.NewBuffer(nil)
+ for path, status := range s {
+ if status.Staging == Unmodified && status.Worktree == Unmodified {
+ continue
+ }
+
+ if status.Staging == Renamed {
+ path = fmt.Sprintf("%s -> %s", path, status.Extra)
+ }
+
+ fmt.Fprintf(buf, "%c%c %s\n", status.Staging, status.Worktree, path)
+ }
+
+ return buf.String()
+}
+
+// FileStatus contains the status of a file in the worktree
+type FileStatus struct {
+ // Staging is the status of a file in the staging area
+ Staging StatusCode
+ // Worktree is the status of a file in the worktree
+ Worktree StatusCode
+ // Extra contains extra information, such as the previous name in a rename
+ Extra string
+}
+
+// StatusCode status code of a file in the Worktree
+type StatusCode byte
+
+const (
+ Unmodified StatusCode = ' '
+ Untracked StatusCode = '?'
+ Modified StatusCode = 'M'
+ Added StatusCode = 'A'
+ Deleted StatusCode = 'D'
+ Renamed StatusCode = 'R'
+ Copied StatusCode = 'C'
+ UpdatedButUnmerged StatusCode = 'U'
+)
diff --git a/submodule.go b/submodule.go
index 69c5d75..c711a2b 100644
--- a/submodule.go
+++ b/submodule.go
@@ -103,10 +103,10 @@ func (s *Submodule) Update(o *SubmoduleUpdateOptions) error {
return err
}
- return s.doRecrusiveUpdate(r, o)
+ return s.doRecursiveUpdate(r, o)
}
-func (s *Submodule) doRecrusiveUpdate(r *Repository, o *SubmoduleUpdateOptions) error {
+func (s *Submodule) doRecursiveUpdate(r *Repository, o *SubmoduleUpdateOptions) error {
if o.RecurseSubmodules == NoRecurseSubmodules {
return nil
}
@@ -140,7 +140,7 @@ func (s *Submodule) fetchAndCheckout(r *Repository, o *SubmoduleUpdateOptions, h
return err
}
- if err := w.Checkout(hash); err != nil {
+ if err := w.Checkout(&CheckoutOptions{Hash: hash}); err != nil {
return err
}
diff --git a/submodule_test.go b/submodule_test.go
index e367a10..88afa18 100644
--- a/submodule_test.go
+++ b/submodule_test.go
@@ -26,8 +26,8 @@ func (s *SubmoduleSuite) SetUpTest(c *C) {
dir, err := ioutil.TempDir("", "submodule")
c.Assert(err, IsNil)
- r, err := PlainClone(dir, false, &CloneOptions{
- URL: fmt.Sprintf("file://%s", filepath.Join(path)),
+ r, err := PlainClone(filepath.Join(dir, "worktree"), false, &CloneOptions{
+ URL: fmt.Sprintf("file://%s", path),
})
c.Assert(err, IsNil)
@@ -74,7 +74,6 @@ func (s *SubmoduleSuite) TestUpdate(c *C) {
ref, err := r.Reference(plumbing.HEAD, true)
c.Assert(err, IsNil)
c.Assert(ref.Hash().String(), Equals, "6ecf0ef2c2dffb796033e5a02219af86ec6584e5")
-
}
func (s *SubmoduleSuite) TestUpdateWithoutInit(c *C) {
diff --git a/utils/merkletrie/filesystem/node.go b/utils/merkletrie/filesystem/node.go
new file mode 100644
index 0000000..6c09d29
--- /dev/null
+++ b/utils/merkletrie/filesystem/node.go
@@ -0,0 +1,144 @@
+package filesystem
+
+import (
+ "io"
+ "os"
+ "path/filepath"
+
+ "gopkg.in/src-d/go-billy.v2"
+ "gopkg.in/src-d/go-git.v4/plumbing"
+ "gopkg.in/src-d/go-git.v4/plumbing/filemode"
+ "gopkg.in/src-d/go-git.v4/utils/merkletrie/noder"
+)
+
+var ignore = map[string]bool{
+ ".git": true,
+}
+
+// The node represents a file or a directory in a billy.Filesystem. It
+// implements the interface noder.Noder of merkletrie package.
+//
+// This implementation implements a "standard" hash method being able to be
+// compared with any other noder.Noder implementation inside of go-git.
+type node struct {
+ fs billy.Filesystem
+
+ path string
+ hash []byte
+ children []noder.Noder
+ isDir bool
+}
+
+// NewRootNode returns the root node based on a given billy.Filesystem
+func NewRootNode(fs billy.Filesystem) noder.Noder {
+ return &node{fs: fs, isDir: true}
+}
+
+// Hash the hash of a filesystem is the result of concatenating the computed
+// plumbing.Hash of the file as a Blob and its plumbing.FileMode; that way the
+// difftree algorithm will detect changes in the contents of files and also in
+// their mode.
+//
+// The hash of a directory is always a 24-bytes slice of zero values
+func (n *node) Hash() []byte {
+ return n.hash
+}
+
+func (n *node) Name() string {
+ return filepath.Base(n.path)
+}
+
+func (n *node) IsDir() bool {
+ return n.isDir
+}
+
+func (n *node) Children() ([]noder.Noder, error) {
+ if err := n.calculateChildren(); err != nil {
+ return nil, err
+ }
+
+ return n.children, nil
+}
+
+func (n *node) NumChildren() (int, error) {
+ if err := n.calculateChildren(); err != nil {
+ return -1, err
+ }
+
+ return len(n.children), nil
+}
+
+func (n *node) calculateChildren() error {
+ if len(n.children) != 0 {
+ return nil
+ }
+
+ files, err := n.fs.ReadDir(n.path)
+ if err != nil {
+ if os.IsNotExist(err) {
+ return nil
+ }
+
+ return nil
+ }
+
+ for _, file := range files {
+ if _, ok := ignore[file.Name()]; ok {
+ continue
+ }
+
+ c, err := n.newChildNode(file)
+ if err != nil {
+ return err
+ }
+
+ n.children = append(n.children, c)
+ }
+
+ return nil
+}
+
+func (n *node) newChildNode(file billy.FileInfo) (*node, error) {
+ path := filepath.Join(n.path, file.Name())
+ hash, err := n.calculateHash(path, file)
+ if err != nil {
+ return nil, err
+ }
+
+ return &node{
+ fs: n.fs,
+ path: path,
+ hash: hash,
+ isDir: file.IsDir(),
+ }, nil
+}
+
+func (n *node) calculateHash(path string, file billy.FileInfo) ([]byte, error) {
+ if file.IsDir() {
+ return make([]byte, 24), nil
+ }
+
+ f, err := n.fs.Open(path)
+ if err != nil {
+ return nil, err
+ }
+
+ defer f.Close()
+
+ h := plumbing.NewHasher(plumbing.BlobObject, file.Size())
+ if _, err := io.Copy(h, f); err != nil {
+ return nil, err
+ }
+
+ mode, err := filemode.NewFromOSFileMode(file.Mode())
+ if err != nil {
+ return nil, err
+ }
+
+ hash := h.Sum()
+ return append(hash[:], mode.Bytes()...), nil
+}
+
+func (n *node) String() string {
+ return n.path
+}
diff --git a/utils/merkletrie/filesystem/node_test.go b/utils/merkletrie/filesystem/node_test.go
new file mode 100644
index 0000000..b7c124d
--- /dev/null
+++ b/utils/merkletrie/filesystem/node_test.go
@@ -0,0 +1,114 @@
+package filesystem
+
+import (
+ "bytes"
+ "io"
+ "os"
+ "testing"
+
+ . "gopkg.in/check.v1"
+ "gopkg.in/src-d/go-billy.v2"
+ "gopkg.in/src-d/go-billy.v2/memfs"
+ "gopkg.in/src-d/go-git.v4/utils/merkletrie"
+ "gopkg.in/src-d/go-git.v4/utils/merkletrie/noder"
+)
+
+func Test(t *testing.T) { TestingT(t) }
+
+type NoderSuite struct{}
+
+var _ = Suite(&NoderSuite{})
+
+func (s *NoderSuite) TestDiff(c *C) {
+ fsA := memfs.New()
+ WriteFile(fsA, "foo", []byte("foo"), 0644)
+ WriteFile(fsA, "qux/bar", []byte("foo"), 0644)
+ WriteFile(fsA, "qux/qux", []byte("foo"), 0644)
+
+ fsB := memfs.New()
+ WriteFile(fsB, "foo", []byte("foo"), 0644)
+ WriteFile(fsB, "qux/bar", []byte("foo"), 0644)
+ WriteFile(fsB, "qux/qux", []byte("foo"), 0644)
+
+ ch, err := merkletrie.DiffTree(NewRootNode(fsA), NewRootNode(fsB), IsEquals)
+ c.Assert(err, IsNil)
+ c.Assert(ch, HasLen, 0)
+}
+
+func (s *NoderSuite) TestDiffChangeContent(c *C) {
+ fsA := memfs.New()
+ WriteFile(fsA, "foo", []byte("foo"), 0644)
+ WriteFile(fsA, "qux/bar", []byte("foo"), 0644)
+ WriteFile(fsA, "qux/qux", []byte("foo"), 0644)
+
+ fsB := memfs.New()
+ WriteFile(fsB, "foo", []byte("foo"), 0644)
+ WriteFile(fsB, "qux/bar", []byte("bar"), 0644)
+ WriteFile(fsB, "qux/qux", []byte("foo"), 0644)
+
+ ch, err := merkletrie.DiffTree(NewRootNode(fsA), NewRootNode(fsB), IsEquals)
+ c.Assert(err, IsNil)
+ c.Assert(ch, HasLen, 1)
+}
+
+func (s *NoderSuite) TestDiffChangeMissing(c *C) {
+ fsA := memfs.New()
+ WriteFile(fsA, "foo", []byte("foo"), 0644)
+
+ fsB := memfs.New()
+ WriteFile(fsB, "bar", []byte("bar"), 0644)
+
+ ch, err := merkletrie.DiffTree(NewRootNode(fsA), NewRootNode(fsB), IsEquals)
+ c.Assert(err, IsNil)
+ c.Assert(ch, HasLen, 2)
+}
+
+func (s *NoderSuite) TestDiffChangeMode(c *C) {
+ fsA := memfs.New()
+ WriteFile(fsA, "foo", []byte("foo"), 0644)
+
+ fsB := memfs.New()
+ WriteFile(fsB, "foo", []byte("foo"), 0755)
+
+ ch, err := merkletrie.DiffTree(NewRootNode(fsA), NewRootNode(fsB), IsEquals)
+ c.Assert(err, IsNil)
+ c.Assert(ch, HasLen, 1)
+}
+
+func (s *NoderSuite) TestDiffChangeModeNotRelevant(c *C) {
+ fsA := memfs.New()
+ WriteFile(fsA, "foo", []byte("foo"), 0644)
+
+ fsB := memfs.New()
+ WriteFile(fsB, "foo", []byte("foo"), 0655)
+
+ ch, err := merkletrie.DiffTree(NewRootNode(fsA), NewRootNode(fsB), IsEquals)
+ c.Assert(err, IsNil)
+ c.Assert(ch, HasLen, 0)
+}
+
+func WriteFile(fs billy.Filesystem, filename string, data []byte, perm os.FileMode) error {
+ f, err := fs.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, perm)
+ if err != nil {
+ return err
+ }
+
+ n, err := f.Write(data)
+ if err == nil && n < len(data) {
+ err = io.ErrShortWrite
+ }
+ if err1 := f.Close(); err == nil {
+ err = err1
+ }
+ return err
+}
+
+var empty = make([]byte, 24)
+
+func IsEquals(a, b noder.Hasher) bool {
+ if bytes.Equal(a.Hash(), empty) || bytes.Equal(b.Hash(), empty) {
+ return false
+ }
+
+ return bytes.Equal(a.Hash(), b.Hash())
+}
diff --git a/utils/merkletrie/index/node.go b/utils/merkletrie/index/node.go
new file mode 100644
index 0000000..2c72f6d
--- /dev/null
+++ b/utils/merkletrie/index/node.go
@@ -0,0 +1,86 @@
+package index
+
+import (
+ "path/filepath"
+ "strings"
+
+ "gopkg.in/src-d/go-git.v4/plumbing/format/index"
+ "gopkg.in/src-d/go-git.v4/utils/merkletrie/noder"
+)
+
+// The node represents a index.Entry or a directory inferred from the path
+// of all entries. It implements the interface noder.Noder of merkletrie
+// package.
+//
+// This implementation implements a "standard" hash method being able to be
+// compared with any other noder.Noder implementation inside of go-git
+type node struct {
+ path string
+ entry index.Entry
+ children []noder.Noder
+ isDir bool
+}
+
+// NewRootNode returns the root node of a computed tree from a index.Index,
+func NewRootNode(idx *index.Index) noder.Noder {
+ const rootNode = ""
+
+ m := map[string]*node{rootNode: {isDir: true}}
+
+ for _, e := range idx.Entries {
+ parts := strings.Split(e.Name, string(filepath.Separator))
+
+ var path string
+ for _, part := range parts {
+ parent := path
+ path = filepath.Join(path, part)
+
+ if _, ok := m[path]; ok {
+ continue
+ }
+
+ n := &node{path: path}
+ if path == e.Name {
+ n.entry = e
+ } else {
+ n.isDir = true
+ }
+
+ m[n.path] = n
+ m[parent].children = append(m[parent].children, n)
+ }
+ }
+
+ return m[rootNode]
+}
+
+func (n *node) String() string {
+ return n.path
+}
+
+// Hash the hash of a filesystem is a 24-byte slice, is the result of
+// concatenating the computed plumbing.Hash of the file as a Blob and its
+// plumbing.FileMode; that way the difftree algorithm will detect changes in the
+// contents of files and also in their mode.
+//
+// If the node is computed and not based on a index.Entry the hash is equals
+// to a 24-bytes slices of zero values.
+func (n *node) Hash() []byte {
+ return append(n.entry.Hash[:], n.entry.Mode.Bytes()...)
+}
+
+func (n *node) Name() string {
+ return filepath.Base(n.path)
+}
+
+func (n *node) IsDir() bool {
+ return n.isDir
+}
+
+func (n *node) Children() ([]noder.Noder, error) {
+ return n.children, nil
+}
+
+func (n *node) NumChildren() (int, error) {
+ return len(n.children), nil
+}
diff --git a/utils/merkletrie/index/node_test.go b/utils/merkletrie/index/node_test.go
new file mode 100644
index 0000000..48aa35f
--- /dev/null
+++ b/utils/merkletrie/index/node_test.go
@@ -0,0 +1,108 @@
+package index
+
+import (
+ "bytes"
+ "testing"
+
+ . "gopkg.in/check.v1"
+ "gopkg.in/src-d/go-git.v4/plumbing"
+ "gopkg.in/src-d/go-git.v4/plumbing/format/index"
+ "gopkg.in/src-d/go-git.v4/utils/merkletrie"
+ "gopkg.in/src-d/go-git.v4/utils/merkletrie/noder"
+)
+
+func Test(t *testing.T) { TestingT(t) }
+
+type NoderSuite struct{}
+
+var _ = Suite(&NoderSuite{})
+
+func (s *NoderSuite) TestDiff(c *C) {
+ indexA := &index.Index{
+ Entries: []index.Entry{
+ {Name: "foo", Hash: plumbing.NewHash("8ab686eafeb1f44702738c8b0f24f2567c36da6d")},
+ {Name: "bar/foo", Hash: plumbing.NewHash("8ab686eafeb1f44702738c8b0f24f2567c36da6d")},
+ {Name: "bar/qux", Hash: plumbing.NewHash("8ab686eafeb1f44702738c8b0f24f2567c36da6d")},
+ {Name: "bar/baz/foo", Hash: plumbing.NewHash("8ab686eafeb1f44702738c8b0f24f2567c36da6d")},
+ },
+ }
+
+ indexB := &index.Index{
+ Entries: []index.Entry{
+ {Name: "foo", Hash: plumbing.NewHash("8ab686eafeb1f44702738c8b0f24f2567c36da6d")},
+ {Name: "bar/foo", Hash: plumbing.NewHash("8ab686eafeb1f44702738c8b0f24f2567c36da6d")},
+ {Name: "bar/qux", Hash: plumbing.NewHash("8ab686eafeb1f44702738c8b0f24f2567c36da6d")},
+ {Name: "bar/baz/foo", Hash: plumbing.NewHash("8ab686eafeb1f44702738c8b0f24f2567c36da6d")},
+ },
+ }
+
+ ch, err := merkletrie.DiffTree(NewRootNode(indexA), NewRootNode(indexB), isEquals)
+ c.Assert(err, IsNil)
+ c.Assert(ch, HasLen, 0)
+}
+
+func (s *NoderSuite) TestDiffChange(c *C) {
+ indexA := &index.Index{
+ Entries: []index.Entry{
+ {Name: "bar/baz/bar", Hash: plumbing.NewHash("8ab686eafeb1f44702738c8b0f24f2567c36da6d")},
+ },
+ }
+
+ indexB := &index.Index{
+ Entries: []index.Entry{
+ {Name: "bar/baz/foo", Hash: plumbing.NewHash("8ab686eafeb1f44702738c8b0f24f2567c36da6d")},
+ },
+ }
+
+ ch, err := merkletrie.DiffTree(NewRootNode(indexA), NewRootNode(indexB), isEquals)
+ c.Assert(err, IsNil)
+ c.Assert(ch, HasLen, 2)
+}
+
+func (s *NoderSuite) TestDiffDir(c *C) {
+ indexA := &index.Index{
+ Entries: []index.Entry{
+ {Name: "foo", Hash: plumbing.NewHash("8ab686eafeb1f44702738c8b0f24f2567c36da6d")},
+ },
+ }
+
+ indexB := &index.Index{
+ Entries: []index.Entry{
+ {Name: "foo/bar", Hash: plumbing.NewHash("8ab686eafeb1f44702738c8b0f24f2567c36da6d")},
+ },
+ }
+
+ ch, err := merkletrie.DiffTree(NewRootNode(indexA), NewRootNode(indexB), isEquals)
+ c.Assert(err, IsNil)
+ c.Assert(ch, HasLen, 2)
+}
+
+func (s *NoderSuite) TestDiffSameRoot(c *C) {
+ indexA := &index.Index{
+ Entries: []index.Entry{
+ {Name: "foo.go", Hash: plumbing.NewHash("aab686eafeb1f44702738c8b0f24f2567c36da6d")},
+ {Name: "foo/bar", Hash: plumbing.NewHash("8ab686eafeb1f44702738c8b0f24f2567c36da6d")},
+ },
+ }
+
+ indexB := &index.Index{
+ Entries: []index.Entry{
+ {Name: "foo/bar", Hash: plumbing.NewHash("8ab686eafeb1f44702738c8b0f24f2567c36da6d")},
+ {Name: "foo.go", Hash: plumbing.NewHash("8ab686eafeb1f44702738c8b0f24f2567c36da6d")},
+ },
+ }
+
+ ch, err := merkletrie.DiffTree(NewRootNode(indexA), NewRootNode(indexB), isEquals)
+ c.Assert(err, IsNil)
+ c.Assert(ch, HasLen, 1)
+}
+
+var empty = make([]byte, 24)
+
+func isEquals(a, b noder.Hasher) bool {
+ if bytes.Equal(a.Hash(), empty) || bytes.Equal(b.Hash(), empty) {
+ return false
+ }
+
+ return bytes.Equal(a.Hash(), b.Hash())
+}
diff --git a/worktree.go b/worktree.go
index 40ebe58..ec8fad2 100644
--- a/worktree.go
+++ b/worktree.go
@@ -12,89 +12,261 @@ import (
"gopkg.in/src-d/go-git.v4/plumbing/filemode"
"gopkg.in/src-d/go-git.v4/plumbing/format/index"
"gopkg.in/src-d/go-git.v4/plumbing/object"
+ "gopkg.in/src-d/go-git.v4/utils/merkletrie"
"gopkg.in/src-d/go-billy.v2"
)
-var ErrWorktreeNotClean = errors.New("worktree is not clean")
-var ErrSubmoduleNotFound = errors.New("submodule not found")
+var (
+ ErrWorktreeNotClean = errors.New("worktree is not clean")
+ ErrSubmoduleNotFound = errors.New("submodule not found")
+ ErrUnstaggedChanges = errors.New("worktree contains unstagged changes")
+)
+// Worktree represents a git worktree.
type Worktree struct {
r *Repository
fs billy.Filesystem
}
-func (w *Worktree) Checkout(commit plumbing.Hash) error {
- s, err := w.Status()
- if err != nil {
+// Checkout switch branches or restore working tree files.
+func (w *Worktree) Checkout(opts *CheckoutOptions) error {
+ if err := opts.Validate(); err != nil {
return err
}
- if !s.IsClean() {
- return ErrWorktreeNotClean
+ if !opts.Force {
+ unstaged, err := w.cointainsUnstagedChanges()
+ if err != nil {
+ return err
+ }
+
+ if unstaged {
+ return ErrUnstaggedChanges
+ }
}
- c, err := w.r.CommitObject(commit)
+ c, err := w.getCommitFromCheckoutOptions(opts)
if err != nil {
return err
}
- t, err := c.Tree()
+ ro := &ResetOptions{Commit: c, Mode: MergeReset}
+ if opts.Force {
+ ro.Mode = HardReset
+ }
+
+ if !opts.Hash.IsZero() {
+ err = w.setHEADToCommit(opts.Hash)
+ } else {
+ err = w.setHEADToBranch(opts.Branch, c)
+ }
+
if err != nil {
return err
}
- idx := &index.Index{Version: 2}
- walker := object.NewTreeWalker(t, true)
+ return w.Reset(ro)
+}
+
+func (w *Worktree) getCommitFromCheckoutOptions(opts *CheckoutOptions) (plumbing.Hash, error) {
+ if !opts.Hash.IsZero() {
+ return opts.Hash, nil
+ }
+
+ b, err := w.r.Reference(opts.Branch, true)
+ if err != nil {
+ return plumbing.ZeroHash, err
+ }
- for {
- name, entry, err := walker.Next()
- if err == io.EOF {
- break
+ if !b.IsTag() {
+ return b.Hash(), nil
+ }
+
+ o, err := w.r.Object(plumbing.AnyObject, b.Hash())
+ if err != nil {
+ return plumbing.ZeroHash, err
+ }
+
+ switch o := o.(type) {
+ case *object.Tag:
+ if o.TargetType != plumbing.CommitObject {
+ return plumbing.ZeroHash, fmt.Errorf("unsupported tag object target %q", o.TargetType)
}
+ return o.Target, nil
+ case *object.Commit:
+ return o.Hash, nil
+ }
+
+ return plumbing.ZeroHash, fmt.Errorf("unsupported tag target %q", o.Type())
+}
+
+func (w *Worktree) setHEADToCommit(commit plumbing.Hash) error {
+ head := plumbing.NewHashReference(plumbing.HEAD, commit)
+ return w.r.Storer.SetReference(head)
+}
+
+func (w *Worktree) setHEADToBranch(branch plumbing.ReferenceName, commit plumbing.Hash) error {
+ target, err := w.r.Storer.Reference(branch)
+ if err != nil {
+ return err
+ }
+
+ var head *plumbing.Reference
+ if target.IsBranch() {
+ head = plumbing.NewSymbolicReference(plumbing.HEAD, target.Name())
+ } else {
+ head = plumbing.NewHashReference(plumbing.HEAD, commit)
+ }
+
+ return w.r.Storer.SetReference(head)
+}
+
+// Reset the worktree to a specified state.
+func (w *Worktree) Reset(opts *ResetOptions) error {
+ if err := opts.Validate(w.r); err != nil {
+ return err
+ }
+
+ if opts.Mode == MergeReset {
+ unstaged, err := w.cointainsUnstagedChanges()
if err != nil {
return err
}
- if err := w.checkoutEntry(name, &entry, idx); err != nil {
+ if unstaged {
+ return ErrUnstaggedChanges
+ }
+ }
+
+ changes, err := w.diffCommitWithStaging(opts.Commit, true)
+ if err != nil {
+ return err
+ }
+
+ idx, err := w.r.Storer.Index()
+ if err != nil {
+ return err
+ }
+
+ t, err := w.getTreeFromCommitHash(opts.Commit)
+ if err != nil {
+ return err
+ }
+
+ for _, ch := range changes {
+ if err := w.checkoutChange(ch, t, idx); err != nil {
return err
}
}
- return w.r.Storer.SetIndex(idx)
+ if err := w.r.Storer.SetIndex(idx); err != nil {
+ return err
+ }
+
+ return w.setHEADCommit(opts.Commit)
}
-func (w *Worktree) checkoutEntry(name string, e *object.TreeEntry, idx *index.Index) error {
- if e.Mode == filemode.Submodule {
- return w.addIndexFromTreeEntry(name, e, idx)
+func (w *Worktree) cointainsUnstagedChanges() (bool, error) {
+ ch, err := w.diffStagingWithWorktree()
+ if err != nil {
+ return false, err
}
- if e.Mode == filemode.Dir {
- return nil
+ return len(ch) != 0, nil
+}
+
+func (w *Worktree) setHEADCommit(commit plumbing.Hash) error {
+ head, err := w.r.Reference(plumbing.HEAD, false)
+ if err != nil {
+ return err
+ }
+
+ if head.Type() == plumbing.HashReference {
+ head = plumbing.NewHashReference(plumbing.HEAD, commit)
+ return w.r.Storer.SetReference(head)
+ }
+
+ branch, err := w.r.Reference(head.Target(), false)
+ if err != nil {
+ return err
}
- return w.checkoutFile(name, e, idx)
+ if !branch.IsBranch() {
+ return fmt.Errorf("invalid HEAD target should be a branch, found %s", branch.Type())
+ }
+
+ branch = plumbing.NewHashReference(branch.Name(), commit)
+ return w.r.Storer.SetReference(branch)
}
-func (w *Worktree) checkoutFile(name string, e *object.TreeEntry, idx *index.Index) error {
- blob, err := object.GetBlob(w.r.Storer, e.Hash)
+func (w *Worktree) checkoutChange(ch merkletrie.Change, t *object.Tree, idx *index.Index) error {
+ a, err := ch.Action()
if err != nil {
return err
}
- from, err := blob.Reader()
+ switch a {
+ case merkletrie.Modify:
+ name := ch.To.String()
+ if err := w.rmIndexFromFile(name, idx); err != nil {
+ return err
+ }
+
+ // to apply perm changes the file is deleted, billy doesn't implement
+ // chmod
+ if err := w.fs.Remove(name); err != nil {
+ return err
+ }
+
+ fallthrough
+ case merkletrie.Insert:
+ name := ch.To.String()
+ e, err := t.FindEntry(name)
+ if err != nil {
+ return err
+ }
+
+ if e.Mode == filemode.Submodule {
+ return w.addIndexFromTreeEntry(name, e, idx)
+ }
+
+ f, err := t.File(name)
+ if err != nil {
+ return err
+ }
+
+ if err := w.checkoutFile(f); err != nil {
+ return err
+ }
+
+ return w.addIndexFromFile(name, e.Hash, idx)
+ case merkletrie.Delete:
+ name := ch.From.String()
+ if err := w.fs.Remove(name); err != nil {
+ return err
+ }
+
+ return w.rmIndexFromFile(name, idx)
+ }
+
+ return nil
+}
+
+func (w *Worktree) checkoutFile(f *object.File) error {
+ from, err := f.Reader()
if err != nil {
return err
}
defer from.Close()
- mode, err := e.Mode.ToOSFileMode()
+ mode, err := f.Mode.ToOSFileMode()
if err != nil {
return err
}
- to, err := w.fs.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, mode.Perm())
+ to, err := w.fs.OpenFile(f.Name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, mode.Perm())
if err != nil {
return err
}
@@ -104,11 +276,9 @@ func (w *Worktree) checkoutFile(name string, e *object.TreeEntry, idx *index.Ind
return err
}
- return w.addIndexFromFile(name, e, idx)
+ return err
}
-var fillSystemInfo func(e *index.Entry, sys interface{})
-
func (w *Worktree) addIndexFromTreeEntry(name string, f *object.TreeEntry, idx *index.Index) error {
idx.Entries = append(idx.Entries, index.Entry{
Hash: f.Hash,
@@ -119,7 +289,7 @@ func (w *Worktree) addIndexFromTreeEntry(name string, f *object.TreeEntry, idx *
return nil
}
-func (w *Worktree) addIndexFromFile(name string, f *object.TreeEntry, idx *index.Index) error {
+func (w *Worktree) addIndexFromFile(name string, h plumbing.Hash, idx *index.Index) error {
fi, err := w.fs.Stat(name)
if err != nil {
return err
@@ -131,7 +301,7 @@ func (w *Worktree) addIndexFromFile(name string, f *object.TreeEntry, idx *index
}
e := index.Entry{
- Hash: f.Hash,
+ Hash: h,
Name: name,
Mode: mode,
ModifiedAt: fi.ModTime(),
@@ -148,65 +318,34 @@ func (w *Worktree) addIndexFromFile(name string, f *object.TreeEntry, idx *index
return nil
}
-func (w *Worktree) Status() (Status, error) {
- idx, err := w.r.Storer.Index()
- if err != nil {
- return nil, err
- }
-
- files, err := readDirAll(w.fs)
- if err != nil {
- return nil, err
- }
-
- s := make(Status, 0)
- for _, e := range idx.Entries {
- fi, ok := files[e.Name]
- delete(files, e.Name)
-
- if !ok {
- s.File(e.Name).Worktree = Deleted
+func (w *Worktree) rmIndexFromFile(name string, idx *index.Index) error {
+ for i, e := range idx.Entries {
+ if e.Name != name {
continue
}
- status, err := w.compareFileWithEntry(fi, &e)
- if err != nil {
- return nil, err
- }
-
- s.File(e.Name).Worktree = status
- }
-
- for f := range files {
- s.File(f).Worktree = Untracked
+ idx.Entries = append(idx.Entries[:i], idx.Entries[i+1:]...)
+ return nil
}
- return s, nil
+ return nil
}
-func (w *Worktree) compareFileWithEntry(fi billy.FileInfo, e *index.Entry) (StatusCode, error) {
- if fi.Size() != int64(e.Size) {
- return Modified, nil
- }
-
- mode, err := filemode.NewFromOSFileMode(fi.Mode())
+func (w *Worktree) getTreeFromCommitHash(commit plumbing.Hash) (*object.Tree, error) {
+ c, err := w.r.CommitObject(commit)
if err != nil {
- return Modified, err
- }
-
- if mode != e.Mode {
- return Modified, nil
+ return nil, err
}
- h, err := calcSHA1(w.fs, e.Name)
- if h != e.Hash || err != nil {
- return Modified, err
-
- }
+ return c.Tree()
+}
- return Unmodified, nil
+func (w *Worktree) initializeIndex() error {
+ return w.r.Storer.SetIndex(&index.Index{Version: 2})
}
+var fillSystemInfo func(e *index.Entry, sys interface{})
+
const gitmodulesFile = ".gitmodules"
// Submodule returns the submodule with the given name
@@ -290,149 +429,3 @@ func (w *Worktree) readIndexEntry(path string) (index.Entry, error) {
return e, fmt.Errorf("unable to find %q entry in the index", path)
}
-
-// Status current status of a Worktree
-type Status map[string]*FileStatus
-
-func (s Status) File(filename string) *FileStatus {
- if _, ok := (s)[filename]; !ok {
- s[filename] = &FileStatus{}
- }
-
- return s[filename]
-
-}
-
-func (s Status) IsClean() bool {
- for _, status := range s {
- if status.Worktree != Unmodified || status.Staging != Unmodified {
- return false
- }
- }
-
- return true
-}
-
-func (s Status) String() string {
- var names []string
- for name := range s {
- names = append(names, name)
- }
-
- var output string
- for _, name := range names {
- status := s[name]
- if status.Staging == 0 && status.Worktree == 0 {
- continue
- }
-
- if status.Staging == Renamed {
- name = fmt.Sprintf("%s -> %s", name, status.Extra)
- }
-
- output += fmt.Sprintf("%s%s %s\n", status.Staging, status.Worktree, name)
- }
-
- return output
-}
-
-// FileStatus status of a file in the Worktree
-type FileStatus struct {
- Staging StatusCode
- Worktree StatusCode
- Extra string
-}
-
-// StatusCode status code of a file in the Worktree
-type StatusCode int8
-
-const (
- Unmodified StatusCode = iota
- Untracked
- Modified
- Added
- Deleted
- Renamed
- Copied
- UpdatedButUnmerged
-)
-
-func (c StatusCode) String() string {
- switch c {
- case Unmodified:
- return " "
- case Modified:
- return "M"
- case Added:
- return "A"
- case Deleted:
- return "D"
- case Renamed:
- return "R"
- case Copied:
- return "C"
- case UpdatedButUnmerged:
- return "U"
- case Untracked:
- return "?"
- default:
- return "-"
- }
-}
-
-func calcSHA1(fs billy.Filesystem, filename string) (plumbing.Hash, error) {
- file, err := fs.Open(filename)
- if err != nil {
- return plumbing.ZeroHash, err
- }
-
- stat, err := fs.Stat(filename)
- if err != nil {
- return plumbing.ZeroHash, err
- }
-
- h := plumbing.NewHasher(plumbing.BlobObject, stat.Size())
- if _, err := io.Copy(h, file); err != nil {
- return plumbing.ZeroHash, err
- }
-
- return h.Sum(), nil
-}
-
-func readDirAll(filesystem billy.Filesystem) (map[string]billy.FileInfo, error) {
- all := make(map[string]billy.FileInfo, 0)
- return all, doReadDirAll(filesystem, "", all)
-}
-
-func doReadDirAll(fs billy.Filesystem, path string, files map[string]billy.FileInfo) error {
- if path == defaultDotGitPath {
- return nil
- }
-
- l, err := fs.ReadDir(path)
- if err != nil {
- if os.IsNotExist(err) {
- return nil
- }
-
- return err
- }
-
- for _, info := range l {
- file := fs.Join(path, info.Name())
- if file == defaultDotGitPath {
- continue
- }
-
- if !info.IsDir() {
- files[file] = info
- continue
- }
-
- if err := doReadDirAll(fs, file, files); err != nil {
- return err
- }
- }
-
- return nil
-}
diff --git a/worktree_status.go b/worktree_status.go
new file mode 100644
index 0000000..ae7518e
--- /dev/null
+++ b/worktree_status.go
@@ -0,0 +1,131 @@
+package git
+
+import (
+ "bytes"
+
+ "gopkg.in/src-d/go-git.v4/plumbing"
+ "gopkg.in/src-d/go-git.v4/plumbing/object"
+ "gopkg.in/src-d/go-git.v4/utils/merkletrie"
+ "gopkg.in/src-d/go-git.v4/utils/merkletrie/filesystem"
+ "gopkg.in/src-d/go-git.v4/utils/merkletrie/index"
+ "gopkg.in/src-d/go-git.v4/utils/merkletrie/noder"
+)
+
+// Status returns the working tree status
+func (w *Worktree) Status() (Status, error) {
+ ref, err := w.r.Head()
+ if err == plumbing.ErrReferenceNotFound {
+ return nil, nil
+ }
+
+ if err != nil {
+ return nil, err
+ }
+
+ return w.status(ref.Hash())
+}
+
+func (w *Worktree) status(commit plumbing.Hash) (Status, error) {
+ s := make(Status, 0)
+
+ right, err := w.diffStagingWithWorktree()
+ if err != nil {
+ return nil, err
+ }
+
+ for _, ch := range right {
+ a, err := ch.Action()
+ if err != nil {
+ return nil, err
+ }
+
+ switch a {
+ case merkletrie.Delete:
+ s.File(ch.From.String()).Worktree = Deleted
+ case merkletrie.Insert:
+ s.File(ch.To.String()).Worktree = Untracked
+ s.File(ch.To.String()).Staging = Untracked
+ case merkletrie.Modify:
+ s.File(ch.To.String()).Worktree = Modified
+ }
+ }
+
+ left, err := w.diffCommitWithStaging(commit, false)
+ if err != nil {
+ return nil, err
+ }
+
+ for _, ch := range left {
+ a, err := ch.Action()
+ if err != nil {
+ return nil, err
+ }
+
+ switch a {
+ case merkletrie.Delete:
+ s.File(ch.From.String()).Staging = Deleted
+ case merkletrie.Insert:
+ s.File(ch.To.String()).Staging = Added
+ case merkletrie.Modify:
+ s.File(ch.To.String()).Staging = Modified
+ }
+ }
+
+ return s, nil
+}
+
+func (w *Worktree) diffStagingWithWorktree() (merkletrie.Changes, error) {
+ idx, err := w.r.Storer.Index()
+ if err != nil {
+ return nil, err
+ }
+
+ from := index.NewRootNode(idx)
+ to := filesystem.NewRootNode(w.fs)
+ return merkletrie.DiffTree(from, to, diffTreeIsEquals)
+}
+
+func (w *Worktree) diffCommitWithStaging(commit plumbing.Hash, reverse bool) (merkletrie.Changes, error) {
+ idx, err := w.r.Storer.Index()
+ if err != nil {
+ return nil, err
+ }
+
+ c, err := w.r.CommitObject(commit)
+ if err != nil {
+ return nil, err
+ }
+
+ t, err := c.Tree()
+ if err != nil {
+ return nil, err
+ }
+
+ to := index.NewRootNode(idx)
+ from := object.NewTreeRootNode(t)
+
+ if reverse {
+ return merkletrie.DiffTree(to, from, diffTreeIsEquals)
+ }
+
+ return merkletrie.DiffTree(from, to, diffTreeIsEquals)
+}
+
+var emptyNoderHash = make([]byte, 24)
+
+// diffTreeIsEquals is a implementation of noder.Equals, used to compare
+// noder.Noder, it compare the content and the length of the hashes.
+//
+// Since some of the noder.Noder implementations doesn't compute a hash for
+// some directories, if any of the hashes is a 24-byte slice of zero values
+// the comparison is not done and the hashes are take as different.
+func diffTreeIsEquals(a, b noder.Hasher) bool {
+ hashA := a.Hash()
+ hashB := b.Hash()
+
+ if bytes.Equal(hashA, emptyNoderHash) || bytes.Equal(hashB, emptyNoderHash) {
+ return false
+ }
+
+ return bytes.Equal(hashA, hashB)
+}
diff --git a/worktree_test.go b/worktree_test.go
index 5330c67..59197a6 100644
--- a/worktree_test.go
+++ b/worktree_test.go
@@ -3,9 +3,12 @@ package git
import (
"io/ioutil"
"os"
+ "path/filepath"
+ "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/index"
+ "gopkg.in/src-d/go-git.v4/plumbing/object"
"github.com/src-d/go-git-fixtures"
. "gopkg.in/check.v1"
@@ -26,16 +29,13 @@ func (s *WorktreeSuite) SetUpTest(c *C) {
}
func (s *WorktreeSuite) TestCheckout(c *C) {
- h, err := s.Repository.Head()
- c.Assert(err, IsNil)
-
fs := memfs.New()
w := &Worktree{
r: s.Repository,
fs: fs,
}
- err = w.Checkout(h.Hash())
+ err := w.Checkout(&CheckoutOptions{})
c.Assert(err, IsNil)
entries, err := fs.ReadDir("/")
@@ -54,17 +54,14 @@ func (s *WorktreeSuite) TestCheckout(c *C) {
c.Assert(idx.Entries, HasLen, 9)
}
-func (s *WorktreeSuite) TestCheckoutIndexmemfs(c *C) {
- h, err := s.Repository.Head()
- c.Assert(err, IsNil)
-
+func (s *WorktreeSuite) TestCheckoutIndexMem(c *C) {
fs := memfs.New()
w := &Worktree{
r: s.Repository,
fs: fs,
}
- err = w.Checkout(h.Hash())
+ err := w.Checkout(&CheckoutOptions{})
c.Assert(err, IsNil)
idx, err := s.Repository.Storer.Index()
@@ -85,19 +82,16 @@ func (s *WorktreeSuite) TestCheckoutIndexmemfs(c *C) {
}
func (s *WorktreeSuite) TestCheckoutIndexOS(c *C) {
- h, err := s.Repository.Head()
- c.Assert(err, IsNil)
-
dir, err := ioutil.TempDir("", "checkout")
defer os.RemoveAll(dir)
- fs := osfs.New(dir)
+ fs := osfs.New(filepath.Join(dir, "worktree"))
w := &Worktree{
r: s.Repository,
fs: fs,
}
- err = w.Checkout(h.Hash())
+ err = w.Checkout(&CheckoutOptions{})
c.Assert(err, IsNil)
idx, err := s.Repository.Storer.Index()
@@ -116,41 +110,247 @@ func (s *WorktreeSuite) TestCheckoutIndexOS(c *C) {
c.Assert(idx.Entries[0].GID, Not(Equals), uint32(0))
}
+func (s *WorktreeSuite) TestCheckoutChange(c *C) {
+ fs := memfs.New()
+ w := &Worktree{
+ r: s.Repository,
+ fs: fs,
+ }
+
+ err := w.Checkout(&CheckoutOptions{})
+ c.Assert(err, IsNil)
+ head, err := w.r.Head()
+ c.Assert(err, IsNil)
+ c.Assert(head.Name().String(), Equals, "refs/heads/master")
+
+ status, err := w.Status()
+ c.Assert(err, IsNil)
+ c.Assert(status.IsClean(), Equals, true)
+
+ _, err = fs.Stat("README")
+ c.Assert(err, Equals, os.ErrNotExist)
+ _, err = fs.Stat("vendor")
+ c.Assert(err, Equals, nil)
+
+ err = w.Checkout(&CheckoutOptions{
+ Branch: "refs/heads/branch",
+ })
+ c.Assert(err, IsNil)
+
+ status, err = w.Status()
+ c.Assert(err, IsNil)
+ c.Assert(status.IsClean(), Equals, true)
+
+ _, err = fs.Stat("README")
+ c.Assert(err, Equals, nil)
+ _, err = fs.Stat("vendor")
+ c.Assert(err, Equals, os.ErrNotExist)
+
+ head, err = w.r.Head()
+ c.Assert(err, IsNil)
+ c.Assert(head.Name().String(), Equals, "refs/heads/branch")
+}
+
+func (s *WorktreeSuite) TestCheckoutTag(c *C) {
+ f := fixtures.ByTag("tags").One()
+
+ fs := memfs.New()
+ w := &Worktree{
+ r: s.NewRepository(f),
+ fs: fs,
+ }
+
+ // we delete the index, since the fixture comes with a real index
+ err := w.r.Storer.SetIndex(&index.Index{Version: 2})
+ c.Assert(err, IsNil)
+
+ err = w.Checkout(&CheckoutOptions{})
+ c.Assert(err, IsNil)
+ head, err := w.r.Head()
+ c.Assert(err, IsNil)
+ c.Assert(head.Name().String(), Equals, "refs/heads/master")
+
+ status, err := w.Status()
+ c.Assert(err, IsNil)
+ c.Assert(status.IsClean(), Equals, true)
+
+ err = w.Checkout(&CheckoutOptions{Branch: "refs/tags/lightweight-tag"})
+ c.Assert(err, IsNil)
+ head, err = w.r.Head()
+ c.Assert(err, IsNil)
+ c.Assert(head.Name().String(), Equals, "HEAD")
+ c.Assert(head.Hash().String(), Equals, "f7b877701fbf855b44c0a9e86f3fdce2c298b07f")
+
+ err = w.Checkout(&CheckoutOptions{Branch: "refs/tags/commit-tag"})
+ c.Assert(err, IsNil)
+ head, err = w.r.Head()
+ c.Assert(err, IsNil)
+ c.Assert(head.Name().String(), Equals, "HEAD")
+ c.Assert(head.Hash().String(), Equals, "f7b877701fbf855b44c0a9e86f3fdce2c298b07f")
+
+ err = w.Checkout(&CheckoutOptions{Branch: "refs/tags/tree-tag"})
+ c.Assert(err, NotNil)
+ head, err = w.r.Head()
+ c.Assert(err, IsNil)
+ c.Assert(head.Name().String(), Equals, "HEAD")
+}
+
+func (s *WorktreeSuite) TestCheckoutBisect(c *C) {
+ s.testCheckoutBisect(c, "https://github.com/src-d/go-git.git")
+}
+
+func (s *WorktreeSuite) TestCheckoutBisectSubmodules(c *C) {
+ c.Skip("not-submodule-support")
+ s.testCheckoutBisect(c, "https://github.com/git-fixtures/submodule.git")
+}
+
+// TestCheckoutBisect simulates a git bisect going through the git history and
+// checking every commit over the previous commit
+func (s *WorktreeSuite) testCheckoutBisect(c *C, url string) {
+ f := fixtures.ByURL(url).One()
+
+ w := &Worktree{
+ r: s.NewRepository(f),
+ fs: memfs.New(),
+ }
+
+ // we delete the index, since the fixture comes with a real index
+ err := w.r.Storer.SetIndex(&index.Index{Version: 2})
+ c.Assert(err, IsNil)
+
+ iter, err := w.r.Log(&LogOptions{})
+ c.Assert(err, IsNil)
+
+ iter.ForEach(func(commit *object.Commit) error {
+ err := w.Checkout(&CheckoutOptions{Hash: commit.Hash})
+ c.Assert(err, IsNil)
+
+ status, err := w.Status()
+ c.Assert(err, IsNil)
+ c.Assert(status.IsClean(), Equals, true)
+
+ return nil
+ })
+}
+
func (s *WorktreeSuite) TestStatus(c *C) {
- h, err := s.Repository.Head()
+ fs := memfs.New()
+ w := &Worktree{
+ r: s.Repository,
+ fs: fs,
+ }
+
+ status, err := w.Status()
c.Assert(err, IsNil)
+ c.Assert(status.IsClean(), Equals, false)
+ c.Assert(status, HasLen, 9)
+}
+func (s *WorktreeSuite) TestReset(c *C) {
fs := memfs.New()
w := &Worktree{
r: s.Repository,
fs: fs,
}
- err = w.Checkout(h.Hash())
+ commit := plumbing.NewHash("35e85108805c84807bc66a02d91535e1e24b38b9")
+
+ err := w.Checkout(&CheckoutOptions{})
c.Assert(err, IsNil)
- status, err := w.Status()
+ branch, err := w.r.Reference(plumbing.Master, false)
c.Assert(err, IsNil)
+ c.Assert(branch.Hash(), Not(Equals), commit)
- c.Assert(status.IsClean(), Equals, true)
+ err = w.Reset(&ResetOptions{Commit: commit})
+ c.Assert(err, IsNil)
+
+ branch, err = w.r.Reference(plumbing.Master, false)
+ c.Assert(err, IsNil)
+ c.Assert(branch.Hash(), Equals, commit)
}
-func (s *WorktreeSuite) TestStatusModified(c *C) {
- c.Assert(s.Repository.Storer.SetIndex(&index.Index{Version: 2}), IsNil)
+func (s *WorktreeSuite) TestResetMerge(c *C) {
+ fs := memfs.New()
+ w := &Worktree{
+ r: s.Repository,
+ fs: fs,
+ }
+
+ commit := plumbing.NewHash("35e85108805c84807bc66a02d91535e1e24b38b9")
+
+ err := w.Checkout(&CheckoutOptions{})
+ c.Assert(err, IsNil)
+
+ f, err := fs.Create(".gitignore")
+ c.Assert(err, IsNil)
+ _, err = f.Write([]byte("foo"))
+ c.Assert(err, IsNil)
+ err = f.Close()
+ c.Assert(err, IsNil)
+
+ err = w.Reset(&ResetOptions{Mode: MergeReset, Commit: commit})
+ c.Assert(err, Equals, ErrUnstaggedChanges)
- h, err := s.Repository.Head()
+ branch, err := w.r.Reference(plumbing.Master, false)
c.Assert(err, IsNil)
+ c.Assert(branch.Hash(), Not(Equals), commit)
+}
+func (s *WorktreeSuite) TestResetHard(c *C) {
+ fs := memfs.New()
+ w := &Worktree{
+ r: s.Repository,
+ fs: fs,
+ }
+
+ commit := plumbing.NewHash("35e85108805c84807bc66a02d91535e1e24b38b9")
+
+ err := w.Checkout(&CheckoutOptions{})
+ c.Assert(err, IsNil)
+
+ f, err := fs.Create(".gitignore")
+ c.Assert(err, IsNil)
+ _, err = f.Write([]byte("foo"))
+ c.Assert(err, IsNil)
+ err = f.Close()
+ c.Assert(err, IsNil)
+
+ err = w.Reset(&ResetOptions{Mode: HardReset, Commit: commit})
+ c.Assert(err, IsNil)
+
+ branch, err := w.r.Reference(plumbing.Master, false)
+ c.Assert(err, IsNil)
+ c.Assert(branch.Hash(), Equals, commit)
+}
+
+func (s *WorktreeSuite) TestStatusAfterCheckout(c *C) {
+ fs := memfs.New()
+ w := &Worktree{
+ r: s.Repository,
+ fs: fs,
+ }
+
+ err := w.Checkout(&CheckoutOptions{Force: true})
+ c.Assert(err, IsNil)
+
+ status, err := w.Status()
+ c.Assert(err, IsNil)
+ c.Assert(status.IsClean(), Equals, true)
+
+}
+
+func (s *WorktreeSuite) TestStatusModified(c *C) {
dir, err := ioutil.TempDir("", "status")
defer os.RemoveAll(dir)
- fs := osfs.New(dir)
+ fs := osfs.New(filepath.Join(dir, "worktree"))
w := &Worktree{
r: s.Repository,
fs: fs,
}
- err = w.Checkout(h.Hash())
+ err = w.Checkout(&CheckoutOptions{})
c.Assert(err, IsNil)
f, err := fs.Create(".gitignore")
@@ -163,6 +363,49 @@ func (s *WorktreeSuite) TestStatusModified(c *C) {
status, err := w.Status()
c.Assert(err, IsNil)
c.Assert(status.IsClean(), Equals, false)
+ c.Assert(status.File(".gitignore").Worktree, Equals, Modified)
+}
+
+func (s *WorktreeSuite) TestStatusUntracked(c *C) {
+ fs := memfs.New()
+ w := &Worktree{
+ r: s.Repository,
+ fs: fs,
+ }
+
+ err := w.Checkout(&CheckoutOptions{Force: true})
+ c.Assert(err, IsNil)
+
+ f, err := w.fs.Create("foo")
+ c.Assert(err, IsNil)
+ c.Assert(f.Close(), IsNil)
+
+ status, err := w.Status()
+ c.Assert(err, IsNil)
+ c.Assert(status.File("foo").Staging, Equals, Untracked)
+ c.Assert(status.File("foo").Worktree, Equals, Untracked)
+}
+
+func (s *WorktreeSuite) TestStatusDeleted(c *C) {
+ dir, err := ioutil.TempDir("", "status")
+ defer os.RemoveAll(dir)
+
+ fs := osfs.New(filepath.Join(dir, "worktree"))
+ w := &Worktree{
+ r: s.Repository,
+ fs: fs,
+ }
+
+ err = w.Checkout(&CheckoutOptions{})
+ c.Assert(err, IsNil)
+
+ err = fs.Remove(".gitignore")
+ c.Assert(err, IsNil)
+
+ status, err := w.Status()
+ c.Assert(err, IsNil)
+ c.Assert(status.IsClean(), Equals, false)
+ c.Assert(status.File(".gitignore").Worktree, Equals, Deleted)
}
func (s *WorktreeSuite) TestSubmodule(c *C) {