diff options
-rw-r--r-- | options.go | 52 | ||||
-rw-r--r-- | plumbing/format/index/index.go | 23 | ||||
-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 | 7 | ||||
-rw-r--r-- | repository.go | 20 | ||||
-rw-r--r-- | status.go | 92 | ||||
-rw-r--r-- | submodule.go | 6 | ||||
-rw-r--r-- | submodule_test.go | 5 | ||||
-rw-r--r-- | utils/merkletrie/filesystem/node.go | 128 | ||||
-rw-r--r-- | utils/merkletrie/filesystem/node_test.go | 127 | ||||
-rw-r--r-- | utils/merkletrie/index/node.go | 113 | ||||
-rw-r--r-- | utils/merkletrie/index/node_test.go | 116 | ||||
-rw-r--r-- | worktree.go | 386 | ||||
-rw-r--r-- | worktree_status.go | 133 | ||||
-rw-r--r-- | worktree_test.go | 206 |
17 files changed, 1156 insertions, 276 deletions
@@ -178,6 +178,58 @@ type SubmoduleUpdateOptions struct { RecurseSubmodules SubmoduleRescursivity } +// CheckoutOptions describes how a checkout operation should be performed. +type CheckoutOptions struct { + // Branch to be checked out, if empty uses `master` + Branch plumbing.ReferenceName + Hash plumbing.Hash + // RemoteName is the name of the remote to be pushed to. + 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 +} + +type ResetMode int + +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 +) + +// 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 + 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..3675c4e 100644 --- a/plumbing/format/index/index.go +++ b/plumbing/format/index/index.go @@ -2,6 +2,7 @@ package index import ( "errors" + "fmt" "time" "gopkg.in/src-d/go-git.v4/plumbing" @@ -47,6 +48,16 @@ type Index struct { ResolveUndo *ResolveUndo } +// String is equivalent to `git ls-files --stage --debug` +func (i *Index) String() string { + var o string + for _, e := range i.Entries { + o += e.String() + } + + return o +} + // 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 +89,18 @@ type Entry struct { IntentToAdd bool } +func (e Entry) String() string { + var o string + o += fmt.Sprintf("%06o %s %d\t%s\n", e.Mode, e.Hash, e.Stage, e.Name) + o += fmt.Sprintf(" ctime: %d:%d\n", e.CreatedAt.Unix(), e.CreatedAt.Nanosecond()) + o += fmt.Sprintf(" mtime: %d:%d\n", e.ModifiedAt.Unix(), e.ModifiedAt.Nanosecond()) + o += fmt.Sprintf(" dev: %d\tino: %d\n", e.Dev, e.Inode) + o += fmt.Sprintf(" uid: %d\tgid: %d\n", e.UID, e.GID) + o += fmt.Sprintf(" size: %d\tflags: %x\n", e.Size, 0) + + return o +} + // 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..b768b96 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..8b56d1b 100644 --- a/plumbing/object/treenoder.go +++ b/plumbing/object/treenoder.go @@ -21,10 +21,11 @@ type treeNoder struct { name string // empty string for the root node mode filemode.FileMode hash plumbing.Hash - children []noder.Noder // memoized + children []noder.Noder // memorized } -func newTreeNoder(t *Tree) *treeNoder { +// NewTreeRootNode returns the root node of a Tree +func NewTreeRootNode(t *Tree) *treeNoder { if t == nil { return &treeNoder{} } @@ -74,7 +75,7 @@ func (t *treeNoder) Children() ([]noder.Noder, error) { return noder.NoChildren, nil } - // children are memoized for efficiency + // children are memorized for efficiency if t.children != nil { return t.children, nil } diff --git a/repository.go b/repository.go index d9a1d7e..2fdbafc 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..e789f4a --- /dev/null +++ b/status.go @@ -0,0 +1,92 @@ +package git + +import "fmt" + +// 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 "-" + } +} 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..847d71e --- /dev/null +++ b/utils/merkletrie/filesystem/node.go @@ -0,0 +1,128 @@ +package filesystem + +import ( + "bytes" + "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, +} + +func IsEquals(a, b noder.Hasher) bool { + pathA := a.(noder.Path) + pathB := b.(noder.Path) + if pathA[len(pathA)-1].IsDir() || pathB[len(pathB)-1].IsDir() { + return false + } + + return bytes.Equal(a.Hash(), b.Hash()) +} + +type Node struct { + parent string + name string + isDir bool + info billy.FileInfo + fs billy.Filesystem +} + +func NewRootNode(fs billy.Filesystem) (*Node, error) { + info, err := fs.Stat("/") + if err != nil && !os.IsNotExist(err) { + return nil, err + } + + return &Node{fs: fs, info: info, isDir: true, name: ""}, nil +} + +func (n *Node) String() string { + return filepath.Join(n.parent, n.name) +} + +func (n *Node) Hash() []byte { + if n.IsDir() { + return nil + } + + f, err := n.fs.Open(n.fullpath()) + if err != nil { + panic(err) + } + + h := plumbing.NewHasher(plumbing.BlobObject, n.info.Size()) + if _, err := io.Copy(h, f); err != nil { + panic(err) + } + + hash := h.Sum() + mode, err := filemode.NewFromOSFileMode(n.info.Mode()) + if err != nil { + panic(err) + } + + return append(hash[:], mode.Bytes()...) +} + +func (n *Node) Name() string { + return n.name +} + +func (n *Node) IsDir() bool { + return n.isDir +} + +func (n *Node) Children() ([]noder.Noder, error) { + files, err := n.readDir() + + if err != nil { + return nil, err + } + + path := n.fullpath() + var c []noder.Noder + for _, file := range files { + if _, ok := ignore[file.Name()]; ok { + continue + } + + c = append(c, &Node{ + fs: n.fs, + parent: path, + info: file, + name: file.Name(), + isDir: file.IsDir(), + }) + } + + return c, nil +} + +func (n *Node) NumChildren() (int, error) { + files, err := n.readDir() + return len(files), err +} + +func (n *Node) fullpath() string { + return filepath.Join(n.parent, n.name) +} + +func (n *Node) readDir() ([]billy.FileInfo, error) { + if !n.IsDir() { + return nil, nil + } + + l, err := n.fs.ReadDir(n.fullpath()) + if err != nil && os.IsNotExist(err) { + return l, nil + } + + return l, err +} diff --git a/utils/merkletrie/filesystem/node_test.go b/utils/merkletrie/filesystem/node_test.go new file mode 100644 index 0000000..291af6b --- /dev/null +++ b/utils/merkletrie/filesystem/node_test.go @@ -0,0 +1,127 @@ +package filesystem + +import ( + "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" +) + +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) + + nodeA, err := NewRootNode(fsA) + c.Assert(err, IsNil) + nodeB, err := NewRootNode(fsB) + c.Assert(err, IsNil) + + ch, err := merkletrie.DiffTree(nodeA, nodeB, 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) + + nodeA, err := NewRootNode(fsA) + c.Assert(err, IsNil) + nodeB, err := NewRootNode(fsB) + c.Assert(err, IsNil) + + ch, err := merkletrie.DiffTree(nodeA, nodeB, 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) + + nodeA, err := NewRootNode(fsA) + c.Assert(err, IsNil) + nodeB, err := NewRootNode(fsB) + c.Assert(err, IsNil) + + ch, err := merkletrie.DiffTree(nodeA, nodeB, 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) + + nodeA, err := NewRootNode(fsA) + c.Assert(err, IsNil) + nodeB, err := NewRootNode(fsB) + c.Assert(err, IsNil) + + ch, err := merkletrie.DiffTree(nodeA, nodeB, 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) + + nodeA, err := NewRootNode(fsA) + c.Assert(err, IsNil) + nodeB, err := NewRootNode(fsB) + c.Assert(err, IsNil) + + ch, err := merkletrie.DiffTree(nodeA, nodeB, 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 +} diff --git a/utils/merkletrie/index/node.go b/utils/merkletrie/index/node.go new file mode 100644 index 0000000..7972f7f --- /dev/null +++ b/utils/merkletrie/index/node.go @@ -0,0 +1,113 @@ +package index + +import ( + "bytes" + "path/filepath" + + "strings" + + "gopkg.in/src-d/go-git.v4/plumbing/format/index" + "gopkg.in/src-d/go-git.v4/utils/merkletrie/noder" +) + +func IsEquals(a, b noder.Hasher) bool { + pathA := a.(noder.Path) + pathB := b.(noder.Path) + if pathA[len(pathA)-1].IsDir() || pathB[len(pathB)-1].IsDir() { + return false + } + + return bytes.Equal(a.Hash(), b.Hash()) +} + +type Node struct { + index *index.Index + parent string + name string + entry index.Entry + isDir bool +} + +func NewRootNode(idx *index.Index) (*Node, error) { + return &Node{index: idx, isDir: true}, nil +} + +func (n *Node) String() string { + return n.fullpath() +} + +func (n *Node) Hash() []byte { + if n.IsDir() { + return nil + } + + return append(n.entry.Hash[:], n.entry.Mode.Bytes()...) +} + +func (n *Node) Name() string { + return n.name +} + +func (n *Node) IsDir() bool { + return n.isDir +} + +func (n *Node) Children() ([]noder.Noder, error) { + path := n.fullpath() + dirs := make(map[string]bool) + + var c []noder.Noder + for _, e := range n.index.Entries { + if e.Name == path { + continue + } + + prefix := path + if prefix != "" { + prefix += "/" + } + + if !strings.HasPrefix(e.Name, prefix) { + continue + } + + name := e.Name[len(path):] + if len(name) != 0 && name[0] == '/' { + name = name[1:] + } + + parts := strings.Split(name, "/") + if len(parts) > 1 { + dirs[parts[0]] = true + continue + } + + c = append(c, &Node{ + index: n.index, + parent: path, + name: name, + entry: e, + }) + } + + for dir := range dirs { + c = append(c, &Node{ + index: n.index, + parent: path, + name: dir, + isDir: true, + }) + + } + + return c, nil +} + +func (n *Node) NumChildren() (int, error) { + files, err := n.Children() + return len(files), err +} + +func (n *Node) fullpath() string { + return filepath.Join(n.parent, n.name) +} diff --git a/utils/merkletrie/index/node_test.go b/utils/merkletrie/index/node_test.go new file mode 100644 index 0000000..0ee0884 --- /dev/null +++ b/utils/merkletrie/index/node_test.go @@ -0,0 +1,116 @@ +package index + +import ( + "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" +) + +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")}, + }, + } + + nodeA, err := NewRootNode(indexA) + c.Assert(err, IsNil) + nodeB, err := NewRootNode(indexB) + c.Assert(err, IsNil) + + ch, err := merkletrie.DiffTree(nodeA, nodeB, 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")}, + }, + } + + nodeA, err := NewRootNode(indexA) + c.Assert(err, IsNil) + nodeB, err := NewRootNode(indexB) + c.Assert(err, IsNil) + + ch, err := merkletrie.DiffTree(nodeA, nodeB, 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")}, + }, + } + + nodeA, err := NewRootNode(indexA) + c.Assert(err, IsNil) + nodeB, err := NewRootNode(indexB) + c.Assert(err, IsNil) + + ch, err := merkletrie.DiffTree(nodeA, nodeB, 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")}, + }, + } + + nodeA, err := NewRootNode(indexA) + c.Assert(err, IsNil) + nodeB, err := NewRootNode(indexB) + c.Assert(err, IsNil) + + ch, err := merkletrie.DiffTree(nodeA, nodeB, IsEquals) + c.Assert(err, IsNil) + c.Assert(ch, HasLen, 1) +} diff --git a/worktree.go b/worktree.go index 40ebe58..f9c4ba5 100644 --- a/worktree.go +++ b/worktree.go @@ -12,6 +12,7 @@ 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" ) @@ -24,77 +25,183 @@ type Worktree struct { fs billy.Filesystem } -func (w *Worktree) Checkout(commit plumbing.Hash) error { - s, err := w.Status() +// Checkout switch branches or restore working tree files. +func (w *Worktree) Checkout(opts *CheckoutOptions) error { + if err := opts.Validate(); err != nil { + return err + } + + c, err := w.getCommitFromCheckoutOptions(opts) if err != nil { return err } - if !s.IsClean() { - return ErrWorktreeNotClean + ro := &ResetOptions{Commit: c} + if opts.Force { + ro.Mode = HardReset } - c, err := w.r.CommitObject(commit) - if err != nil { + if err := w.Reset(ro); err != nil { return err } - t, err := c.Tree() + if !opts.Hash.IsZero() { + return w.setCommit(opts.Hash) + } + + return w.setBranch(opts.Branch, c) +} + +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 err + return plumbing.ZeroHash, err } - idx := &index.Index{Version: 2} - walker := object.NewTreeWalker(t, true) + if !b.IsTag() { + return b.Hash(), nil + } - for { - name, entry, err := walker.Next() - if err == io.EOF { - break - } + o, err := w.r.Object(plumbing.AnyObject, b.Hash()) + if err != nil { + return plumbing.ZeroHash, err + } - if err != nil { - return 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) } - if err := w.checkoutEntry(name, &entry, idx); err != nil { - return err - } + return o.Target, nil + case *object.Commit: + return o.Hash, nil } - return w.r.Storer.SetIndex(idx) + return plumbing.ZeroHash, fmt.Errorf("unsupported tag target %q", o.Type()) } -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) setCommit(commit plumbing.Hash) error { + head := plumbing.NewHashReference(plumbing.HEAD, commit) + return w.r.Storer.SetReference(head) +} + +func (w *Worktree) setBranch(branch plumbing.ReferenceName, commit plumbing.Hash) error { + target, err := w.r.Storer.Reference(branch) + if err != nil { + return err } - if e.Mode == filemode.Dir { - return nil + var head *plumbing.Reference + if target.IsBranch() { + head = plumbing.NewSymbolicReference(plumbing.HEAD, target.Name()) + } else { + head = plumbing.NewHashReference(plumbing.HEAD, commit) } - return w.checkoutFile(name, e, idx) + return w.r.Storer.SetReference(head) } -func (w *Worktree) checkoutFile(name string, e *object.TreeEntry, idx *index.Index) error { - blob, err := object.GetBlob(w.r.Storer, e.Hash) +// Reset the worktree to a specified state. +func (w *Worktree) Reset(opts *ResetOptions) error { + if err := opts.Validate(w.r); err != nil { + return err + } + + changes, err := w.diffCommitWithStaging(opts.Commit, true) + if err != nil { + return err + } + + idx, err := w.r.Storer.Index() if err != nil { return err } - from, err := blob.Reader() + 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) +} + +func (w *Worktree) checkoutChange(ch merkletrie.Change, t *object.Tree, idx *index.Index) error { + a, err := ch.Action() + if err != nil { + return err + } + + 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 +211,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 +224,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 +236,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 +253,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 +364,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..d472fde --- /dev/null +++ b/worktree_status.go @@ -0,0 +1,133 @@ +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, err := index.NewRootNode(idx) + if err != nil { + return nil, err + } + + to, err := filesystem.NewRootNode(w.fs) + if err != nil { + return nil, err + } + + return merkletrie.DiffTree(from, to, IsEquals) +} + +func (w *Worktree) diffCommitWithStaging(commit plumbing.Hash, reverse bool) (merkletrie.Changes, error) { + idx, err := w.r.Storer.Index() + if err != nil { + return nil, err + } + + to, err := index.NewRootNode(idx) + 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 + } + + from := object.NewTreeRootNode(t) + if reverse { + return merkletrie.DiffTree(to, from, IsEquals) + } + + return merkletrie.DiffTree(from, to, IsEquals) +} + +func IsEquals(a, b noder.Hasher) bool { + pathA := a.(noder.Path) + pathB := b.(noder.Path) + if pathA[len(pathA)-1].IsDir() || pathB[len(pathB)-1].IsDir() { + return false + } + + return bytes.Equal(a.Hash(), b.Hash()) +} diff --git a/worktree_test.go b/worktree_test.go index 5330c67..d32e648 100644 --- a/worktree_test.go +++ b/worktree_test.go @@ -3,6 +3,7 @@ package git import ( "io/ioutil" "os" + "path/filepath" "gopkg.in/src-d/go-git.v4/plumbing/filemode" "gopkg.in/src-d/go-git.v4/plumbing/format/index" @@ -26,16 +27,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 +52,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 +80,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 +108,164 @@ func (s *WorktreeSuite) TestCheckoutIndexOS(c *C) { c.Assert(idx.Entries[0].GID, Not(Equals), uint32(0)) } -func (s *WorktreeSuite) TestStatus(c *C) { - h, err := s.Repository.Head() - c.Assert(err, IsNil) - +func (s *WorktreeSuite) TestCheckoutChange(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) + 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) TestStatusModified(c *C) { - c.Assert(s.Repository.Storer.SetIndex(&index.Index{Version: 2}), IsNil) +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") +} + +// TestCheckoutBisect simulates a git bisect going through the git history and +// checking every commit over the previous commit +func (s *WorktreeSuite) TestCheckoutBisect(c *C) { + f := fixtures.ByURL("https://github.com/src-d/go-git.git").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) + + ref, err := w.r.Head() + c.Assert(err, IsNil) + + commit, err := w.r.CommitObject(ref.Hash()) + c.Assert(err, IsNil) + + history, err := commit.History() + c.Assert(err, IsNil) + + for i := len(history) - 1; i >= 0; i-- { + err := w.Checkout(&CheckoutOptions{Hash: history[i].Hash}) + c.Assert(err, IsNil) - h, err := s.Repository.Head() + status, err := w.Status() + c.Assert(err, IsNil) + c.Assert(status.IsClean(), Equals, true) + } +} + +func (s *WorktreeSuite) TestStatus(c *C) { + 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) 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 +278,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) { |