aboutsummaryrefslogtreecommitdiffstats
path: root/storage
diff options
context:
space:
mode:
Diffstat (limited to 'storage')
-rw-r--r--storage/filesystem/dotgit/dotgit.go3
-rw-r--r--storage/filesystem/dotgit/dotgit_setref.go51
-rw-r--r--storage/filesystem/dotgit/dotgit_setref_norwfs.go47
-rw-r--r--storage/filesystem/dotgit/dotgit_test.go25
-rw-r--r--storage/filesystem/object.go65
-rw-r--r--storage/filesystem/object_test.go73
-rw-r--r--storage/filesystem/storage.go6
-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
23 files changed, 1259 insertions, 82 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/filesystem/dotgit/dotgit_setref.go b/storage/filesystem/dotgit/dotgit_setref.go
index d27c1a3..9da2f31 100644
--- a/storage/filesystem/dotgit/dotgit_setref.go
+++ b/storage/filesystem/dotgit/dotgit_setref.go
@@ -1,15 +1,24 @@
-// +build !norwfs
-
package dotgit
import (
+ "fmt"
"os"
"gopkg.in/src-d/go-git.v4/plumbing"
"gopkg.in/src-d/go-git.v4/utils/ioutil"
+
+ "gopkg.in/src-d/go-billy.v4"
)
func (d *DotGit) setRef(fileName, content string, old *plumbing.Reference) (err error) {
+ if billy.CapabilityCheck(d.fs, billy.ReadAndWriteCapability) {
+ return d.setRefRwfs(fileName, content, old)
+ }
+
+ return d.setRefNorwfs(fileName, content, old)
+}
+
+func (d *DotGit) setRefRwfs(fileName, content string, old *plumbing.Reference) (err error) {
// If we are not checking an old ref, just truncate the file.
mode := os.O_RDWR | os.O_CREATE
if old == nil {
@@ -41,3 +50,41 @@ func (d *DotGit) setRef(fileName, content string, old *plumbing.Reference) (err
_, err = f.Write([]byte(content))
return err
}
+
+// There are some filesystems that don't support opening files in RDWD mode.
+// In these filesystems the standard SetRef function can not be used as it
+// reads the reference file to check that it's not modified before updating it.
+//
+// This version of the function writes the reference without extra checks
+// making it compatible with these simple filesystems. This is usually not
+// a problem as they should be accessed by only one process at a time.
+func (d *DotGit) setRefNorwfs(fileName, content string, old *plumbing.Reference) error {
+ _, err := d.fs.Stat(fileName)
+ if err == nil && old != nil {
+ fRead, err := d.fs.Open(fileName)
+ if err != nil {
+ return err
+ }
+
+ ref, err := d.readReferenceFrom(fRead, old.Name().String())
+ fRead.Close()
+
+ if err != nil {
+ return err
+ }
+
+ if ref.Hash() != old.Hash() {
+ return fmt.Errorf("reference has changed concurrently")
+ }
+ }
+
+ f, err := d.fs.Create(fileName)
+ if err != nil {
+ return err
+ }
+
+ defer f.Close()
+
+ _, err = f.Write([]byte(content))
+ return err
+}
diff --git a/storage/filesystem/dotgit/dotgit_setref_norwfs.go b/storage/filesystem/dotgit/dotgit_setref_norwfs.go
deleted file mode 100644
index 5695bd3..0000000
--- a/storage/filesystem/dotgit/dotgit_setref_norwfs.go
+++ /dev/null
@@ -1,47 +0,0 @@
-// +build norwfs
-
-package dotgit
-
-import (
- "fmt"
-
- "gopkg.in/src-d/go-git.v4/plumbing"
-)
-
-// There are some filesystems that don't support opening files in RDWD mode.
-// In these filesystems the standard SetRef function can not be used as i
-// reads the reference file to check that it's not modified before updating it.
-//
-// This version of the function writes the reference without extra checks
-// making it compatible with these simple filesystems. This is usually not
-// a problem as they should be accessed by only one process at a time.
-func (d *DotGit) setRef(fileName, content string, old *plumbing.Reference) error {
- _, err := d.fs.Stat(fileName)
- if err == nil && old != nil {
- fRead, err := d.fs.Open(fileName)
- if err != nil {
- return err
- }
-
- ref, err := d.readReferenceFrom(fRead, old.Name().String())
- fRead.Close()
-
- if err != nil {
- return err
- }
-
- if ref.Hash() != old.Hash() {
- return fmt.Errorf("reference has changed concurrently")
- }
- }
-
- f, err := d.fs.Create(fileName)
- if err != nil {
- return err
- }
-
- defer f.Close()
-
- _, err = f.Write([]byte(content))
- return err
-}
diff --git a/storage/filesystem/dotgit/dotgit_test.go b/storage/filesystem/dotgit/dotgit_test.go
index 308c6b7..73b0291 100644
--- a/storage/filesystem/dotgit/dotgit_test.go
+++ b/storage/filesystem/dotgit/dotgit_test.go
@@ -57,11 +57,26 @@ func (s *SuiteDotGit) TestSetRefs(c *C) {
fs := osfs.New(tmp)
dir := New(fs)
+ testSetRefs(c, dir)
+}
+
+func (s *SuiteDotGit) TestSetRefsNorwfs(c *C) {
+ tmp, err := ioutil.TempDir("", "dot-git")
+ c.Assert(err, IsNil)
+ defer os.RemoveAll(tmp)
+
+ fs := osfs.New(tmp)
+ dir := New(&norwfs{fs})
+
+ testSetRefs(c, dir)
+}
+
+func testSetRefs(c *C, dir *DotGit) {
firstFoo := plumbing.NewReferenceFromStrings(
"refs/heads/foo",
"e8d3ffab552895c19b9fcf7aa264d277cde33881",
)
- err = dir.SetRef(firstFoo, nil)
+ err := dir.SetRef(firstFoo, nil)
c.Assert(err, IsNil)
@@ -795,3 +810,11 @@ func (s *SuiteDotGit) TestAlternates(c *C) {
}
c.Assert(dotgits[1].fs.Root(), Equals, expectedPath)
}
+
+type norwfs struct {
+ billy.Filesystem
+}
+
+func (f *norwfs) Capabilities() billy.Capability {
+ return billy.Capabilities(f.Filesystem) &^ billy.ReadAndWriteCapability
+}
diff --git a/storage/filesystem/object.go b/storage/filesystem/object.go
index 6cd2d4c..3eb62a2 100644
--- a/storage/filesystem/object.go
+++ b/storage/filesystem/object.go
@@ -20,24 +20,25 @@ import (
type ObjectStorage struct {
options Options
- // deltaBaseCache is an object cache uses to cache delta's bases when
- deltaBaseCache cache.Object
+ // objectCache is an object cache uses to cache delta's bases and also recently
+ // loaded loose objects
+ objectCache cache.Object
dir *dotgit.DotGit
index map[plumbing.Hash]idxfile.Index
}
// NewObjectStorage creates a new ObjectStorage with the given .git directory and cache.
-func NewObjectStorage(dir *dotgit.DotGit, cache cache.Object) *ObjectStorage {
- return NewObjectStorageWithOptions(dir, cache, Options{})
+func NewObjectStorage(dir *dotgit.DotGit, objectCache cache.Object) *ObjectStorage {
+ return NewObjectStorageWithOptions(dir, objectCache, Options{})
}
// NewObjectStorageWithOptions creates a new ObjectStorage with the given .git directory, cache and extra options
-func NewObjectStorageWithOptions(dir *dotgit.DotGit, cache cache.Object, ops Options) *ObjectStorage {
+func NewObjectStorageWithOptions(dir *dotgit.DotGit, objectCache cache.Object, ops Options) *ObjectStorage {
return &ObjectStorage{
- options: ops,
- deltaBaseCache: cache,
- dir: dir,
+ options: ops,
+ objectCache: objectCache,
+ dir: dir,
}
}
@@ -61,6 +62,11 @@ func (s *ObjectStorage) requireIndex() error {
return nil
}
+// Reindex indexes again all packfiles. Useful if git changed packfiles externally
+func (s *ObjectStorage) Reindex() {
+ s.index = nil
+}
+
func (s *ObjectStorage) loadIdxFile(h plumbing.Hash) (err error) {
f, err := s.dir.ObjectPackIdx(h)
if err != nil {
@@ -201,7 +207,7 @@ func (s *ObjectStorage) encodedObjectSizeFromPackfile(h plumbing.Hash) (
idx := s.index[pack]
hash, err := idx.FindHash(offset)
if err == nil {
- obj, ok := s.deltaBaseCache.Get(hash)
+ obj, ok := s.objectCache.Get(hash)
if ok {
return obj.Size(), nil
}
@@ -210,8 +216,8 @@ func (s *ObjectStorage) encodedObjectSizeFromPackfile(h plumbing.Hash) (
}
var p *packfile.Packfile
- if s.deltaBaseCache != nil {
- p = packfile.NewPackfileWithCache(idx, s.dir.Fs(), f, s.deltaBaseCache)
+ if s.objectCache != nil {
+ p = packfile.NewPackfileWithCache(idx, s.dir.Fs(), f, s.objectCache)
} else {
p = packfile.NewPackfile(idx, s.dir.Fs(), f)
}
@@ -236,9 +242,19 @@ func (s *ObjectStorage) EncodedObjectSize(h plumbing.Hash) (
// EncodedObject returns the object with the given hash, by searching for it in
// the packfile and the git object directories.
func (s *ObjectStorage) EncodedObject(t plumbing.ObjectType, h plumbing.Hash) (plumbing.EncodedObject, error) {
- obj, err := s.getFromUnpacked(h)
- if err == plumbing.ErrObjectNotFound {
+ var obj plumbing.EncodedObject
+ var err error
+
+ if s.index != nil {
obj, err = s.getFromPackfile(h, false)
+ if err == plumbing.ErrObjectNotFound {
+ obj, err = s.getFromUnpacked(h)
+ }
+ } else {
+ obj, err = s.getFromUnpacked(h)
+ if err == plumbing.ErrObjectNotFound {
+ obj, err = s.getFromPackfile(h, false)
+ }
}
// If the error is still object not found, check if it's a shared object
@@ -249,7 +265,7 @@ func (s *ObjectStorage) EncodedObject(t plumbing.ObjectType, h plumbing.Hash) (p
// Create a new object storage with the DotGit(s) and check for the
// required hash object. Skip when not found.
for _, dg := range dotgits {
- o := NewObjectStorage(dg, s.deltaBaseCache)
+ o := NewObjectStorage(dg, s.objectCache)
enobj, enerr := o.EncodedObject(t, h)
if enerr != nil {
continue
@@ -299,9 +315,12 @@ func (s *ObjectStorage) getFromUnpacked(h plumbing.Hash) (obj plumbing.EncodedOb
return nil, err
}
-
defer ioutil.CheckClose(f, &err)
+ if cacheObj, found := s.objectCache.Get(h); found {
+ return cacheObj, nil
+ }
+
obj = s.NewEncodedObject()
r, err := objfile.NewReader(f)
if err != nil {
@@ -322,6 +341,8 @@ func (s *ObjectStorage) getFromUnpacked(h plumbing.Hash) (obj plumbing.EncodedOb
return nil, err
}
+ s.objectCache.Put(obj)
+
_, err = io.Copy(w, r)
return obj, err
}
@@ -364,7 +385,7 @@ func (s *ObjectStorage) decodeObjectAt(
) (plumbing.EncodedObject, error) {
hash, err := idx.FindHash(offset)
if err == nil {
- obj, ok := s.deltaBaseCache.Get(hash)
+ obj, ok := s.objectCache.Get(hash)
if ok {
return obj, nil
}
@@ -375,8 +396,8 @@ func (s *ObjectStorage) decodeObjectAt(
}
var p *packfile.Packfile
- if s.deltaBaseCache != nil {
- p = packfile.NewPackfileWithCache(idx, s.dir.Fs(), f, s.deltaBaseCache)
+ if s.objectCache != nil {
+ p = packfile.NewPackfileWithCache(idx, s.dir.Fs(), f, s.objectCache)
} else {
p = packfile.NewPackfile(idx, s.dir.Fs(), f)
}
@@ -395,11 +416,7 @@ func (s *ObjectStorage) decodeDeltaObjectAt(
}
p := packfile.NewScanner(f)
- if _, err := p.SeekFromStart(offset); err != nil {
- return nil, err
- }
-
- header, err := p.NextObjectHeader()
+ header, err := p.SeekObjectHeader(offset)
if err != nil {
return nil, err
}
@@ -490,7 +507,7 @@ func (s *ObjectStorage) buildPackfileIters(
}
return newPackfileIter(
s.dir.Fs(), pack, t, seen, s.index[h],
- s.deltaBaseCache, s.options.KeepDescriptors,
+ s.objectCache, s.options.KeepDescriptors,
)
},
}, nil
diff --git a/storage/filesystem/object_test.go b/storage/filesystem/object_test.go
index 4e6bbfb..5cfb227 100644
--- a/storage/filesystem/object_test.go
+++ b/storage/filesystem/object_test.go
@@ -1,8 +1,11 @@
package filesystem
import (
+ "fmt"
+ "io"
"io/ioutil"
"os"
+ "path/filepath"
"testing"
"gopkg.in/src-d/go-git.v4/plumbing"
@@ -204,6 +207,59 @@ func (s *FsSuite) TestPackfileIter(c *C) {
})
}
+func copyFile(c *C, dstDir, dstFilename string, srcFile *os.File) {
+ _, err := srcFile.Seek(0, 0)
+ c.Assert(err, IsNil)
+
+ err = os.MkdirAll(dstDir, 0750|os.ModeDir)
+ c.Assert(err, IsNil)
+
+ dst, err := os.OpenFile(filepath.Join(dstDir, dstFilename), os.O_CREATE|os.O_WRONLY, 0666)
+ c.Assert(err, IsNil)
+ defer dst.Close()
+
+ _, err = io.Copy(dst, srcFile)
+ c.Assert(err, IsNil)
+}
+
+// TestPackfileReindex tests that externally-added packfiles are considered by go-git
+// after calling the Reindex method
+func (s *FsSuite) TestPackfileReindex(c *C) {
+ // obtain a standalone packfile that is not part of any other repository
+ // in the fixtures:
+ packFixture := fixtures.ByTag("packfile").ByTag("standalone").One()
+ packFile := packFixture.Packfile()
+ idxFile := packFixture.Idx()
+ packFilename := packFixture.PackfileHash.String()
+ testObjectHash := plumbing.NewHash("a771b1e94141480861332fd0e4684d33071306c6") // this is an object we know exists in the standalone packfile
+ fixtures.ByTag(".git").Test(c, func(f *fixtures.Fixture) {
+ fs := f.DotGit()
+ storer := NewStorage(fs, cache.NewObjectLRUDefault())
+
+ // check that our test object is NOT found
+ _, err := storer.EncodedObject(plumbing.CommitObject, testObjectHash)
+ c.Assert(err, Equals, plumbing.ErrObjectNotFound)
+
+ // add the external packfile+idx to the packs folder
+ // this simulates a git bundle unbundle command, or a repack, for example.
+ copyFile(c, filepath.Join(storer.Filesystem().Root(), "objects", "pack"),
+ fmt.Sprintf("pack-%s.pack", packFilename), packFile)
+ copyFile(c, filepath.Join(storer.Filesystem().Root(), "objects", "pack"),
+ fmt.Sprintf("pack-%s.idx", packFilename), idxFile)
+
+ // check that we cannot still retrieve the test object
+ _, err = storer.EncodedObject(plumbing.CommitObject, testObjectHash)
+ c.Assert(err, Equals, plumbing.ErrObjectNotFound)
+
+ storer.Reindex() // actually reindex
+
+ // Now check that the test object can be retrieved
+ _, err = storer.EncodedObject(plumbing.CommitObject, testObjectHash)
+ c.Assert(err, IsNil)
+
+ })
+}
+
func (s *FsSuite) TestPackfileIterKeepDescriptors(c *C) {
fixtures.ByTag(".git").Test(c, func(f *fixtures.Fixture) {
fs := f.DotGit()
@@ -241,6 +297,23 @@ func (s *FsSuite) TestPackfileIterKeepDescriptors(c *C) {
})
}
+func (s *FsSuite) TestGetFromObjectFileSharedCache(c *C) {
+ f1 := fixtures.ByTag("worktree").One().DotGit()
+ f2 := fixtures.ByTag("worktree").ByTag("submodule").One().DotGit()
+
+ ch := cache.NewObjectLRUDefault()
+ o1 := NewObjectStorage(dotgit.New(f1), ch)
+ o2 := NewObjectStorage(dotgit.New(f2), ch)
+
+ expected := plumbing.NewHash("af2d6a6954d532f8ffb47615169c8fdf9d383a1a")
+ obj, err := o1.EncodedObject(plumbing.CommitObject, expected)
+ c.Assert(err, IsNil)
+ c.Assert(obj.Hash(), Equals, expected)
+
+ obj, err = o2.EncodedObject(plumbing.CommitObject, expected)
+ c.Assert(err, Equals, plumbing.ErrObjectNotFound)
+}
+
func BenchmarkPackfileIter(b *testing.B) {
if err := fixtures.Init(); err != nil {
b.Fatal(err)
diff --git a/storage/filesystem/storage.go b/storage/filesystem/storage.go
index 14a772a..370f7bd 100644
--- a/storage/filesystem/storage.go
+++ b/storage/filesystem/storage.go
@@ -51,11 +51,7 @@ func NewStorageWithOptions(fs billy.Filesystem, cache cache.Object, ops Options)
fs: fs,
dir: dir,
- ObjectStorage: ObjectStorage{
- options: ops,
- deltaBaseCache: cache,
- dir: dir,
- },
+ ObjectStorage: *NewObjectStorageWithOptions(dir, cache, ops),
ReferenceStorage: ReferenceStorage{dir: dir},
IndexStorage: IndexStorage{dir: dir},
ShallowStorage: ShallowStorage{dir: dir},
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())
+}