aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--options.go52
-rw-r--r--plumbing/format/index/index.go23
-rw-r--r--plumbing/object/difftree.go4
-rw-r--r--plumbing/object/tree.go8
-rw-r--r--plumbing/object/tree_test.go6
-rw-r--r--plumbing/object/treenoder.go7
-rw-r--r--repository.go20
-rw-r--r--status.go92
-rw-r--r--submodule.go6
-rw-r--r--submodule_test.go5
-rw-r--r--utils/merkletrie/filesystem/node.go128
-rw-r--r--utils/merkletrie/filesystem/node_test.go127
-rw-r--r--utils/merkletrie/index/node.go113
-rw-r--r--utils/merkletrie/index/node_test.go116
-rw-r--r--worktree.go386
-rw-r--r--worktree_status.go133
-rw-r--r--worktree_test.go206
17 files changed, 1156 insertions, 276 deletions
diff --git a/options.go b/options.go
index d033654..f46bb6d 100644
--- a/options.go
+++ b/options.go
@@ -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) {