diff options
author | Máximo Cuadros <mcuadros@gmail.com> | 2017-05-04 01:45:28 +0200 |
---|---|---|
committer | Máximo Cuadros <mcuadros@gmail.com> | 2017-05-04 01:45:28 +0200 |
commit | 40fa5882a2c73f8c075403b7ec85870f04deda07 (patch) | |
tree | 97a7708e159721f92d643ceb802ee18143d78191 | |
parent | ff18ce3751ad80cfd0297845872ba1d796c36ca5 (diff) | |
download | go-git-40fa5882a2c73f8c075403b7ec85870f04deda07.tar.gz |
worktree: Commit method implementation
-rw-r--r-- | options.go | 47 | ||||
-rw-r--r-- | status.go | 2 | ||||
-rw-r--r-- | worktree_commit.go | 243 | ||||
-rw-r--r-- | worktree_commit_test.go | 126 | ||||
-rw-r--r-- | worktree_status.go | 27 |
5 files changed, 437 insertions, 8 deletions
@@ -5,6 +5,7 @@ import ( "gopkg.in/src-d/go-git.v4/config" "gopkg.in/src-d/go-git.v4/plumbing" + "gopkg.in/src-d/go-git.v4/plumbing/object" "gopkg.in/src-d/go-git.v4/plumbing/protocol/packp/sideband" "gopkg.in/src-d/go-git.v4/plumbing/transport" ) @@ -42,7 +43,7 @@ type CloneOptions struct { // Limit fetching to the specified number of commits. Depth int // RecurseSubmodules after the clone is created, initialize all submodules - // within, using their default settings. This option is ignored if the + // within, using their defaut settings. This option is ignored if the // cloned repository does not have a worktree. RecurseSubmodules SubmoduleRescursivity // Progress is where the human readable information sent by the server is @@ -251,3 +252,47 @@ type LogOptions struct { // the default From. From plumbing.Hash } + +var ( + ErrMissingAuthor = errors.New("author field is required") + ErrMissingCommitter = errors.New("committer field is required") +) + +// CommitOptions describes how a commit operation should be performed. +type CommitOptions struct { + // All automatically stage files that have been modified and deleted, but + // new files you have not told Git about are not affected. + All bool + // Author is the author's signature of the commit. + Author *object.Signature + // Committer is the committer's signature of the commit. If Committer is + // equal to nil the Author signature is used. + Committer *object.Signature + // Parents parents commits for the new commit, by default is the hash of + // HEAD reference. + Parents []plumbing.Hash +} + +// Validate validates the fields and sets the default values. +func (o *CommitOptions) Validate(r *Repository) error { + if o.Author == nil { + return ErrMissingAuthor + } + + if o.Committer == nil { + o.Committer = o.Author + } + + if len(o.Parents) == 0 { + head, err := r.Head() + if err != nil && err != plumbing.ErrReferenceNotFound { + return err + } + + if head != nil { + o.Parents = []plumbing.Hash{head.Hash()} + } + } + + return nil +} @@ -11,7 +11,7 @@ type Status map[string]*FileStatus // exists a new FileStatus is added to the map using the path as key. func (s Status) File(path string) *FileStatus { if _, ok := (s)[path]; !ok { - s[path] = &FileStatus{Worktree: Unmodified, Staging: Unmodified} + s[path] = &FileStatus{Worktree: Untracked, Staging: Untracked} } return s[path] diff --git a/worktree_commit.go b/worktree_commit.go new file mode 100644 index 0000000..9cefdcf --- /dev/null +++ b/worktree_commit.go @@ -0,0 +1,243 @@ +package git + +import ( + "io" + "path/filepath" + "strings" + + "gopkg.in/src-d/go-git.v4/plumbing" + "gopkg.in/src-d/go-git.v4/plumbing/filemode" + "gopkg.in/src-d/go-git.v4/plumbing/format/index" + "gopkg.in/src-d/go-git.v4/plumbing/object" + "gopkg.in/src-d/go-git.v4/storage" + "gopkg.in/src-d/go-git.v4/utils/ioutil" + + "gopkg.in/src-d/go-billy.v2" +) + +// Commit stores the current contents of the index in a new commit along with +// a log message from the user describing the changes. +func (w *Worktree) Commit(msg string, opts *CommitOptions) (plumbing.Hash, error) { + if err := opts.Validate(w.r); err != nil { + return plumbing.ZeroHash, err + } + + if opts.All == true { + if err := w.autoAddModifiedAndDeleted(); err != nil { + return plumbing.ZeroHash, err + } + } + + idx, err := w.r.Storer.Index() + if err != nil { + return plumbing.ZeroHash, err + } + + h := &commitIndexHelper{ + fs: w.fs, + s: w.r.Storer, + } + + tree, err := h.buildTreeAndBlobObjects(idx) + if err != nil { + return plumbing.ZeroHash, err + } + + commit, err := w.buildCommitObject(msg, opts, tree) + if err != nil { + return plumbing.ZeroHash, err + } + + return commit, w.updateHEAD(commit) +} + +func (w *Worktree) autoAddModifiedAndDeleted() error { + s, err := w.Status() + if err != nil { + return err + } + + for path, fs := range s { + if fs.Worktree != Modified && fs.Worktree != Deleted { + continue + } + + if _, err := w.Add(path); err != nil { + return err + } + + } + + return nil +} + +func (w *Worktree) updateHEAD(commit plumbing.Hash) error { + head, err := w.r.Storer.Reference(plumbing.HEAD) + if err != nil { + return err + } + + name := plumbing.HEAD + if head.Type() != plumbing.HashReference { + name = head.Target() + } + + ref := plumbing.NewHashReference(name, commit) + return w.r.Storer.SetReference(ref) +} + +func (w *Worktree) buildCommitObject(msg string, opts *CommitOptions, tree plumbing.Hash) (plumbing.Hash, error) { + commit := &object.Commit{ + Author: *opts.Author, + Committer: *opts.Committer, + Message: msg, + TreeHash: tree, + ParentHashes: opts.Parents, + } + + obj := w.r.Storer.NewEncodedObject() + if err := commit.Encode(obj); err != nil { + return plumbing.ZeroHash, err + } + return w.r.Storer.SetEncodedObject(obj) +} + +// commitIndexHelper converts a given index.Index file into multiple git objects +// reading the blogs from the given filesystem and creating the trees from the +// index structure. The created objects are pushed to a given Storer. +type commitIndexHelper struct { + fs billy.Filesystem + s storage.Storer + + trees map[string]*object.Tree + entries map[string]*object.TreeEntry +} + +// buildTreesAndBlobs builds the objects and push its to the storer, the hash +// of the root tree is returned. +func (h *commitIndexHelper) buildTreeAndBlobObjects(idx *index.Index) (plumbing.Hash, error) { + const rootNode = "" + h.trees = map[string]*object.Tree{rootNode: {}} + h.entries = map[string]*object.TreeEntry{} + + for _, e := range idx.Entries { + if err := h.commitIndexEntry(e); err != nil { + return plumbing.ZeroHash, err + } + } + + return h.copyTreeToStorageRecursive(rootNode, h.trees[rootNode]) +} + +func (h *commitIndexHelper) commitIndexEntry(e *index.Entry) error { + parts := strings.Split(e.Name, string(filepath.Separator)) + + var path string + for _, part := range parts { + parent := path + path = filepath.Join(path, part) + + if !h.buildTree(e, parent, path) { + continue + } + + if err := h.copyIndexEntryToStorage(e); err != nil { + return err + } + } + + return nil +} + +func (h *commitIndexHelper) buildTree(e *index.Entry, parent, path string) bool { + if _, ok := h.trees[path]; ok { + return false + } + + if _, ok := h.entries[path]; ok { + return false + } + + te := object.TreeEntry{Name: filepath.Base(path)} + + if path == e.Name { + te.Mode = e.Mode + te.Hash = e.Hash + } else { + te.Mode = filemode.Dir + h.trees[path] = &object.Tree{} + } + + h.trees[parent].Entries = append(h.trees[parent].Entries, te) + return true +} + +func (h *commitIndexHelper) copyIndexEntryToStorage(e *index.Entry) error { + _, err := h.s.EncodedObject(plumbing.BlobObject, e.Hash) + if err == nil { + return nil + } + + if err != plumbing.ErrObjectNotFound { + return err + } + + return h.doCopyIndexEntryToStorage(e) +} + +func (h *commitIndexHelper) doCopyIndexEntryToStorage(e *index.Entry) (err error) { + fi, err := h.fs.Stat(e.Name) + if err != nil { + return err + } + + obj := h.s.NewEncodedObject() + obj.SetType(plumbing.BlobObject) + obj.SetSize(fi.Size()) + + reader, err := h.fs.Open(e.Name) + if err != nil { + return err + } + + defer ioutil.CheckClose(reader, &err) + + writer, err := obj.Writer() + if err != nil { + return err + } + + defer ioutil.CheckClose(writer, &err) + + if _, err := io.Copy(writer, reader); err != nil { + return err + } + + _, err = h.s.SetEncodedObject(obj) + return err +} + +func (h *commitIndexHelper) copyTreeToStorageRecursive(parent string, t *object.Tree) (plumbing.Hash, error) { + for i, e := range t.Entries { + if e.Mode != filemode.Dir && !e.Hash.IsZero() { + continue + } + + path := filepath.Join(parent, e.Name) + + var err error + e.Hash, err = h.copyTreeToStorageRecursive(path, h.trees[path]) + if err != nil { + return plumbing.ZeroHash, err + } + + t.Entries[i] = e + } + + o := h.s.NewEncodedObject() + if err := t.Encode(o); err != nil { + return plumbing.ZeroHash, err + } + + return h.s.SetEncodedObject(o) +} diff --git a/worktree_commit_test.go b/worktree_commit_test.go new file mode 100644 index 0000000..f9cf4f3 --- /dev/null +++ b/worktree_commit_test.go @@ -0,0 +1,126 @@ +package git + +import ( + "time" + + "gopkg.in/src-d/go-git.v4/plumbing" + "gopkg.in/src-d/go-git.v4/plumbing/object" + "gopkg.in/src-d/go-git.v4/plumbing/storer" + "gopkg.in/src-d/go-git.v4/storage/memory" + + . "gopkg.in/check.v1" + "gopkg.in/src-d/go-billy.v2" + "gopkg.in/src-d/go-billy.v2/memfs" +) + +func (s *WorktreeSuite) TestCommitInitial(c *C) { + expected := plumbing.NewHash("98c4ac7c29c913f7461eae06e024dc18e80d23a4") + + fs := memfs.New() + storage := memory.NewStorage() + + r, err := Init(storage, fs) + c.Assert(err, IsNil) + + w, err := r.Worktree() + c.Assert(err, IsNil) + + billy.WriteFile(fs, "foo", []byte("foo"), 0644) + + _, err = w.Add("foo") + c.Assert(err, IsNil) + + hash, err := w.Commit("foo\n", &CommitOptions{Author: defaultSignature()}) + c.Assert(hash, Equals, expected) + c.Assert(err, IsNil) + + assertStorageStatus(c, r, 1, 1, 1, expected) +} + +func (s *WorktreeSuite) TestCommitParent(c *C) { + expected := plumbing.NewHash("ef3ca05477530b37f48564be33ddd48063fc7a22") + + fs := memfs.New() + w := &Worktree{ + r: s.Repository, + fs: fs, + } + + err := w.Checkout(&CheckoutOptions{}) + c.Assert(err, IsNil) + + billy.WriteFile(fs, "foo", []byte("foo"), 0644) + + _, err = w.Add("foo") + c.Assert(err, IsNil) + + hash, err := w.Commit("foo\n", &CommitOptions{Author: defaultSignature()}) + c.Assert(hash, Equals, expected) + c.Assert(err, IsNil) + + assertStorageStatus(c, s.Repository, 13, 11, 10, expected) +} + +func (s *WorktreeSuite) TestCommitAll(c *C) { + expected := plumbing.NewHash("aede6f8c9c1c7ec9ca8d287c64b8ed151276fa28") + + fs := memfs.New() + w := &Worktree{ + r: s.Repository, + fs: fs, + } + + err := w.Checkout(&CheckoutOptions{}) + c.Assert(err, IsNil) + + billy.WriteFile(fs, "LICENSE", []byte("foo"), 0644) + + hash, err := w.Commit("foo\n", &CommitOptions{ + All: true, + Author: defaultSignature(), + }) + + c.Assert(hash, Equals, expected) + c.Assert(err, IsNil) + + assertStorageStatus(c, s.Repository, 13, 11, 10, expected) +} + +func assertStorageStatus( + c *C, r *Repository, + treesCount, blobCount, commitCount int, head plumbing.Hash, +) { + trees, err := r.Storer.IterEncodedObjects(plumbing.TreeObject) + c.Assert(err, IsNil) + blobs, err := r.Storer.IterEncodedObjects(plumbing.BlobObject) + c.Assert(err, IsNil) + commits, err := r.Storer.IterEncodedObjects(plumbing.CommitObject) + c.Assert(err, IsNil) + + c.Assert(lenIterEncodedObjects(trees), Equals, treesCount) + c.Assert(lenIterEncodedObjects(blobs), Equals, blobCount) + c.Assert(lenIterEncodedObjects(commits), Equals, commitCount) + + ref, err := r.Head() + c.Assert(err, IsNil) + c.Assert(ref.Hash(), Equals, head) +} + +func lenIterEncodedObjects(iter storer.EncodedObjectIter) int { + count := 0 + iter.ForEach(func(plumbing.EncodedObject) error { + count++ + return nil + }) + + return count +} + +func defaultSignature() *object.Signature { + when, _ := time.Parse(object.DateFormat, "Thu May 04 00:03:43 2017 +0200") + return &object.Signature{ + Name: "foo", + Email: "foo@foo.foo", + When: when, + } +} diff --git a/worktree_status.go b/worktree_status.go index 8dc743d..46953c7 100644 --- a/worktree_status.go +++ b/worktree_status.go @@ -43,16 +43,16 @@ func (w *Worktree) status(commit plumbing.Hash) (Status, error) { return nil, err } + fs := s.File(nameFromAction(&ch)) + fs.Worktree = Unmodified + switch a { case merkletrie.Delete: s.File(ch.From.String()).Staging = Deleted - s.File(ch.From.String()).Worktree = Unmodified case merkletrie.Insert: s.File(ch.To.String()).Staging = Added - s.File(ch.To.String()).Worktree = Unmodified case merkletrie.Modify: s.File(ch.To.String()).Staging = Modified - s.File(ch.To.String()).Worktree = Unmodified } } @@ -67,19 +67,34 @@ func (w *Worktree) status(commit plumbing.Hash) (Status, error) { return nil, err } + fs := s.File(nameFromAction(&ch)) + if fs.Staging == Untracked { + fs.Staging = Unmodified + } + switch a { case merkletrie.Delete: - s.File(ch.From.String()).Worktree = Deleted + fs.Worktree = Deleted case merkletrie.Insert: - s.File(ch.To.String()).Worktree = Untracked + fs.Worktree = Untracked + fs.Staging = Untracked case merkletrie.Modify: - s.File(ch.To.String()).Worktree = Modified + fs.Worktree = Modified } } return s, nil } +func nameFromAction(ch *merkletrie.Change) string { + name := ch.To.String() + if name == "" { + return ch.From.String() + } + + return name +} + func (w *Worktree) diffStagingWithWorktree() (merkletrie.Changes, error) { idx, err := w.r.Storer.Index() if err != nil { |