aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--config/config.go5
-rw-r--r--internal/url/url.go37
-rw-r--r--plumbing/format/packfile/packfile.go2
-rw-r--r--plumbing/format/packfile/packfile_test.go26
-rw-r--r--plumbing/revlist/revlist.go16
-rw-r--r--plumbing/revlist/revlist_test.go26
-rw-r--r--plumbing/storer/object.go2
-rw-r--r--plumbing/storer/reference.go70
-rw-r--r--plumbing/storer/reference_test.go23
-rw-r--r--plumbing/transport/common.go22
-rw-r--r--remote.go15
-rw-r--r--storage/filesystem/dotgit/dotgit.go3
-rw-r--r--storage/memory/storage.go3
-rw-r--r--storage/storer.go4
-rw-r--r--storage/test/storage_suite.go51
-rw-r--r--storage/transactional/config.go50
-rw-r--r--storage/transactional/config_test.go82
-rw-r--r--storage/transactional/doc.go7
-rw-r--r--storage/transactional/index.go56
-rw-r--r--storage/transactional/index_test.go52
-rw-r--r--storage/transactional/object.go84
-rw-r--r--storage/transactional/object_test.go153
-rw-r--r--storage/transactional/reference.go138
-rw-r--r--storage/transactional/reference_test.go157
-rw-r--r--storage/transactional/shallow.go51
-rw-r--r--storage/transactional/shallow_test.go62
-rw-r--r--storage/transactional/storage.go69
-rw-r--r--storage/transactional/storage_test.go52
-rw-r--r--worktree.go11
-rw-r--r--worktree_test.go2
30 files changed, 1299 insertions, 32 deletions
diff --git a/config/config.go b/config/config.go
index a637f6d..2c3b8b9 100644
--- a/config/config.go
+++ b/config/config.go
@@ -8,6 +8,7 @@ import (
"sort"
"strconv"
+ "gopkg.in/src-d/go-git.v4/internal/url"
format "gopkg.in/src-d/go-git.v4/plumbing/format/config"
)
@@ -399,3 +400,7 @@ func (c *RemoteConfig) marshal() *format.Subsection {
return c.raw
}
+
+func (c *RemoteConfig) IsFirstURLLocal() bool {
+ return url.IsLocalEndpoint(c.URLs[0])
+}
diff --git a/internal/url/url.go b/internal/url/url.go
new file mode 100644
index 0000000..0f0d709
--- /dev/null
+++ b/internal/url/url.go
@@ -0,0 +1,37 @@
+package url
+
+import (
+ "regexp"
+)
+
+var (
+ isSchemeRegExp = regexp.MustCompile(`^[^:]+://`)
+ scpLikeUrlRegExp = regexp.MustCompile(`^(?:(?P<user>[^@]+)@)?(?P<host>[^:\s]+):(?:(?P<port>[0-9]{1,5})/)?(?P<path>[^\\].*)$`)
+)
+
+// MatchesScheme returns true if the given string matches a URL-like
+// format scheme.
+func MatchesScheme(url string) bool {
+ return isSchemeRegExp.MatchString(url)
+}
+
+// MatchesScpLike returns true if the given string matches an SCP-like
+// format scheme.
+func MatchesScpLike(url string) bool {
+ return scpLikeUrlRegExp.MatchString(url)
+}
+
+// FindScpLikeComponents returns the user, host, port and path of the
+// given SCP-like URL.
+func FindScpLikeComponents(url string) (user, host, port, path string) {
+ m := scpLikeUrlRegExp.FindStringSubmatch(url)
+ return m[1], m[2], m[3], m[4]
+}
+
+// IsLocalEndpoint returns true if the given URL string specifies a
+// local file endpoint. For example, on a Linux machine,
+// `/home/user/src/go-git` would match as a local endpoint, but
+// `https://github.com/src-d/go-git` would not.
+func IsLocalEndpoint(url string) bool {
+ return !MatchesScheme(url) && !MatchesScpLike(url)
+}
diff --git a/plumbing/format/packfile/packfile.go b/plumbing/format/packfile/packfile.go
index 1e7ef26..69b6e85 100644
--- a/plumbing/format/packfile/packfile.go
+++ b/plumbing/format/packfile/packfile.go
@@ -107,7 +107,7 @@ func (p *Packfile) GetSizeByOffset(o int64) (size int64, err error) {
if err != nil {
return 0, err
}
- return h.Length, nil
+ return p.getObjectSize(h)
}
func (p *Packfile) objectHeaderAtOffset(offset int64) (*ObjectHeader, error) {
diff --git a/plumbing/format/packfile/packfile_test.go b/plumbing/format/packfile/packfile_test.go
index 05dc8a7..455fe65 100644
--- a/plumbing/format/packfile/packfile_test.go
+++ b/plumbing/format/packfile/packfile_test.go
@@ -277,3 +277,29 @@ func getIndexFromIdxFile(r io.Reader) idxfile.Index {
return idxf
}
+
+func (s *PackfileSuite) TestSize(c *C) {
+ f := fixtures.Basic().ByTag("ref-delta").One()
+
+ index := getIndexFromIdxFile(f.Idx())
+ fs := osfs.New("")
+ pf, err := fs.Open(f.Packfile().Name())
+ c.Assert(err, IsNil)
+
+ packfile := packfile.NewPackfile(index, fs, pf)
+ defer packfile.Close()
+
+ // Get the size of binary.jpg, which is not delta-encoded.
+ offset, err := packfile.FindOffset(plumbing.NewHash("d5c0f4ab811897cadf03aec358ae60d21f91c50d"))
+ c.Assert(err, IsNil)
+ size, err := packfile.GetSizeByOffset(offset)
+ c.Assert(err, IsNil)
+ c.Assert(size, Equals, int64(76110))
+
+ // Get the size of the root commit, which is delta-encoded.
+ offset, err = packfile.FindOffset(f.Head)
+ c.Assert(err, IsNil)
+ size, err = packfile.GetSizeByOffset(offset)
+ c.Assert(err, IsNil)
+ c.Assert(size, Equals, int64(245))
+}
diff --git a/plumbing/revlist/revlist.go b/plumbing/revlist/revlist.go
index 0a9d1e8..7ad71ac 100644
--- a/plumbing/revlist/revlist.go
+++ b/plumbing/revlist/revlist.go
@@ -21,7 +21,20 @@ func Objects(
objs,
ignore []plumbing.Hash,
) ([]plumbing.Hash, error) {
- ignore, err := objects(s, ignore, nil, true)
+ return ObjectsWithStorageForIgnores(s, s, objs, ignore)
+}
+
+// ObjectsWithStorageForIgnores is the same as Objects, but a
+// secondary storage layer can be provided, to be used to finding the
+// full set of objects to be ignored while finding the reachable
+// objects. This is useful when the main `s` storage layer is slow
+// and/or remote, while the ignore list is available somewhere local.
+func ObjectsWithStorageForIgnores(
+ s, ignoreStore storer.EncodedObjectStorer,
+ objs,
+ ignore []plumbing.Hash,
+) ([]plumbing.Hash, error) {
+ ignore, err := objects(ignoreStore, ignore, nil, true)
if err != nil {
return nil, err
}
@@ -114,7 +127,6 @@ func reachableObjects(
i := object.NewCommitPreorderIter(commit, seen, ignore)
pending := make(map[plumbing.Hash]bool)
addPendingParents(pending, visited, commit)
-
for {
commit, err := i.Next()
if err == io.EOF {
diff --git a/plumbing/revlist/revlist_test.go b/plumbing/revlist/revlist_test.go
index dea1c73..ceae727 100644
--- a/plumbing/revlist/revlist_test.go
+++ b/plumbing/revlist/revlist_test.go
@@ -129,6 +129,32 @@ func (s *RevListSuite) TestRevListObjectsTagObject(c *C) {
c.Assert(len(hist), Equals, len(expected))
}
+func (s *RevListSuite) TestRevListObjectsWithStorageForIgnores(c *C) {
+ sto := filesystem.NewStorage(
+ fixtures.ByTag("merge-conflict").One().DotGit(),
+ cache.NewObjectLRUDefault())
+
+ // The "merge-conflict" repo has one extra commit in it, with a
+ // two files modified in two different subdirs.
+ expected := map[string]bool{
+ "1980fcf55330d9d94c34abee5ab734afecf96aba": true, // commit
+ "73d9cf44e9045254346c73f6646b08f9302c8570": true, // root dir
+ "e8435d512a98586bd2e4fcfcdf04101b0bb1b500": true, // go/
+ "257cc5642cb1a054f08cc83f2d943e56fd3ebe99": true, // haskal.hs
+ "d499a1a0b79b7d87a35155afd0c1cce78b37a91c": true, // example.go
+ "d108adc364fb6f21395d011ae2c8a11d96905b0d": true, // haskal/
+ }
+
+ hist, err := ObjectsWithStorageForIgnores(sto, s.Storer, []plumbing.Hash{plumbing.NewHash("1980fcf55330d9d94c34abee5ab734afecf96aba")}, []plumbing.Hash{plumbing.NewHash("6ecf0ef2c2dffb796033e5a02219af86ec6584e5")})
+ c.Assert(err, IsNil)
+
+ for _, h := range hist {
+ c.Assert(expected[h.String()], Equals, true)
+ }
+
+ c.Assert(len(hist), Equals, len(expected))
+}
+
// ---
// | |\
// | | * b8e471f Creating changelog
diff --git a/plumbing/storer/object.go b/plumbing/storer/object.go
index 2ac9b09..98d1ec3 100644
--- a/plumbing/storer/object.go
+++ b/plumbing/storer/object.go
@@ -222,7 +222,7 @@ type MultiEncodedObjectIter struct {
}
// NewMultiEncodedObjectIter returns an object iterator for the given slice of
-// objects.
+// EncodedObjectIters.
func NewMultiEncodedObjectIter(iters []EncodedObjectIter) EncodedObjectIter {
return &MultiEncodedObjectIter{iters: iters}
}
diff --git a/plumbing/storer/reference.go b/plumbing/storer/reference.go
index 5e85a3b..cce72b4 100644
--- a/plumbing/storer/reference.go
+++ b/plumbing/storer/reference.go
@@ -131,9 +131,27 @@ func (iter *ReferenceSliceIter) Next() (*plumbing.Reference, error) {
// an error happens or the end of the iter is reached. If ErrStop is sent
// the iteration is stop but no error is returned. The iterator is closed.
func (iter *ReferenceSliceIter) ForEach(cb func(*plumbing.Reference) error) error {
+ return forEachReferenceIter(iter, cb)
+}
+
+type bareReferenceIterator interface {
+ Next() (*plumbing.Reference, error)
+ Close()
+}
+
+func forEachReferenceIter(iter bareReferenceIterator, cb func(*plumbing.Reference) error) error {
defer iter.Close()
- for _, r := range iter.series {
- if err := cb(r); err != nil {
+ for {
+ obj, err := iter.Next()
+ if err != nil {
+ if err == io.EOF {
+ return nil
+ }
+
+ return err
+ }
+
+ if err := cb(obj); err != nil {
if err == ErrStop {
return nil
}
@@ -141,8 +159,6 @@ func (iter *ReferenceSliceIter) ForEach(cb func(*plumbing.Reference) error) erro
return err
}
}
-
- return nil
}
// Close releases any resources used by the iterator.
@@ -150,6 +166,52 @@ func (iter *ReferenceSliceIter) Close() {
iter.pos = len(iter.series)
}
+// MultiReferenceIter implements ReferenceIter. It iterates over several
+// ReferenceIter,
+//
+// The MultiReferenceIter must be closed with a call to Close() when it is no
+// longer needed.
+type MultiReferenceIter struct {
+ iters []ReferenceIter
+}
+
+// NewMultiReferenceIter returns an reference iterator for the given slice of
+// EncodedObjectIters.
+func NewMultiReferenceIter(iters []ReferenceIter) ReferenceIter {
+ return &MultiReferenceIter{iters: iters}
+}
+
+// Next returns the next reference from the iterator, if one iterator reach
+// io.EOF is removed and the next one is used.
+func (iter *MultiReferenceIter) Next() (*plumbing.Reference, error) {
+ if len(iter.iters) == 0 {
+ return nil, io.EOF
+ }
+
+ obj, err := iter.iters[0].Next()
+ if err == io.EOF {
+ iter.iters[0].Close()
+ iter.iters = iter.iters[1:]
+ return iter.Next()
+ }
+
+ return obj, err
+}
+
+// ForEach call the cb function for each reference contained on this iter until
+// an error happens or the end of the iter is reached. If ErrStop is sent
+// the iteration is stop but no error is returned. The iterator is closed.
+func (iter *MultiReferenceIter) ForEach(cb func(*plumbing.Reference) error) error {
+ return forEachReferenceIter(iter, cb)
+}
+
+// Close releases any resources used by the iterator.
+func (iter *MultiReferenceIter) Close() {
+ for _, i := range iter.iters {
+ i.Close()
+ }
+}
+
// ResolveReference resolves a SymbolicReference to a HashReference.
func ResolveReference(s ReferenceStorer, n plumbing.ReferenceName) (*plumbing.Reference, error) {
r, err := s.Reference(n)
diff --git a/plumbing/storer/reference_test.go b/plumbing/storer/reference_test.go
index 490ec95..1d02c22 100644
--- a/plumbing/storer/reference_test.go
+++ b/plumbing/storer/reference_test.go
@@ -172,3 +172,26 @@ func (s *ReferenceSuite) TestReferenceFilteredIterForEachStop(c *C) {
c.Assert(count, Equals, 1)
}
+
+func (s *ReferenceSuite) TestMultiReferenceIterForEach(c *C) {
+ i := NewMultiReferenceIter(
+ []ReferenceIter{
+ NewReferenceSliceIter([]*plumbing.Reference{
+ plumbing.NewReferenceFromStrings("foo", "foo"),
+ }),
+ NewReferenceSliceIter([]*plumbing.Reference{
+ plumbing.NewReferenceFromStrings("bar", "bar"),
+ }),
+ },
+ )
+
+ var result []string
+ err := i.ForEach(func(r *plumbing.Reference) error {
+ result = append(result, r.Name().String())
+ return nil
+ })
+
+ c.Assert(err, IsNil)
+ c.Assert(result, HasLen, 2)
+ c.Assert(result, DeepEquals, []string{"foo", "bar"})
+}
diff --git a/plumbing/transport/common.go b/plumbing/transport/common.go
index f7b882b..dcf9391 100644
--- a/plumbing/transport/common.go
+++ b/plumbing/transport/common.go
@@ -19,10 +19,10 @@ import (
"fmt"
"io"
"net/url"
- "regexp"
"strconv"
"strings"
+ giturl "gopkg.in/src-d/go-git.v4/internal/url"
"gopkg.in/src-d/go-git.v4/plumbing"
"gopkg.in/src-d/go-git.v4/plumbing/protocol/packp"
"gopkg.in/src-d/go-git.v4/plumbing/protocol/packp/capability"
@@ -224,34 +224,28 @@ func getPath(u *url.URL) string {
return res
}
-var (
- isSchemeRegExp = regexp.MustCompile(`^[^:]+://`)
- scpLikeUrlRegExp = regexp.MustCompile(`^(?:(?P<user>[^@]+)@)?(?P<host>[^:\s]+):(?:(?P<port>[0-9]{1,5})/)?(?P<path>[^\\].*)$`)
-)
-
func parseSCPLike(endpoint string) (*Endpoint, bool) {
- if isSchemeRegExp.MatchString(endpoint) || !scpLikeUrlRegExp.MatchString(endpoint) {
+ if giturl.MatchesScheme(endpoint) || !giturl.MatchesScpLike(endpoint) {
return nil, false
}
- m := scpLikeUrlRegExp.FindStringSubmatch(endpoint)
-
- port, err := strconv.Atoi(m[3])
+ user, host, portStr, path := giturl.FindScpLikeComponents(endpoint)
+ port, err := strconv.Atoi(portStr)
if err != nil {
port = 22
}
return &Endpoint{
Protocol: "ssh",
- User: m[1],
- Host: m[2],
+ User: user,
+ Host: host,
Port: port,
- Path: m[4],
+ Path: path,
}, true
}
func parseFile(endpoint string) (*Endpoint, bool) {
- if isSchemeRegExp.MatchString(endpoint) {
+ if giturl.MatchesScheme(endpoint) {
return nil, false
}
diff --git a/remote.go b/remote.go
index 8f4e41d..de537ce 100644
--- a/remote.go
+++ b/remote.go
@@ -6,8 +6,10 @@ import (
"fmt"
"io"
+ "gopkg.in/src-d/go-billy.v4/osfs"
"gopkg.in/src-d/go-git.v4/config"
"gopkg.in/src-d/go-git.v4/plumbing"
+ "gopkg.in/src-d/go-git.v4/plumbing/cache"
"gopkg.in/src-d/go-git.v4/plumbing/format/packfile"
"gopkg.in/src-d/go-git.v4/plumbing/object"
"gopkg.in/src-d/go-git.v4/plumbing/protocol/packp"
@@ -18,6 +20,7 @@ import (
"gopkg.in/src-d/go-git.v4/plumbing/transport"
"gopkg.in/src-d/go-git.v4/plumbing/transport/client"
"gopkg.in/src-d/go-git.v4/storage"
+ "gopkg.in/src-d/go-git.v4/storage/filesystem"
"gopkg.in/src-d/go-git.v4/storage/memory"
"gopkg.in/src-d/go-git.v4/utils/ioutil"
)
@@ -149,7 +152,17 @@ func (r *Remote) PushContext(ctx context.Context, o *PushOptions) (err error) {
var hashesToPush []plumbing.Hash
// Avoid the expensive revlist operation if we're only doing deletes.
if !allDelete {
- hashesToPush, err = revlist.Objects(r.s, objects, haves)
+ if r.c.IsFirstURLLocal() {
+ // If we're are pushing to a local repo, it might be much
+ // faster to use a local storage layer to get the commits
+ // to ignore, when calculating the object revlist.
+ localStorer := filesystem.NewStorage(
+ osfs.New(r.c.URLs[0]), cache.NewObjectLRUDefault())
+ hashesToPush, err = revlist.ObjectsWithStorageForIgnores(
+ r.s, localStorer, objects, haves)
+ } else {
+ hashesToPush, err = revlist.Objects(r.s, objects, haves)
+ }
if err != nil {
return err
}
diff --git a/storage/filesystem/dotgit/dotgit.go b/storage/filesystem/dotgit/dotgit.go
index a58c248..ba9667e 100644
--- a/storage/filesystem/dotgit/dotgit.go
+++ b/storage/filesystem/dotgit/dotgit.go
@@ -14,6 +14,7 @@ import (
"gopkg.in/src-d/go-billy.v4/osfs"
"gopkg.in/src-d/go-git.v4/plumbing"
+ "gopkg.in/src-d/go-git.v4/storage"
"gopkg.in/src-d/go-git.v4/utils/ioutil"
"gopkg.in/src-d/go-billy.v4"
@@ -596,7 +597,7 @@ func (d *DotGit) checkReferenceAndTruncate(f billy.File, old *plumbing.Reference
return err
}
if ref.Hash() != old.Hash() {
- return fmt.Errorf("reference has changed concurrently")
+ return storage.ErrReferenceHasChanged
}
_, err = f.Seek(0, io.SeekStart)
if err != nil {
diff --git a/storage/memory/storage.go b/storage/memory/storage.go
index 6e11742..f240f2a 100644
--- a/storage/memory/storage.go
+++ b/storage/memory/storage.go
@@ -13,7 +13,6 @@ import (
)
var ErrUnsupportedObjectType = fmt.Errorf("unsupported object type")
-var ErrRefHasChanged = fmt.Errorf("reference has changed concurrently")
// Storage is an implementation of git.Storer that stores data on memory, being
// ephemeral. The use of this storage should be done in controlled envoriments,
@@ -258,7 +257,7 @@ func (r ReferenceStorage) CheckAndSetReference(ref, old *plumbing.Reference) err
if old != nil {
tmp := r[ref.Name()]
if tmp != nil && tmp.Hash() != old.Hash() {
- return ErrRefHasChanged
+ return storage.ErrReferenceHasChanged
}
}
r[ref.Name()] = ref
diff --git a/storage/storer.go b/storage/storer.go
index d1a94f2..5de0cfb 100644
--- a/storage/storer.go
+++ b/storage/storer.go
@@ -1,10 +1,14 @@
package storage
import (
+ "errors"
+
"gopkg.in/src-d/go-git.v4/config"
"gopkg.in/src-d/go-git.v4/plumbing/storer"
)
+var ErrReferenceHasChanged = errors.New("reference has changed concurrently")
+
// Storer is a generic storage of objects, references and any information
// related to a particular repository. The package gopkg.in/src-d/go-git.v4/storage
// contains two implementation a filesystem base implementation (such as `.git`)
diff --git a/storage/test/storage_suite.go b/storage/test/storage_suite.go
index 79a12c5..e050b73 100644
--- a/storage/test/storage_suite.go
+++ b/storage/test/storage_suite.go
@@ -280,6 +280,57 @@ func (s *BaseStorageSuite) TestSetReferenceAndGetReference(c *C) {
c.Assert(e.Hash().String(), Equals, "bc9968d75e48de59f0870ffb71f5e160bbbdcf52")
}
+func (s *BaseStorageSuite) TestCheckAndSetReference(c *C) {
+ err := s.Storer.SetReference(
+ plumbing.NewReferenceFromStrings("foo", "482e0eada5de4039e6f216b45b3c9b683b83bfa"),
+ )
+ c.Assert(err, IsNil)
+
+ err = s.Storer.CheckAndSetReference(
+ plumbing.NewReferenceFromStrings("foo", "bc9968d75e48de59f0870ffb71f5e160bbbdcf52"),
+ plumbing.NewReferenceFromStrings("foo", "482e0eada5de4039e6f216b45b3c9b683b83bfa"),
+ )
+ c.Assert(err, IsNil)
+
+ e, err := s.Storer.Reference(plumbing.ReferenceName("foo"))
+ c.Assert(err, IsNil)
+ c.Assert(e.Hash().String(), Equals, "bc9968d75e48de59f0870ffb71f5e160bbbdcf52")
+}
+
+func (s *BaseStorageSuite) TestCheckAndSetReferenceNil(c *C) {
+ err := s.Storer.SetReference(
+ plumbing.NewReferenceFromStrings("foo", "482e0eada5de4039e6f216b45b3c9b683b83bfa"),
+ )
+ c.Assert(err, IsNil)
+
+ err = s.Storer.CheckAndSetReference(
+ plumbing.NewReferenceFromStrings("foo", "bc9968d75e48de59f0870ffb71f5e160bbbdcf52"),
+ nil,
+ )
+ c.Assert(err, IsNil)
+
+ e, err := s.Storer.Reference(plumbing.ReferenceName("foo"))
+ c.Assert(err, IsNil)
+ c.Assert(e.Hash().String(), Equals, "bc9968d75e48de59f0870ffb71f5e160bbbdcf52")
+}
+
+func (s *BaseStorageSuite) TestCheckAndSetReferenceError(c *C) {
+ err := s.Storer.SetReference(
+ plumbing.NewReferenceFromStrings("foo", "c3f4688a08fd86f1bf8e055724c84b7a40a09733"),
+ )
+ c.Assert(err, IsNil)
+
+ err = s.Storer.CheckAndSetReference(
+ plumbing.NewReferenceFromStrings("foo", "bc9968d75e48de59f0870ffb71f5e160bbbdcf52"),
+ plumbing.NewReferenceFromStrings("foo", "482e0eada5de4039e6f216b45b3c9b683b83bfa"),
+ )
+ c.Assert(err, Equals, storage.ErrReferenceHasChanged)
+
+ e, err := s.Storer.Reference(plumbing.ReferenceName("foo"))
+ c.Assert(err, IsNil)
+ c.Assert(e.Hash().String(), Equals, "c3f4688a08fd86f1bf8e055724c84b7a40a09733")
+}
+
func (s *BaseStorageSuite) TestRemoveReference(c *C) {
err := s.Storer.SetReference(
plumbing.NewReferenceFromStrings("foo", "bc9968d75e48de59f0870ffb71f5e160bbbdcf52"),
diff --git a/storage/transactional/config.go b/storage/transactional/config.go
new file mode 100644
index 0000000..4d8efe1
--- /dev/null
+++ b/storage/transactional/config.go
@@ -0,0 +1,50 @@
+package transactional
+
+import "gopkg.in/src-d/go-git.v4/config"
+
+// ConfigStorage implements the storer.ConfigStorage for the transactional package.
+type ConfigStorage struct {
+ config.ConfigStorer
+ temporal config.ConfigStorer
+
+ set bool
+}
+
+// NewConfigStorage returns a new ConfigStorer based on a base storer and a
+// temporal storer.
+func NewConfigStorage(s, temporal config.ConfigStorer) *ConfigStorage {
+ return &ConfigStorage{ConfigStorer: s, temporal: temporal}
+}
+
+// SetConfig honors the storer.ConfigStorer interface.
+func (c *ConfigStorage) SetConfig(cfg *config.Config) error {
+ if err := c.temporal.SetConfig(cfg); err != nil {
+ return err
+ }
+
+ c.set = true
+ return nil
+}
+
+// Config honors the storer.ConfigStorer interface.
+func (c *ConfigStorage) Config() (*config.Config, error) {
+ if !c.set {
+ return c.ConfigStorer.Config()
+ }
+
+ return c.temporal.Config()
+}
+
+// Commit it copies the config from the temporal storage into the base storage.
+func (c *ConfigStorage) Commit() error {
+ if !c.set {
+ return nil
+ }
+
+ cfg, err := c.temporal.Config()
+ if err != nil {
+ return err
+ }
+
+ return c.ConfigStorer.SetConfig(cfg)
+}
diff --git a/storage/transactional/config_test.go b/storage/transactional/config_test.go
new file mode 100644
index 0000000..5d1e019
--- /dev/null
+++ b/storage/transactional/config_test.go
@@ -0,0 +1,82 @@
+package transactional
+
+import (
+ . "gopkg.in/check.v1"
+ "gopkg.in/src-d/go-git.v4/config"
+ "gopkg.in/src-d/go-git.v4/storage/memory"
+)
+
+var _ = Suite(&ConfigSuite{})
+
+type ConfigSuite struct{}
+
+func (s *ConfigSuite) TestSetConfigBase(c *C) {
+ cfg := config.NewConfig()
+ cfg.Core.Worktree = "foo"
+
+ base := memory.NewStorage()
+ err := base.SetConfig(cfg)
+ c.Assert(err, IsNil)
+
+ temporal := memory.NewStorage()
+ cs := NewConfigStorage(base, temporal)
+
+ cfg, err = cs.Config()
+ c.Assert(err, IsNil)
+ c.Assert(cfg.Core.Worktree, Equals, "foo")
+}
+
+func (s *ConfigSuite) TestSetConfigTemporal(c *C) {
+ cfg := config.NewConfig()
+ cfg.Core.Worktree = "foo"
+
+ base := memory.NewStorage()
+ err := base.SetConfig(cfg)
+ c.Assert(err, IsNil)
+
+ temporal := memory.NewStorage()
+
+ cfg = config.NewConfig()
+ cfg.Core.Worktree = "bar"
+
+ cs := NewConfigStorage(base, temporal)
+ err = cs.SetConfig(cfg)
+ c.Assert(err, IsNil)
+
+ baseCfg, err := base.Config()
+ c.Assert(err, IsNil)
+ c.Assert(baseCfg.Core.Worktree, Equals, "foo")
+
+ temporalCfg, err := temporal.Config()
+ c.Assert(err, IsNil)
+ c.Assert(temporalCfg.Core.Worktree, Equals, "bar")
+
+ cfg, err = cs.Config()
+ c.Assert(err, IsNil)
+ c.Assert(temporalCfg.Core.Worktree, Equals, "bar")
+}
+
+func (s *ConfigSuite) TestCommit(c *C) {
+ cfg := config.NewConfig()
+ cfg.Core.Worktree = "foo"
+
+ base := memory.NewStorage()
+ err := base.SetConfig(cfg)
+ c.Assert(err, IsNil)
+
+ temporal := memory.NewStorage()
+
+ cfg = config.NewConfig()
+ cfg.Core.Worktree = "bar"
+
+ cs := NewConfigStorage(base, temporal)
+ err = cs.SetConfig(cfg)
+ c.Assert(err, IsNil)
+
+ err = cs.Commit()
+ c.Assert(err, IsNil)
+
+ baseCfg, err := base.Config()
+ c.Assert(err, IsNil)
+ c.Assert(baseCfg.Core.Worktree, Equals, "bar")
+}
diff --git a/storage/transactional/doc.go b/storage/transactional/doc.go
new file mode 100644
index 0000000..3a68f5f
--- /dev/null
+++ b/storage/transactional/doc.go
@@ -0,0 +1,7 @@
+// Package transactional is a transactional implementation of git.Storer, it
+// demux the write and read operation of two separate storers, allowing to merge
+// content calling Storage.Commit.
+//
+// The API and functionality of this package are considered EXPERIMENTAL and is
+// not considered stable nor production ready.
+package transactional
diff --git a/storage/transactional/index.go b/storage/transactional/index.go
new file mode 100644
index 0000000..84e0e2f
--- /dev/null
+++ b/storage/transactional/index.go
@@ -0,0 +1,56 @@
+package transactional
+
+import (
+ "gopkg.in/src-d/go-git.v4/plumbing/format/index"
+ "gopkg.in/src-d/go-git.v4/plumbing/storer"
+)
+
+// IndexStorage implements the storer.IndexStorage for the transactional package.
+type IndexStorage struct {
+ storer.IndexStorer
+ temporal storer.IndexStorer
+
+ set bool
+}
+
+// NewIndexStorage returns a new IndexStorer based on a base storer and a
+// temporal storer.
+func NewIndexStorage(s, temporal storer.IndexStorer) *IndexStorage {
+ return &IndexStorage{
+ IndexStorer: s,
+ temporal: temporal,
+ }
+}
+
+// SetIndex honors the storer.IndexStorer interface.
+func (s *IndexStorage) SetIndex(idx *index.Index) (err error) {
+ if err := s.temporal.SetIndex(idx); err != nil {
+ return err
+ }
+
+ s.set = true
+ return nil
+}
+
+// Index honors the storer.IndexStorer interface.
+func (s *IndexStorage) Index() (*index.Index, error) {
+ if !s.set {
+ return s.IndexStorer.Index()
+ }
+
+ return s.temporal.Index()
+}
+
+// Commit it copies the index from the temporal storage into the base storage.
+func (s *IndexStorage) Commit() error {
+ if !s.set {
+ return nil
+ }
+
+ idx, err := s.temporal.Index()
+ if err != nil {
+ return err
+ }
+
+ return s.IndexStorer.SetIndex(idx)
+}
diff --git a/storage/transactional/index_test.go b/storage/transactional/index_test.go
new file mode 100644
index 0000000..e1c571a
--- /dev/null
+++ b/storage/transactional/index_test.go
@@ -0,0 +1,52 @@
+package transactional
+
+import (
+ . "gopkg.in/check.v1"
+ "gopkg.in/src-d/go-git.v4/plumbing/format/index"
+ "gopkg.in/src-d/go-git.v4/storage/memory"
+)
+
+var _ = Suite(&IndexSuite{})
+
+type IndexSuite struct{}
+
+func (s *IndexSuite) TestSetIndexBase(c *C) {
+ idx := &index.Index{}
+ idx.Version = 2
+
+ base := memory.NewStorage()
+ err := base.SetIndex(idx)
+ c.Assert(err, IsNil)
+
+ temporal := memory.NewStorage()
+ cs := NewIndexStorage(base, temporal)
+
+ idx, err = cs.Index()
+ c.Assert(err, IsNil)
+ c.Assert(idx.Version, Equals, uint32(2))
+}
+
+func (s *IndexSuite) TestCommit(c *C) {
+ idx := &index.Index{}
+ idx.Version = 2
+
+ base := memory.NewStorage()
+ err := base.SetIndex(idx)
+ c.Assert(err, IsNil)
+
+ temporal := memory.NewStorage()
+
+ idx = &index.Index{}
+ idx.Version = 3
+
+ is := NewIndexStorage(base, temporal)
+ err = is.SetIndex(idx)
+ c.Assert(err, IsNil)
+
+ err = is.Commit()
+ c.Assert(err, IsNil)
+
+ baseIndex, err := base.Index()
+ c.Assert(err, IsNil)
+ c.Assert(baseIndex.Version, Equals, uint32(3))
+}
diff --git a/storage/transactional/object.go b/storage/transactional/object.go
new file mode 100644
index 0000000..beb63d6
--- /dev/null
+++ b/storage/transactional/object.go
@@ -0,0 +1,84 @@
+package transactional
+
+import (
+ "gopkg.in/src-d/go-git.v4/plumbing"
+ "gopkg.in/src-d/go-git.v4/plumbing/storer"
+)
+
+// ObjectStorage implements the storer.EncodedObjectStorer for the transactional package.
+type ObjectStorage struct {
+ storer.EncodedObjectStorer
+ temporal storer.EncodedObjectStorer
+}
+
+// NewObjectStorage returns a new EncodedObjectStorer based on a base storer and
+// a temporal storer.
+func NewObjectStorage(base, temporal storer.EncodedObjectStorer) *ObjectStorage {
+ return &ObjectStorage{EncodedObjectStorer: base, temporal: temporal}
+}
+
+// SetEncodedObject honors the storer.EncodedObjectStorer interface.
+func (o *ObjectStorage) SetEncodedObject(obj plumbing.EncodedObject) (plumbing.Hash, error) {
+ return o.temporal.SetEncodedObject(obj)
+}
+
+// HasEncodedObject honors the storer.EncodedObjectStorer interface.
+func (o *ObjectStorage) HasEncodedObject(h plumbing.Hash) error {
+ err := o.EncodedObjectStorer.HasEncodedObject(h)
+ if err == plumbing.ErrObjectNotFound {
+ return o.temporal.HasEncodedObject(h)
+ }
+
+ return err
+}
+
+// EncodedObjectSize honors the storer.EncodedObjectStorer interface.
+func (o *ObjectStorage) EncodedObjectSize(h plumbing.Hash) (int64, error) {
+ sz, err := o.EncodedObjectStorer.EncodedObjectSize(h)
+ if err == plumbing.ErrObjectNotFound {
+ return o.temporal.EncodedObjectSize(h)
+ }
+
+ return sz, err
+}
+
+// EncodedObject honors the storer.EncodedObjectStorer interface.
+func (o *ObjectStorage) EncodedObject(t plumbing.ObjectType, h plumbing.Hash) (plumbing.EncodedObject, error) {
+ obj, err := o.EncodedObjectStorer.EncodedObject(t, h)
+ if err == plumbing.ErrObjectNotFound {
+ return o.temporal.EncodedObject(t, h)
+ }
+
+ return obj, err
+}
+
+// IterEncodedObjects honors the storer.EncodedObjectStorer interface.
+func (o *ObjectStorage) IterEncodedObjects(t plumbing.ObjectType) (storer.EncodedObjectIter, error) {
+ baseIter, err := o.EncodedObjectStorer.IterEncodedObjects(t)
+ if err != nil {
+ return nil, err
+ }
+
+ temporalIter, err := o.temporal.IterEncodedObjects(t)
+ if err != nil {
+ return nil, err
+ }
+
+ return storer.NewMultiEncodedObjectIter([]storer.EncodedObjectIter{
+ baseIter,
+ temporalIter,
+ }), nil
+}
+
+// Commit it copies the objects of the temporal storage into the base storage.
+func (o *ObjectStorage) Commit() error {
+ iter, err := o.temporal.IterEncodedObjects(plumbing.AnyObject)
+ if err != nil {
+ return err
+ }
+
+ return iter.ForEach(func(obj plumbing.EncodedObject) error {
+ _, err := o.EncodedObjectStorer.SetEncodedObject(obj)
+ return err
+ })
+}
diff --git a/storage/transactional/object_test.go b/storage/transactional/object_test.go
new file mode 100644
index 0000000..10b6318
--- /dev/null
+++ b/storage/transactional/object_test.go
@@ -0,0 +1,153 @@
+package transactional
+
+import (
+ . "gopkg.in/check.v1"
+ "gopkg.in/src-d/go-git.v4/plumbing"
+ "gopkg.in/src-d/go-git.v4/storage/memory"
+)
+
+var _ = Suite(&ObjectSuite{})
+
+type ObjectSuite struct{}
+
+func (s *ObjectSuite) TestHasEncodedObject(c *C) {
+ base := memory.NewStorage()
+ temporal := memory.NewStorage()
+
+ os := NewObjectStorage(base, temporal)
+
+ commit := base.NewEncodedObject()
+ commit.SetType(plumbing.CommitObject)
+
+ ch, err := base.SetEncodedObject(commit)
+ c.Assert(ch.IsZero(), Equals, false)
+ c.Assert(err, IsNil)
+
+ tree := base.NewEncodedObject()
+ tree.SetType(plumbing.TreeObject)
+
+ th, err := os.SetEncodedObject(tree)
+ c.Assert(th.IsZero(), Equals, false)
+ c.Assert(err, IsNil)
+
+ err = os.HasEncodedObject(th)
+ c.Assert(err, IsNil)
+
+ err = os.HasEncodedObject(ch)
+ c.Assert(err, IsNil)
+
+ err = base.HasEncodedObject(th)
+ c.Assert(err, Equals, plumbing.ErrObjectNotFound)
+}
+
+func (s *ObjectSuite) TestEncodedObjectAndEncodedObjectSize(c *C) {
+ base := memory.NewStorage()
+ temporal := memory.NewStorage()
+
+ os := NewObjectStorage(base, temporal)
+
+ commit := base.NewEncodedObject()
+ commit.SetType(plumbing.CommitObject)
+
+ ch, err := base.SetEncodedObject(commit)
+ c.Assert(ch.IsZero(), Equals, false)
+ c.Assert(err, IsNil)
+
+ tree := base.NewEncodedObject()
+ tree.SetType(plumbing.TreeObject)
+
+ th, err := os.SetEncodedObject(tree)
+ c.Assert(th.IsZero(), Equals, false)
+ c.Assert(err, IsNil)
+
+ otree, err := os.EncodedObject(plumbing.TreeObject, th)
+ c.Assert(err, IsNil)
+ c.Assert(otree.Hash(), Equals, tree.Hash())
+
+ treeSz, err := os.EncodedObjectSize(th)
+ c.Assert(err, IsNil)
+ c.Assert(treeSz, Equals, int64(0))
+
+ ocommit, err := os.EncodedObject(plumbing.CommitObject, ch)
+ c.Assert(err, IsNil)
+ c.Assert(ocommit.Hash(), Equals, commit.Hash())
+
+ commitSz, err := os.EncodedObjectSize(ch)
+ c.Assert(err, IsNil)
+ c.Assert(commitSz, Equals, int64(0))
+
+ _, err = base.EncodedObject(plumbing.TreeObject, th)
+ c.Assert(err, Equals, plumbing.ErrObjectNotFound)
+
+ _, err = base.EncodedObjectSize(th)
+ c.Assert(err, Equals, plumbing.ErrObjectNotFound)
+}
+
+func (s *ObjectSuite) TestIterEncodedObjects(c *C) {
+ base := memory.NewStorage()
+ temporal := memory.NewStorage()
+
+ os := NewObjectStorage(base, temporal)
+
+ commit := base.NewEncodedObject()
+ commit.SetType(plumbing.CommitObject)
+
+ ch, err := base.SetEncodedObject(commit)
+ c.Assert(ch.IsZero(), Equals, false)
+ c.Assert(err, IsNil)
+
+ tree := base.NewEncodedObject()
+ tree.SetType(plumbing.TreeObject)
+
+ th, err := os.SetEncodedObject(tree)
+ c.Assert(th.IsZero(), Equals, false)
+ c.Assert(err, IsNil)
+
+ iter, err := os.IterEncodedObjects(plumbing.AnyObject)
+ c.Assert(err, IsNil)
+
+ var hashes []plumbing.Hash
+ err = iter.ForEach(func(obj plumbing.EncodedObject) error {
+ hashes = append(hashes, obj.Hash())
+ return nil
+ })
+
+ c.Assert(err, IsNil)
+ c.Assert(hashes, HasLen, 2)
+ c.Assert(hashes[0], Equals, ch)
+ c.Assert(hashes[1], Equals, th)
+}
+
+func (s *ObjectSuite) TestCommit(c *C) {
+ base := memory.NewStorage()
+ temporal := memory.NewStorage()
+
+ os := NewObjectStorage(base, temporal)
+
+ commit := base.NewEncodedObject()
+ commit.SetType(plumbing.CommitObject)
+
+ _, err := os.SetEncodedObject(commit)
+ c.Assert(err, IsNil)
+
+ tree := base.NewEncodedObject()
+ tree.SetType(plumbing.TreeObject)
+
+ _, err = os.SetEncodedObject(tree)
+ c.Assert(err, IsNil)
+
+ err = os.Commit()
+ c.Assert(err, IsNil)
+
+ iter, err := base.IterEncodedObjects(plumbing.AnyObject)
+ c.Assert(err, IsNil)
+
+ var hashes []plumbing.Hash
+ err = iter.ForEach(func(obj plumbing.EncodedObject) error {
+ hashes = append(hashes, obj.Hash())
+ return nil
+ })
+
+ c.Assert(err, IsNil)
+ c.Assert(hashes, HasLen, 2)
+}
diff --git a/storage/transactional/reference.go b/storage/transactional/reference.go
new file mode 100644
index 0000000..a7be532
--- /dev/null
+++ b/storage/transactional/reference.go
@@ -0,0 +1,138 @@
+package transactional
+
+import (
+ "gopkg.in/src-d/go-git.v4/plumbing"
+ "gopkg.in/src-d/go-git.v4/plumbing/storer"
+ "gopkg.in/src-d/go-git.v4/storage"
+)
+
+// ReferenceStorage implements the storer.ReferenceStorage for the transactional package.
+type ReferenceStorage struct {
+ storer.ReferenceStorer
+ temporal storer.ReferenceStorer
+
+ // deleted, remaining references at this maps are going to be deleted when
+ // commit is requested, the entries are added when RemoveReference is called
+ // and deleted if SetReference is called.
+ deleted map[plumbing.ReferenceName]struct{}
+ // packRefs if true PackRefs is going to be called in the based storer when
+ // commit is called.
+ packRefs bool
+}
+
+// NewReferenceStorage returns a new ReferenceStorer based on a base storer and
+// a temporal storer.
+func NewReferenceStorage(base, temporal storer.ReferenceStorer) *ReferenceStorage {
+ return &ReferenceStorage{
+ ReferenceStorer: base,
+ temporal: temporal,
+
+ deleted: make(map[plumbing.ReferenceName]struct{}, 0),
+ }
+}
+
+// SetReference honors the storer.ReferenceStorer interface.
+func (r *ReferenceStorage) SetReference(ref *plumbing.Reference) error {
+ delete(r.deleted, ref.Name())
+ return r.temporal.SetReference(ref)
+}
+
+// SetReference honors the storer.ReferenceStorer interface.
+func (r *ReferenceStorage) CheckAndSetReference(ref, old *plumbing.Reference) error {
+ if old == nil {
+ return r.SetReference(ref)
+ }
+
+ tmp, err := r.temporal.Reference(old.Name())
+ if err == plumbing.ErrReferenceNotFound {
+ tmp, err = r.ReferenceStorer.Reference(old.Name())
+ }
+
+ if err != nil {
+ return err
+ }
+
+ if tmp.Hash() != old.Hash() {
+ return storage.ErrReferenceHasChanged
+ }
+
+ return r.SetReference(ref)
+}
+
+// Reference honors the storer.ReferenceStorer interface.
+func (r ReferenceStorage) Reference(n plumbing.ReferenceName) (*plumbing.Reference, error) {
+ if _, deleted := r.deleted[n]; deleted {
+ return nil, plumbing.ErrReferenceNotFound
+ }
+
+ ref, err := r.temporal.Reference(n)
+ if err == plumbing.ErrReferenceNotFound {
+ return r.ReferenceStorer.Reference(n)
+ }
+
+ return ref, err
+}
+
+// IterReferences honors the storer.ReferenceStorer interface.
+func (r ReferenceStorage) IterReferences() (storer.ReferenceIter, error) {
+ baseIter, err := r.ReferenceStorer.IterReferences()
+ if err != nil {
+ return nil, err
+ }
+
+ temporalIter, err := r.temporal.IterReferences()
+ if err != nil {
+ return nil, err
+ }
+
+ return storer.NewMultiReferenceIter([]storer.ReferenceIter{
+ baseIter,
+ temporalIter,
+ }), nil
+}
+
+// CountLooseRefs honors the storer.ReferenceStorer interface.
+func (r ReferenceStorage) CountLooseRefs() (int, error) {
+ tc, err := r.temporal.CountLooseRefs()
+ if err != nil {
+ return -1, err
+ }
+
+ bc, err := r.ReferenceStorer.CountLooseRefs()
+ if err != nil {
+ return -1, err
+ }
+
+ return tc + bc, nil
+}
+
+// PackRefs honors the storer.ReferenceStorer interface.
+func (r ReferenceStorage) PackRefs() error {
+ r.packRefs = true
+ return nil
+}
+
+// RemoveReference honors the storer.ReferenceStorer interface.
+func (r ReferenceStorage) RemoveReference(n plumbing.ReferenceName) error {
+ r.deleted[n] = struct{}{}
+ return r.temporal.RemoveReference(n)
+}
+
+// Commit it copies the reference information of the temporal storage into the
+// base storage.
+func (r ReferenceStorage) Commit() error {
+ for name := range r.deleted {
+ if err := r.ReferenceStorer.RemoveReference(name); err != nil {
+ return err
+ }
+ }
+
+ iter, err := r.temporal.IterReferences()
+ if err != nil {
+ return err
+ }
+
+ return iter.ForEach(func(ref *plumbing.Reference) error {
+ return r.ReferenceStorer.SetReference(ref)
+ })
+}
diff --git a/storage/transactional/reference_test.go b/storage/transactional/reference_test.go
new file mode 100644
index 0000000..5793549
--- /dev/null
+++ b/storage/transactional/reference_test.go
@@ -0,0 +1,157 @@
+package transactional
+
+import (
+ . "gopkg.in/check.v1"
+ "gopkg.in/src-d/go-git.v4/plumbing"
+ "gopkg.in/src-d/go-git.v4/storage/memory"
+)
+
+var _ = Suite(&ReferenceSuite{})
+
+type ReferenceSuite struct{}
+
+func (s *ReferenceSuite) TestReference(c *C) {
+ base := memory.NewStorage()
+ temporal := memory.NewStorage()
+
+ rs := NewReferenceStorage(base, temporal)
+
+ refA := plumbing.NewReferenceFromStrings("refs/a", "bc9968d75e48de59f0870ffb71f5e160bbbdcf52")
+ refB := plumbing.NewReferenceFromStrings("refs/b", "bc9968d75e48de59f0870ffb71f5e160bbbdcf52")
+
+ err := base.SetReference(refA)
+ c.Assert(err, IsNil)
+
+ err = rs.SetReference(refB)
+ c.Assert(err, IsNil)
+
+ _, err = rs.Reference("refs/a")
+ c.Assert(err, IsNil)
+
+ _, err = rs.Reference("refs/b")
+ c.Assert(err, IsNil)
+
+ _, err = base.Reference("refs/b")
+ c.Assert(err, Equals, plumbing.ErrReferenceNotFound)
+}
+
+func (s *ReferenceSuite) TestRemoveReferenceTemporal(c *C) {
+ base := memory.NewStorage()
+ temporal := memory.NewStorage()
+
+ ref := plumbing.NewReferenceFromStrings("refs/a", "bc9968d75e48de59f0870ffb71f5e160bbbdcf52")
+
+ rs := NewReferenceStorage(base, temporal)
+ err := rs.SetReference(ref)
+ c.Assert(err, IsNil)
+
+ err = rs.RemoveReference("refs/a")
+ c.Assert(err, IsNil)
+
+ _, err = rs.Reference("refs/a")
+ c.Assert(err, Equals, plumbing.ErrReferenceNotFound)
+}
+
+func (s *ReferenceSuite) TestRemoveReferenceBase(c *C) {
+ base := memory.NewStorage()
+ temporal := memory.NewStorage()
+
+ ref := plumbing.NewReferenceFromStrings("refs/a", "bc9968d75e48de59f0870ffb71f5e160bbbdcf52")
+
+ rs := NewReferenceStorage(base, temporal)
+ err := base.SetReference(ref)
+ c.Assert(err, IsNil)
+
+ err = rs.RemoveReference("refs/a")
+ c.Assert(err, IsNil)
+
+ _, err = rs.Reference("refs/a")
+ c.Assert(err, Equals, plumbing.ErrReferenceNotFound)
+}
+
+func (s *ReferenceSuite) TestCheckAndSetReferenceInBase(c *C) {
+ base := memory.NewStorage()
+ temporal := memory.NewStorage()
+ rs := NewReferenceStorage(base, temporal)
+
+ err := base.SetReference(
+ plumbing.NewReferenceFromStrings("foo", "482e0eada5de4039e6f216b45b3c9b683b83bfa"),
+ )
+ c.Assert(err, IsNil)
+
+ err = rs.CheckAndSetReference(
+ plumbing.NewReferenceFromStrings("foo", "bc9968d75e48de59f0870ffb71f5e160bbbdcf52"),
+ plumbing.NewReferenceFromStrings("foo", "482e0eada5de4039e6f216b45b3c9b683b83bfa"),
+ )
+ c.Assert(err, IsNil)
+
+ e, err := rs.Reference(plumbing.ReferenceName("foo"))
+ c.Assert(err, IsNil)
+ c.Assert(e.Hash().String(), Equals, "bc9968d75e48de59f0870ffb71f5e160bbbdcf52")
+}
+
+func (s *ReferenceSuite) TestCommit(c *C) {
+ base := memory.NewStorage()
+ temporal := memory.NewStorage()
+
+ refA := plumbing.NewReferenceFromStrings("refs/a", "bc9968d75e48de59f0870ffb71f5e160bbbdcf52")
+ refB := plumbing.NewReferenceFromStrings("refs/b", "b66c08ba28aa1f81eb06a1127aa3936ff77e5e2c")
+ refC := plumbing.NewReferenceFromStrings("refs/c", "c3f4688a08fd86f1bf8e055724c84b7a40a09733")
+
+ rs := NewReferenceStorage(base, temporal)
+ c.Assert(rs.SetReference(refA), IsNil)
+ c.Assert(rs.SetReference(refB), IsNil)
+ c.Assert(rs.SetReference(refC), IsNil)
+
+ err := rs.Commit()
+ c.Assert(err, IsNil)
+
+ iter, err := base.IterReferences()
+ c.Assert(err, IsNil)
+
+ var count int
+ iter.ForEach(func(ref *plumbing.Reference) error {
+ count++
+ return nil
+ })
+
+ c.Assert(count, Equals, 3)
+}
+
+func (s *ReferenceSuite) TestCommitDelete(c *C) {
+ base := memory.NewStorage()
+ temporal := memory.NewStorage()
+
+ refA := plumbing.NewReferenceFromStrings("refs/a", "bc9968d75e48de59f0870ffb71f5e160bbbdcf52")
+ refB := plumbing.NewReferenceFromStrings("refs/b", "b66c08ba28aa1f81eb06a1127aa3936ff77e5e2c")
+ refC := plumbing.NewReferenceFromStrings("refs/c", "c3f4688a08fd86f1bf8e055724c84b7a40a09733")
+
+ rs := NewReferenceStorage(base, temporal)
+ c.Assert(base.SetReference(refA), IsNil)
+ c.Assert(base.SetReference(refB), IsNil)
+ c.Assert(base.SetReference(refC), IsNil)
+
+ c.Assert(rs.RemoveReference(refA.Name()), IsNil)
+ c.Assert(rs.RemoveReference(refB.Name()), IsNil)
+ c.Assert(rs.RemoveReference(refC.Name()), IsNil)
+ c.Assert(rs.SetReference(refC), IsNil)
+
+ err := rs.Commit()
+ c.Assert(err, IsNil)
+
+ iter, err := base.IterReferences()
+ c.Assert(err, IsNil)
+
+ var count int
+ iter.ForEach(func(ref *plumbing.Reference) error {
+ count++
+ return nil
+ })
+
+ c.Assert(count, Equals, 1)
+
+ ref, err := rs.Reference(refC.Name())
+ c.Assert(err, IsNil)
+ c.Assert(ref.Hash().String(), Equals, "c3f4688a08fd86f1bf8e055724c84b7a40a09733")
+
+}
diff --git a/storage/transactional/shallow.go b/storage/transactional/shallow.go
new file mode 100644
index 0000000..bedc325
--- /dev/null
+++ b/storage/transactional/shallow.go
@@ -0,0 +1,51 @@
+package transactional
+
+import (
+ "gopkg.in/src-d/go-git.v4/plumbing"
+ "gopkg.in/src-d/go-git.v4/plumbing/storer"
+)
+
+// ShallowStorage implements the storer.ShallowStorer for the transactional package.
+type ShallowStorage struct {
+ storer.ShallowStorer
+ temporal storer.ShallowStorer
+}
+
+// NewShallowStorage returns a new ShallowStorage based on a base storer and
+// a temporal storer.
+func NewShallowStorage(base, temporal storer.ShallowStorer) *ShallowStorage {
+ return &ShallowStorage{
+ ShallowStorer: base,
+ temporal: temporal,
+ }
+}
+
+// SetShallow honors the storer.ShallowStorer interface.
+func (s *ShallowStorage) SetShallow(commits []plumbing.Hash) error {
+ return s.temporal.SetShallow(commits)
+}
+
+// Shallow honors the storer.ShallowStorer interface.
+func (s *ShallowStorage) Shallow() ([]plumbing.Hash, error) {
+ shallow, err := s.temporal.Shallow()
+ if err != nil {
+ return nil, err
+ }
+
+ if len(shallow) != 0 {
+ return shallow, nil
+ }
+
+ return s.ShallowStorer.Shallow()
+}
+
+// Commit it copies the shallow information of the temporal storage into the
+// base storage.
+func (s *ShallowStorage) Commit() error {
+ commits, err := s.temporal.Shallow()
+ if err != nil || len(commits) == 0 {
+ return err
+ }
+
+ return s.ShallowStorer.SetShallow(commits)
+}
diff --git a/storage/transactional/shallow_test.go b/storage/transactional/shallow_test.go
new file mode 100644
index 0000000..5141782
--- /dev/null
+++ b/storage/transactional/shallow_test.go
@@ -0,0 +1,62 @@
+package transactional
+
+import (
+ . "gopkg.in/check.v1"
+ "gopkg.in/src-d/go-git.v4/plumbing"
+ "gopkg.in/src-d/go-git.v4/storage/memory"
+)
+
+var _ = Suite(&ShallowSuite{})
+
+type ShallowSuite struct{}
+
+func (s *ShallowSuite) TestShallow(c *C) {
+ base := memory.NewStorage()
+ temporal := memory.NewStorage()
+
+ rs := NewShallowStorage(base, temporal)
+
+ commitA := plumbing.NewHash("bc9968d75e48de59f0870ffb71f5e160bbbdcf52")
+ commitB := plumbing.NewHash("aa9968d75e48de59f0870ffb71f5e160bbbdcf52")
+
+ err := base.SetShallow([]plumbing.Hash{commitA})
+ c.Assert(err, IsNil)
+
+ err = rs.SetShallow([]plumbing.Hash{commitB})
+ c.Assert(err, IsNil)
+
+ commits, err := rs.Shallow()
+ c.Assert(err, IsNil)
+ c.Assert(commits, HasLen, 1)
+ c.Assert(commits[0], Equals, commitB)
+
+ commits, err = base.Shallow()
+ c.Assert(err, IsNil)
+ c.Assert(commits, HasLen, 1)
+ c.Assert(commits[0], Equals, commitA)
+}
+
+func (s *ShallowSuite) TestCommit(c *C) {
+ base := memory.NewStorage()
+ temporal := memory.NewStorage()
+
+ rs := NewShallowStorage(base, temporal)
+
+ commitA := plumbing.NewHash("bc9968d75e48de59f0870ffb71f5e160bbbdcf52")
+ commitB := plumbing.NewHash("aa9968d75e48de59f0870ffb71f5e160bbbdcf52")
+
+ c.Assert(base.SetShallow([]plumbing.Hash{commitA}), IsNil)
+ c.Assert(rs.SetShallow([]plumbing.Hash{commitB}), IsNil)
+
+ c.Assert(rs.Commit(), IsNil)
+
+ commits, err := rs.Shallow()
+ c.Assert(err, IsNil)
+ c.Assert(commits, HasLen, 1)
+ c.Assert(commits[0], Equals, commitB)
+
+ commits, err = base.Shallow()
+ c.Assert(err, IsNil)
+ c.Assert(commits, HasLen, 1)
+ c.Assert(commits[0], Equals, commitB)
+}
diff --git a/storage/transactional/storage.go b/storage/transactional/storage.go
new file mode 100644
index 0000000..fbb3d35
--- /dev/null
+++ b/storage/transactional/storage.go
@@ -0,0 +1,69 @@
+package transactional
+
+import (
+ "gopkg.in/src-d/go-git.v4/storage"
+)
+
+// Storage is a transactional implementation of git.Storer, it demux the write
+// and read operation of two separate storers, allowing to merge content calling
+// Storage.Commit.
+//
+// The API and functionality of this package are considered EXPERIMENTAL and is
+// not considered stable nor production ready.
+type Storage struct {
+ s, temporal storage.Storer
+
+ *ObjectStorage
+ *ReferenceStorage
+ *IndexStorage
+ *ShallowStorage
+ *ConfigStorage
+}
+
+// NewStorage returns a new Storage based on two repositories, base is the base
+// repository where the read operations are read and temportal is were all
+// the write operations are stored.
+func NewStorage(base, temporal storage.Storer) *Storage {
+ return &Storage{
+ s: base,
+ temporal: temporal,
+
+ ObjectStorage: NewObjectStorage(base, temporal),
+ ReferenceStorage: NewReferenceStorage(base, temporal),
+ IndexStorage: NewIndexStorage(base, temporal),
+ ShallowStorage: NewShallowStorage(base, temporal),
+ ConfigStorage: NewConfigStorage(base, temporal),
+ }
+}
+
+// Module it honors the storage.ModuleStorer interface.
+func (s *Storage) Module(name string) (storage.Storer, error) {
+ base, err := s.s.Module(name)
+ if err != nil {
+ return nil, err
+ }
+
+ temporal, err := s.temporal.Module(name)
+ if err != nil {
+ return nil, err
+ }
+
+ return NewStorage(base, temporal), nil
+}
+
+// Commit it copies the content of the temporal storage into the base storage.
+func (s *Storage) Commit() error {
+ for _, c := range []interface{ Commit() error }{
+ s.ObjectStorage,
+ s.ReferenceStorage,
+ s.IndexStorage,
+ s.ShallowStorage,
+ s.ConfigStorage,
+ } {
+ if err := c.Commit(); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
diff --git a/storage/transactional/storage_test.go b/storage/transactional/storage_test.go
new file mode 100644
index 0000000..6aaea0d
--- /dev/null
+++ b/storage/transactional/storage_test.go
@@ -0,0 +1,52 @@
+package transactional
+
+import (
+ "testing"
+
+ . "gopkg.in/check.v1"
+ "gopkg.in/src-d/go-git.v4/plumbing"
+ "gopkg.in/src-d/go-git.v4/storage/memory"
+ "gopkg.in/src-d/go-git.v4/storage/test"
+)
+
+func Test(t *testing.T) { TestingT(t) }
+
+type StorageSuite struct {
+ test.BaseStorageSuite
+}
+
+var _ = Suite(&StorageSuite{})
+
+func (s *StorageSuite) SetUpTest(c *C) {
+ base := memory.NewStorage()
+ temporal := memory.NewStorage()
+
+ s.BaseStorageSuite = test.NewBaseStorageSuite(NewStorage(base, temporal))
+ s.BaseStorageSuite.SetUpTest(c)
+}
+
+func (s *StorageSuite) TestCommit(c *C) {
+ base := memory.NewStorage()
+ temporal := memory.NewStorage()
+ st := NewStorage(base, temporal)
+
+ commit := base.NewEncodedObject()
+ commit.SetType(plumbing.CommitObject)
+
+ _, err := st.SetEncodedObject(commit)
+ c.Assert(err, IsNil)
+
+ ref := plumbing.NewHashReference("refs/a", commit.Hash())
+ c.Assert(st.SetReference(ref), IsNil)
+
+ err = st.Commit()
+ c.Assert(err, IsNil)
+
+ ref, err = base.Reference(ref.Name())
+ c.Assert(err, IsNil)
+ c.Assert(ref.Hash(), Equals, commit.Hash())
+
+ obj, err := base.EncodedObject(plumbing.AnyObject, commit.Hash())
+ c.Assert(err, IsNil)
+ c.Assert(obj.Hash(), Equals, commit.Hash())
+}
diff --git a/worktree.go b/worktree.go
index e45d815..a14fd8d 100644
--- a/worktree.go
+++ b/worktree.go
@@ -25,10 +25,11 @@ import (
)
var (
- ErrWorktreeNotClean = errors.New("worktree is not clean")
- ErrSubmoduleNotFound = errors.New("submodule not found")
- ErrUnstagedChanges = errors.New("worktree contains unstaged changes")
- ErrGitModulesSymlink = errors.New(gitmodulesFile + " is a symlink")
+ ErrWorktreeNotClean = errors.New("worktree is not clean")
+ ErrSubmoduleNotFound = errors.New("submodule not found")
+ ErrUnstagedChanges = errors.New("worktree contains unstaged changes")
+ ErrGitModulesSymlink = errors.New(gitmodulesFile + " is a symlink")
+ ErrNonFastForwardUpdate = errors.New("non-fast-forward update")
)
// Worktree represents a git worktree.
@@ -101,7 +102,7 @@ func (w *Worktree) PullContext(ctx context.Context, o *PullOptions) error {
}
if !ff {
- return fmt.Errorf("non-fast-forward update")
+ return ErrNonFastForwardUpdate
}
}
diff --git a/worktree_test.go b/worktree_test.go
index c714011..26b7da0 100644
--- a/worktree_test.go
+++ b/worktree_test.go
@@ -119,7 +119,7 @@ func (s *WorktreeSuite) TestPullNonFastForward(c *C) {
c.Assert(err, IsNil)
err = w.Pull(&PullOptions{})
- c.Assert(err, ErrorMatches, "non-fast-forward update")
+ c.Assert(err, Equals, ErrNonFastForwardUpdate)
}
func (s *WorktreeSuite) TestPullUpdateReferencesIfNeeded(c *C) {