diff options
author | Máximo Cuadros <mcuadros@gmail.com> | 2017-04-12 15:18:41 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2017-04-12 15:18:41 +0200 |
commit | 932ced9f55f556de02610425cfa161c35c6a758b (patch) | |
tree | 9b5dd9ad1665fad8424dfbdc5bd93b531f714b09 | |
parent | 9b45f468c61a0756dd19d09b64c2b1a88cc99ec5 (diff) | |
parent | 5bcf802213e801c4d52102612f007defa5d0397f (diff) | |
download | go-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.go | 26 | ||||
-rw-r--r-- | _examples/common_test.go | 2 | ||||
-rw-r--r-- | options.go | 66 | ||||
-rw-r--r-- | plumbing/format/index/index.go | 26 | ||||
-rw-r--r-- | plumbing/object/difftree.go | 4 | ||||
-rw-r--r-- | plumbing/object/tree.go | 8 | ||||
-rw-r--r-- | plumbing/object/tree_test.go | 6 | ||||
-rw-r--r-- | plumbing/object/treenoder.go | 26 | ||||
-rw-r--r-- | repository.go | 20 | ||||
-rw-r--r-- | status.go | 70 | ||||
-rw-r--r-- | submodule.go | 6 | ||||
-rw-r--r-- | submodule_test.go | 5 | ||||
-rw-r--r-- | utils/merkletrie/filesystem/node.go | 144 | ||||
-rw-r--r-- | utils/merkletrie/filesystem/node_test.go | 114 | ||||
-rw-r--r-- | utils/merkletrie/index/node.go | 86 | ||||
-rw-r--r-- | utils/merkletrie/index/node_test.go | 108 | ||||
-rw-r--r-- | worktree.go | 443 | ||||
-rw-r--r-- | worktree_status.go | 131 | ||||
-rw-r--r-- | worktree_test.go | 289 |
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"}, @@ -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) { |