Skip to content
This repository has been archived by the owner on Sep 11, 2020. It is now read-only.

storage: transactional, new storage with transactional capabilities #1006

Merged
merged 5 commits into from
Feb 2, 2019
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion plumbing/storer/object.go
Original file line number Diff line number Diff line change
Expand Up @@ -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}
}
Expand Down
70 changes: 66 additions & 4 deletions plumbing/storer/reference.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,25 +131,87 @@ 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
}

return err
}
}

return nil
}

// Close releases any resources used by the iterator.
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)
Expand Down
23 changes: 23 additions & 0 deletions plumbing/storer/reference_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"})
}
3 changes: 2 additions & 1 deletion storage/filesystem/dotgit/dotgit.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 {
Expand Down
3 changes: 1 addition & 2 deletions storage/memory/storage.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions storage/storer.go
Original file line number Diff line number Diff line change
@@ -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`)
Expand Down
51 changes: 51 additions & 0 deletions storage/test/storage_suite.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down
44 changes: 44 additions & 0 deletions storage/transactional/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package transactional

import "gopkg.in/src-d/go-git.v4/config"

type ConfigStorage struct {
config.ConfigStorer
temporal config.ConfigStorer

set bool
}

func NewConfigStorage(s, temporal config.ConfigStorer) *ConfigStorage {
return &ConfigStorage{ConfigStorer: s, temporal: temporal}
}

func (c *ConfigStorage) SetConfig(cfg *config.Config) error {
if err := c.temporal.SetConfig(cfg); err != nil {
return err
}

c.set = true
return nil
}

func (c *ConfigStorage) Config() (*config.Config, error) {
if !c.set {
return c.ConfigStorer.Config()
}

return c.temporal.Config()
}

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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might not be transactional depending on the ConfigStorer implementation under the hood. Per example, using filesystem implementation SetConfig can fail on Write, causing a partial write.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, this can be a problem. At least for our use case, since we don't do a commit per operation, but after a group of operations. That is, we want a Commit on the full storage, not just part of it.

}
82 changes: 82 additions & 0 deletions storage/transactional/config_test.go
Original file line number Diff line number Diff line change
@@ -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")
}
Loading