aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--options.go52
-rw-r--r--repository.go20
-rw-r--r--status.go92
-rw-r--r--submodule.go6
-rw-r--r--submodule_test.go5
-rw-r--r--worktree.go386
-rw-r--r--worktree_status.go133
-rw-r--r--worktree_test.go206
8 files changed, 632 insertions, 268 deletions
diff --git a/options.go b/options.go
index 1047d7e..61c0388 100644
--- a/options.go
+++ b/options.go
@@ -177,3 +177,55 @@ type SubmoduleUpdateOptions struct {
// submodules (and so on). Until the SubmoduleRescursivity is reached.
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
+}
diff --git a/repository.go b/repository.go
index 9e325e4..71a53e7 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/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) {