diff options
-rw-r--r-- | options.go | 52 | ||||
-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-- | worktree.go | 386 | ||||
-rw-r--r-- | worktree_status.go | 133 | ||||
-rw-r--r-- | worktree_test.go | 206 |
8 files changed, 632 insertions, 268 deletions
@@ -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) { |