aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMáximo Cuadros <mcuadros@gmail.com>2017-05-04 01:45:28 +0200
committerMáximo Cuadros <mcuadros@gmail.com>2017-05-04 01:45:28 +0200
commit40fa5882a2c73f8c075403b7ec85870f04deda07 (patch)
tree97a7708e159721f92d643ceb802ee18143d78191
parentff18ce3751ad80cfd0297845872ba1d796c36ca5 (diff)
downloadgo-git-40fa5882a2c73f8c075403b7ec85870f04deda07.tar.gz
worktree: Commit method implementation
-rw-r--r--options.go47
-rw-r--r--status.go2
-rw-r--r--worktree_commit.go243
-rw-r--r--worktree_commit_test.go126
-rw-r--r--worktree_status.go27
5 files changed, 437 insertions, 8 deletions
diff --git a/options.go b/options.go
index 50fba10..48c9819 100644
--- a/options.go
+++ b/options.go
@@ -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
+}
diff --git a/status.go b/status.go
index 2517e50..ef8a500 100644
--- a/status.go
+++ b/status.go
@@ -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 {