aboutsummaryrefslogtreecommitdiffstats
path: root/storage
diff options
context:
space:
mode:
authorMáximo Cuadros <mcuadros@gmail.com>2019-02-02 13:08:52 +0100
committerGitHub <noreply@github.com>2019-02-02 13:08:52 +0100
commitd1b5bceb228e528bf085a3d4e2c35218e692da01 (patch)
tree9dadb6130108cce350f546e4e30bc421c2b422a1 /storage
parenta1f6ef44dfed1253ef7f3bc049f66b15f8fc2ab2 (diff)
parent96317743391ac87aeb07d292469e212671628437 (diff)
downloadgo-git-d1b5bceb228e528bf085a3d4e2c35218e692da01.tar.gz
Merge pull request #1006 from mcuadros/transactional-storage
storage: transactional, new storage with transactional capabilities
Diffstat (limited to 'storage')
-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
17 files changed, 1071 insertions, 3 deletions
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())
+}