aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMichael Muré <batolettre@gmail.com>2020-06-23 18:02:54 +0200
committerMichael Muré <batolettre@gmail.com>2020-06-26 19:14:22 +0200
commit88ad7e606f1cbf9e47b968a208e3510f7f9a81c5 (patch)
tree00e847500b34c6f9a721c71474993d1c08ae8fb1
parent2dd0dbb1344ae9293aae05346f977b5d5907934b (diff)
downloadgit-bug-88ad7e606f1cbf9e47b968a208e3510f7f9a81c5.tar.gz
repository: remove tie to Bug, improved and reusable testing
- allow the creation of arbitrary Lamport clocks, freeing the way to new entities and removing Bug specific (upper layer) code. - generalize the memory-only and persisted Lamport clocks behind a common interface - rework the tests to provide reusable testing code for a Repo, a Clock, a Config, opening a path to add a new Repo implementation more easily - test previously untested components with those new tests Note: one problem found during this endeavor is that `identity.Version` also need to store one time + Lamport time for each other Entity (Bug, config, PR ...). This could possibly done without breaking change but it would be much easier to wait for https://github.com/MichaelMure/git-bug-migration to happen.
-rw-r--r--bridge/github/export_test.go4
-rw-r--r--bridge/github/import_test.go2
-rw-r--r--bridge/gitlab/export_test.go4
-rw-r--r--bridge/gitlab/import_test.go2
-rw-r--r--bug/bug.go27
-rw-r--r--bug/bug_actions_test.go16
-rw-r--r--bug/clocks.go42
-rw-r--r--bug/operation_test.go2
-rw-r--r--cache/repo_cache.go6
-rw-r--r--cache/repo_cache_test.go6
-rw-r--r--commands/root.go2
-rw-r--r--commands/select/select_test.go2
-rw-r--r--graphql/graphql_test.go2
-rw-r--r--identity/identity.go9
-rw-r--r--identity/identity_actions_test.go4
-rw-r--r--identity/version.go3
-rw-r--r--misc/random_bugs/main.go4
-rw-r--r--repository/config_mem.go8
-rw-r--r--repository/config_mem_test.go7
-rw-r--r--repository/config_testing.go63
-rw-r--r--repository/git.go202
-rw-r--r--repository/git_config.go (renamed from repository/config_git.go)0
-rw-r--r--repository/git_test.go61
-rw-r--r--repository/git_testing.go44
-rw-r--r--repository/mock_repo.go45
-rw-r--r--repository/mock_repo_test.go10
-rw-r--r--repository/repo.go54
-rw-r--r--repository/repo_testing.go70
-rw-r--r--repository/tree_entry_test.go8
-rw-r--r--termui/show_bug.go2
-rw-r--r--tests/read_bugs_test.go4
-rw-r--r--util/lamport/clock.go15
-rw-r--r--util/lamport/clock_testing.go28
-rw-r--r--util/lamport/lamport_test.go66
-rw-r--r--util/lamport/mem_clock.go (renamed from util/lamport/lamport.go)39
-rw-r--r--util/lamport/mem_clock_test.go8
-rw-r--r--util/lamport/persisted_clock.go100
-rw-r--r--util/lamport/persisted_clock_test.go19
-rw-r--r--util/lamport/persisted_lamport.go84
39 files changed, 563 insertions, 511 deletions
diff --git a/bridge/github/export_test.go b/bridge/github/export_test.go
index 0748ecbf..b160ac7f 100644
--- a/bridge/github/export_test.go
+++ b/bridge/github/export_test.go
@@ -138,7 +138,7 @@ func TestPushPull(t *testing.T) {
// create repo backend
repo := repository.CreateTestRepo(false)
- defer repository.CleanupTestRepos(t, repo)
+ defer repository.CleanupTestRepos(repo)
backend, err := cache.NewRepoCache(repo)
require.NoError(t, err)
@@ -210,7 +210,7 @@ func TestPushPull(t *testing.T) {
fmt.Printf("test repository exported in %f seconds\n", time.Since(start).Seconds())
repoTwo := repository.CreateTestRepo(false)
- defer repository.CleanupTestRepos(t, repoTwo)
+ defer repository.CleanupTestRepos(repoTwo)
// create a second backend
backendTwo, err := cache.NewRepoCache(repoTwo)
diff --git a/bridge/github/import_test.go b/bridge/github/import_test.go
index 107d74c5..f33b30c2 100644
--- a/bridge/github/import_test.go
+++ b/bridge/github/import_test.go
@@ -128,7 +128,7 @@ func Test_Importer(t *testing.T) {
}
repo := repository.CreateTestRepo(false)
- defer repository.CleanupTestRepos(t, repo)
+ defer repository.CleanupTestRepos(repo)
backend, err := cache.NewRepoCache(repo)
require.NoError(t, err)
diff --git a/bridge/gitlab/export_test.go b/bridge/gitlab/export_test.go
index d8966163..6c151d61 100644
--- a/bridge/gitlab/export_test.go
+++ b/bridge/gitlab/export_test.go
@@ -143,7 +143,7 @@ func TestPushPull(t *testing.T) {
// create repo backend
repo := repository.CreateTestRepo(false)
- defer repository.CleanupTestRepos(t, repo)
+ defer repository.CleanupTestRepos(repo)
backend, err := cache.NewRepoCache(repo)
require.NoError(t, err)
@@ -216,7 +216,7 @@ func TestPushPull(t *testing.T) {
fmt.Printf("test repository exported in %f seconds\n", time.Since(start).Seconds())
repoTwo := repository.CreateTestRepo(false)
- defer repository.CleanupTestRepos(t, repoTwo)
+ defer repository.CleanupTestRepos(repoTwo)
// create a second backend
backendTwo, err := cache.NewRepoCache(repoTwo)
diff --git a/bridge/gitlab/import_test.go b/bridge/gitlab/import_test.go
index 42a37cda..9a936ae4 100644
--- a/bridge/gitlab/import_test.go
+++ b/bridge/gitlab/import_test.go
@@ -77,7 +77,7 @@ func TestImport(t *testing.T) {
}
repo := repository.CreateTestRepo(false)
- defer repository.CleanupTestRepos(t, repo)
+ defer repository.CleanupTestRepos(repo)
backend, err := cache.NewRepoCache(repo)
require.NoError(t, err)
diff --git a/bug/bug.go b/bug/bug.go
index e3378770..66609e6c 100644
--- a/bug/bug.go
+++ b/bug/bug.go
@@ -27,6 +27,9 @@ const createClockEntryPattern = "create-clock-%d"
const editClockEntryPrefix = "edit-clock-"
const editClockEntryPattern = "edit-clock-%d"
+const creationClockName = "bug-create"
+const editClockName = "bug-edit"
+
var ErrBugNotExist = errors.New("bug doesn't exist")
func NewErrMultipleMatchBug(matching []entity.Id) *entity.ErrMultipleMatch {
@@ -197,10 +200,18 @@ func readBug(repo repository.ClockedRepo, ref string) (*Bug, error) {
}
// Update the clocks
- if err := repo.WitnessCreate(bug.createTime); err != nil {
+ createClock, err := repo.GetOrCreateClock(creationClockName)
+ if err != nil {
+ return nil, err
+ }
+ if err := createClock.Witness(bug.createTime); err != nil {
return nil, errors.Wrap(err, "failed to update create lamport clock")
}
- if err := repo.WitnessEdit(bug.editTime); err != nil {
+ editClock, err := repo.GetOrCreateClock(editClockName)
+ if err != nil {
+ return nil, err
+ }
+ if err := editClock.Witness(bug.editTime); err != nil {
return nil, errors.Wrap(err, "failed to update edit lamport clock")
}
@@ -412,7 +423,11 @@ func (bug *Bug) Commit(repo repository.ClockedRepo) error {
return err
}
- bug.editTime, err = repo.EditTimeIncrement()
+ editClock, err := repo.GetOrCreateClock(editClockName)
+ if err != nil {
+ return err
+ }
+ bug.editTime, err = editClock.Increment()
if err != nil {
return err
}
@@ -423,7 +438,11 @@ func (bug *Bug) Commit(repo repository.ClockedRepo) error {
Name: fmt.Sprintf(editClockEntryPattern, bug.editTime),
})
if bug.lastCommit == "" {
- bug.createTime, err = repo.CreateTimeIncrement()
+ createClock, err := repo.GetOrCreateClock(creationClockName)
+ if err != nil {
+ return err
+ }
+ bug.createTime, err = createClock.Increment()
if err != nil {
return err
}
diff --git a/bug/bug_actions_test.go b/bug/bug_actions_test.go
index 38dddce2..c867ac80 100644
--- a/bug/bug_actions_test.go
+++ b/bug/bug_actions_test.go
@@ -12,8 +12,8 @@ import (
)
func TestPushPull(t *testing.T) {
- repoA, repoB, remote := repository.SetupReposAndRemote(t)
- defer repository.CleanupTestRepos(t, repoA, repoB, remote)
+ repoA, repoB, remote := repository.SetupReposAndRemote()
+ defer repository.CleanupTestRepos(repoA, repoB, remote)
reneA := identity.NewIdentity("René Descartes", "rene@descartes.fr")
@@ -87,8 +87,8 @@ func BenchmarkRebaseTheirs(b *testing.B) {
}
func _RebaseTheirs(t testing.TB) {
- repoA, repoB, remote := repository.SetupReposAndRemote(t)
- defer repository.CleanupTestRepos(t, repoA, repoB, remote)
+ repoA, repoB, remote := repository.SetupReposAndRemote()
+ defer repository.CleanupTestRepos(repoA, repoB, remote)
reneA := identity.NewIdentity("René Descartes", "rene@descartes.fr")
@@ -165,8 +165,8 @@ func BenchmarkRebaseOurs(b *testing.B) {
}
func _RebaseOurs(t testing.TB) {
- repoA, repoB, remote := repository.SetupReposAndRemote(t)
- defer repository.CleanupTestRepos(t, repoA, repoB, remote)
+ repoA, repoB, remote := repository.SetupReposAndRemote()
+ defer repository.CleanupTestRepos(repoA, repoB, remote)
reneA := identity.NewIdentity("René Descartes", "rene@descartes.fr")
@@ -254,8 +254,8 @@ func BenchmarkRebaseConflict(b *testing.B) {
}
func _RebaseConflict(t testing.TB) {
- repoA, repoB, remote := repository.SetupReposAndRemote(t)
- defer repository.CleanupTestRepos(t, repoA, repoB, remote)
+ repoA, repoB, remote := repository.SetupReposAndRemote()
+ defer repository.CleanupTestRepos(repoA, repoB, remote)
reneA := identity.NewIdentity("René Descartes", "rene@descartes.fr")
diff --git a/bug/clocks.go b/bug/clocks.go
index 52d23544..ba93e0dc 100644
--- a/bug/clocks.go
+++ b/bug/clocks.go
@@ -4,24 +4,34 @@ import (
"github.com/MichaelMure/git-bug/repository"
)
-// Witnesser will read all the available Bug to recreate the different logical
-// clocks
-func Witnesser(repo repository.ClockedRepo) error {
- for b := range ReadAllLocalBugs(repo) {
- if b.Err != nil {
- return b.Err
- }
+// ClockLoader is the repository.ClockLoader for the Bug entity
+var ClockLoader = repository.ClockLoader{
+ Clocks: []string{creationClockName, editClockName},
+ Witnesser: func(repo repository.ClockedRepo) error {
+ for b := range ReadAllLocalBugs(repo) {
+ if b.Err != nil {
+ return b.Err
+ }
- err := repo.WitnessCreate(b.Bug.createTime)
- if err != nil {
- return err
- }
+ createClock, err := repo.GetOrCreateClock(creationClockName)
+ if err != nil {
+ return err
+ }
+ err = createClock.Witness(b.Bug.createTime)
+ if err != nil {
+ return err
+ }
- err = repo.WitnessEdit(b.Bug.editTime)
- if err != nil {
- return err
+ editClock, err := repo.GetOrCreateClock(editClockName)
+ if err != nil {
+ return err
+ }
+ err = editClock.Witness(b.Bug.editTime)
+ if err != nil {
+ return err
+ }
}
- }
- return nil
+ return nil
+ },
}
diff --git a/bug/operation_test.go b/bug/operation_test.go
index 69c66dc8..cfd15e13 100644
--- a/bug/operation_test.go
+++ b/bug/operation_test.go
@@ -81,7 +81,7 @@ func TestMetadata(t *testing.T) {
func TestID(t *testing.T) {
repo := repository.CreateTestRepo(false)
- defer repository.CleanupTestRepos(t, repo)
+ defer repository.CleanupTestRepos(repo)
repos := []repository.ClockedRepo{
repository.NewMockRepoForTest(),
diff --git a/cache/repo_cache.go b/cache/repo_cache.go
index e70904df..4a6b007f 100644
--- a/cache/repo_cache.go
+++ b/cache/repo_cache.go
@@ -8,6 +8,7 @@ import (
"io/ioutil"
"os"
"path"
+ "path/filepath"
"sort"
"strconv"
"sync"
@@ -149,6 +150,11 @@ func (c *RepoCache) lock() error {
return err
}
+ err = os.MkdirAll(filepath.Dir(lockPath), 0777)
+ if err != nil {
+ return err
+ }
+
f, err := os.Create(lockPath)
if err != nil {
return err
diff --git a/cache/repo_cache_test.go b/cache/repo_cache_test.go
index 51393dfd..0a333c8f 100644
--- a/cache/repo_cache_test.go
+++ b/cache/repo_cache_test.go
@@ -11,7 +11,7 @@ import (
func TestCache(t *testing.T) {
repo := repository.CreateTestRepo(false)
- defer repository.CleanupTestRepos(t, repo)
+ defer repository.CleanupTestRepos(repo)
cache, err := NewRepoCache(repo)
require.NoError(t, err)
@@ -104,8 +104,8 @@ func TestCache(t *testing.T) {
}
func TestPushPull(t *testing.T) {
- repoA, repoB, remote := repository.SetupReposAndRemote(t)
- defer repository.CleanupTestRepos(t, repoA, repoB, remote)
+ repoA, repoB, remote := repository.SetupReposAndRemote()
+ defer repository.CleanupTestRepos(repoA, repoB, remote)
cacheA, err := NewRepoCache(repoA)
require.NoError(t, err)
diff --git a/commands/root.go b/commands/root.go
index 92d32a5a..2ea95d4b 100644
--- a/commands/root.go
+++ b/commands/root.go
@@ -63,7 +63,7 @@ func loadRepo(cmd *cobra.Command, args []string) error {
return fmt.Errorf("unable to get the current working directory: %q", err)
}
- repo, err = repository.NewGitRepo(cwd, bug.Witnesser)
+ repo, err = repository.NewGitRepo(cwd, []repository.ClockLoader{bug.ClockLoader})
if err == repository.ErrNotARepo {
return fmt.Errorf("%s must be run from within a git repo", rootCommandName)
}
diff --git a/commands/select/select_test.go b/commands/select/select_test.go
index 989d6b3c..ac0d0903 100644
--- a/commands/select/select_test.go
+++ b/commands/select/select_test.go
@@ -12,7 +12,7 @@ import (
func TestSelect(t *testing.T) {
repo := repository.CreateTestRepo(false)
- defer repository.CleanupTestRepos(t, repo)
+ defer repository.CleanupTestRepos(repo)
repoCache, err := cache.NewRepoCache(repo)
require.NoError(t, err)
diff --git a/graphql/graphql_test.go b/graphql/graphql_test.go
index 19770150..0ff2c3fb 100644
--- a/graphql/graphql_test.go
+++ b/graphql/graphql_test.go
@@ -12,7 +12,7 @@ import (
func TestQueries(t *testing.T) {
repo := repository.CreateTestRepo(false)
- defer repository.CleanupTestRepos(t, repo)
+ defer repository.CleanupTestRepos(repo)
random_bugs.FillRepoWithSeed(repo, 10, 42)
diff --git a/identity/identity.go b/identity/identity.go
index 3da8c5a3..c1bca4d6 100644
--- a/identity/identity.go
+++ b/identity/identity.go
@@ -332,7 +332,14 @@ func (i *Identity) Commit(repo repository.ClockedRepo) error {
}
// get the times where new versions starts to be valid
- v.time = repo.EditTime()
+ // TODO: instead of this hardcoded clock for bugs only, this need to be
+ // a vector of edit clock, one for each entity (bug, PR, config ..)
+ bugEditClock, err := repo.GetOrCreateClock("bug-edit")
+ if err != nil {
+ return err
+ }
+
+ v.time = bugEditClock.Time()
v.unixTime = time.Now().Unix()
blobHash, err := v.Write(repo)
diff --git a/identity/identity_actions_test.go b/identity/identity_actions_test.go
index 713b3246..48652438 100644
--- a/identity/identity_actions_test.go
+++ b/identity/identity_actions_test.go
@@ -9,8 +9,8 @@ import (
)
func TestPushPull(t *testing.T) {
- repoA, repoB, remote := repository.SetupReposAndRemote(t)
- defer repository.CleanupTestRepos(t, repoA, repoB, remote)
+ repoA, repoB, remote := repository.SetupReposAndRemote()
+ defer repository.CleanupTestRepos(repoA, repoB, remote)
identity1 := NewIdentity("name1", "email1")
err := identity1.Commit(repoA)
diff --git a/identity/version.go b/identity/version.go
index 757b029f..2359bd11 100644
--- a/identity/version.go
+++ b/identity/version.go
@@ -20,6 +20,9 @@ type Version struct {
// The lamport time at which this version become effective
// The reference time is the bug edition lamport clock
// It must be the first field in this struct due to https://github.com/golang/go/issues/599
+ //
+ // TODO: BREAKING CHANGE - this need to actually be one edition lamport time **per entity**
+ // This is not a problem right now but will be when more entities are added (pull-request, config ...)
time lamport.Time
unixTime int64
diff --git a/misc/random_bugs/main.go b/misc/random_bugs/main.go
index 274b3dac..f67cd432 100644
--- a/misc/random_bugs/main.go
+++ b/misc/random_bugs/main.go
@@ -17,9 +17,7 @@ func main() {
panic(err)
}
- repo, err := repository.NewGitRepo(dir, func(repo repository.ClockedRepo) error {
- return nil
- })
+ repo, err := repository.NewGitRepo(dir)
if err != nil {
panic(err)
}
diff --git a/repository/config_mem.go b/repository/config_mem.go
index 5ce577ac..9725e8d5 100644
--- a/repository/config_mem.go
+++ b/repository/config_mem.go
@@ -1,6 +1,7 @@
package repository
import (
+ "fmt"
"strconv"
"strings"
"time"
@@ -77,10 +78,17 @@ func (mc *MemConfig) ReadTimestamp(key string) (time.Time, error) {
// RmConfigs remove all key/value pair matching the key prefix
func (mc *MemConfig) RemoveAll(keyPrefix string) error {
+ found := false
for key := range mc.config {
if strings.HasPrefix(key, keyPrefix) {
delete(mc.config, key)
+ found = true
}
}
+
+ if !found {
+ return fmt.Errorf("section not found")
+ }
+
return nil
}
diff --git a/repository/config_mem_test.go b/repository/config_mem_test.go
new file mode 100644
index 00000000..d9c33851
--- /dev/null
+++ b/repository/config_mem_test.go
@@ -0,0 +1,7 @@
+package repository
+
+import "testing"
+
+func TestNewMemConfig(t *testing.T) {
+ testConfig(t, NewMemConfig())
+}
diff --git a/repository/config_testing.go b/repository/config_testing.go
new file mode 100644
index 00000000..25639d59
--- /dev/null
+++ b/repository/config_testing.go
@@ -0,0 +1,63 @@
+package repository
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func testConfig(t *testing.T, config Config) {
+ err := config.StoreString("section.key", "value")
+ assert.NoError(t, err)
+
+ val, err := config.ReadString("section.key")
+ assert.NoError(t, err)
+ assert.Equal(t, "value", val)
+
+ err = config.StoreString("section.true", "true")
+ assert.NoError(t, err)
+
+ val2, err := config.ReadBool("section.true")
+ assert.NoError(t, err)
+ assert.Equal(t, true, val2)
+
+ configs, err := config.ReadAll("section")
+ assert.NoError(t, err)
+ assert.Equal(t, map[string]string{
+ "section.key": "value",
+ "section.true": "true",
+ }, configs)
+
+ err = config.RemoveAll("section.true")
+ assert.NoError(t, err)
+
+ configs, err = config.ReadAll("section")
+ assert.NoError(t, err)
+ assert.Equal(t, map[string]string{
+ "section.key": "value",
+ }, configs)
+
+ _, err = config.ReadBool("section.true")
+ assert.Equal(t, ErrNoConfigEntry, err)
+
+ err = config.RemoveAll("section.nonexistingkey")
+ assert.Error(t, err)
+
+ err = config.RemoveAll("section.key")
+ assert.NoError(t, err)
+
+ _, err = config.ReadString("section.key")
+ assert.Equal(t, ErrNoConfigEntry, err)
+
+ err = config.RemoveAll("nonexistingsection")
+ assert.Error(t, err)
+
+ err = config.RemoveAll("section")
+ assert.Error(t, err)
+
+ _, err = config.ReadString("section.key")
+ assert.Error(t, err)
+
+ err = config.RemoveAll("section.key")
+ assert.Error(t, err)
+}
diff --git a/repository/git.go b/repository/git.go
index 1eb3b8b5..e4cb57e6 100644
--- a/repository/git.go
+++ b/repository/git.go
@@ -8,30 +8,25 @@ import (
"os/exec"
"path"
"strings"
-
- "github.com/pkg/errors"
+ "sync"
"github.com/MichaelMure/git-bug/util/git"
"github.com/MichaelMure/git-bug/util/lamport"
)
const (
- createClockFile = "/git-bug/create-clock"
- editClockFile = "/git-bug/edit-clock"
-)
-
-var (
- // ErrNotARepo is the error returned when the git repo root wan't be found
- ErrNotARepo = errors.New("not a git repository")
+ clockPath = "git-bug"
)
var _ ClockedRepo = &GitRepo{}
+var _ TestedRepo = &GitRepo{}
// GitRepo represents an instance of a (local) git repository.
type GitRepo struct {
- Path string
- createClock *lamport.Persisted
- editClock *lamport.Persisted
+ path string
+
+ clocksMutex sync.Mutex
+ clocks map[string]lamport.Clock
}
// LocalConfig give access to the repository scoped configuration
@@ -46,19 +41,14 @@ func (repo *GitRepo) GlobalConfig() Config {
// Run the given git command with the given I/O reader/writers, returning an error if it fails.
func (repo *GitRepo) runGitCommandWithIO(stdin io.Reader, stdout, stderr io.Writer, args ...string) error {
- repopath := repo.Path
- if repopath == ".git" {
- // seeduvax> trangely the git command sometimes fail for very unknown
- // reason wihtout this replacement.
- // observed with rev-list command when git-bug is called from git
- // hook script, even the same command with same args runs perfectly
- // when called directly from the same hook script.
- repopath = ""
- }
- // fmt.Printf("[%s] Running git %s\n", repopath, strings.Join(args, " "))
+ // make sure that the working directory for the command
+ // always exist, in particular when running "git init".
+ path := strings.TrimSuffix(repo.path, ".git")
+
+ // fmt.Printf("[%s] Running git %s\n", path, strings.Join(args, " "))
cmd := exec.Command("git", args...)
- cmd.Dir = repopath
+ cmd.Dir = path
cmd.Stdin = stdin
cmd.Stdout = stdout
cmd.Stderr = stderr
@@ -93,8 +83,11 @@ func (repo *GitRepo) runGitCommand(args ...string) (string, error) {
// NewGitRepo determines if the given working directory is inside of a git repository,
// and returns the corresponding GitRepo instance if it is.
-func NewGitRepo(path string, witnesser Witnesser) (*GitRepo, error) {
- repo := &GitRepo{Path: path}
+func NewGitRepo(path string, clockLoaders []ClockLoader) (*GitRepo, error) {
+ repo := &GitRepo{
+ path: path,
+ clocks: make(map[string]lamport.Clock),
+ }
// Check the repo and retrieve the root path
stdout, err := repo.runGitCommand("rev-parse", "--git-dir")
@@ -107,28 +100,22 @@ func NewGitRepo(path string, witnesser Witnesser) (*GitRepo, error) {
}
// Fix the path to be sure we are at the root
- repo.Path = stdout
-
- err = repo.LoadClocks()
-
- if err != nil {
- // No clock yet, trying to initialize them
- err = repo.createClocks()
- if err != nil {
- return nil, err
+ repo.path = stdout
+
+ for _, loader := range clockLoaders {
+ allExist := true
+ for _, name := range loader.Clocks {
+ if _, err := repo.getClock(name); err != nil {
+ allExist = false
+ }
}
- err = witnesser(repo)
- if err != nil {
- return nil, err
+ if !allExist {
+ err = loader.Witnesser(repo)
+ if err != nil {
+ return nil, err
+ }
}
-
- err = repo.WriteClocks()
- if err != nil {
- return nil, err
- }
-
- return repo, nil
}
return repo, nil
@@ -136,13 +123,12 @@ func NewGitRepo(path string, witnesser Witnesser) (*GitRepo, error) {
// InitGitRepo create a new empty git repo at the given path
func InitGitRepo(path string) (*GitRepo, error) {
- repo := &GitRepo{Path: path + "/.git"}
- err := repo.createClocks()
- if err != nil {
- return nil, err
+ repo := &GitRepo{
+ path: path + "/.git",
+ clocks: make(map[string]lamport.Clock),
}
- _, err = repo.runGitCommand("init", path)
+ _, err := repo.runGitCommand("init", path)
if err != nil {
return nil, err
}
@@ -152,13 +138,12 @@ func InitGitRepo(path string) (*GitRepo, error) {
// InitBareGitRepo create a new --bare empty git repo at the given path
func InitBareGitRepo(path string) (*GitRepo, error) {
- repo := &GitRepo{Path: path}
- err := repo.createClocks()
- if err != nil {
- return nil, err
+ repo := &GitRepo{
+ path: path,
+ clocks: make(map[string]lamport.Clock),
}
- _, err = repo.runGitCommand("init", "--bare", path)
+ _, err := repo.runGitCommand("init", "--bare", path)
if err != nil {
return nil, err
}
@@ -168,7 +153,7 @@ func InitBareGitRepo(path string) (*GitRepo, error) {
// GetPath returns the path to the repo.
func (repo *GitRepo) GetPath() string {
- return repo.Path
+ return repo.path
}
// GetUserName returns the name the the user has used to configure git
@@ -385,93 +370,56 @@ func (repo *GitRepo) GetTreeHash(commit git.Hash) (git.Hash, error) {
return git.Hash(stdout), nil
}
-// AddRemote add a new remote to the repository
-// Not in the interface because it's only used for testing
-func (repo *GitRepo) AddRemote(name string, url string) error {
- _, err := repo.runGitCommand("remote", "add", name, url)
-
- return err
-}
-
-func (repo *GitRepo) createClocks() error {
- createPath := path.Join(repo.Path, createClockFile)
- createClock, err := lamport.NewPersisted(createPath)
- if err != nil {
- return err
+// GetOrCreateClock return a Lamport clock stored in the Repo.
+// If the clock doesn't exist, it's created.
+func (repo *GitRepo) GetOrCreateClock(name string) (lamport.Clock, error) {
+ c, err := repo.getClock(name)
+ if err == nil {
+ return c, nil
}
-
- editPath := path.Join(repo.Path, editClockFile)
- editClock, err := lamport.NewPersisted(editPath)
- if err != nil {
- return err
+ if err != ErrClockNotExist {
+ return nil, err
}
- repo.createClock = createClock
- repo.editClock = editClock
+ repo.clocksMutex.Lock()
+ defer repo.clocksMutex.Unlock()
- return nil
-}
-
-// LoadClocks read the clocks values from the on-disk repo
-func (repo *GitRepo) LoadClocks() error {
- createClock, err := lamport.LoadPersisted(repo.GetPath() + createClockFile)
- if err != nil {
- return err
- }
+ p := path.Join(repo.path, clockPath, name+"-clock")
- editClock, err := lamport.LoadPersisted(repo.GetPath() + editClockFile)
+ c, err = lamport.NewPersistedClock(p)
if err != nil {
- return err
+ return nil, err
}
- repo.createClock = createClock
- repo.editClock = editClock
- return nil
+ repo.clocks[name] = c
+ return c, nil
}
-// WriteClocks write the clocks values into the repo
-func (repo *GitRepo) WriteClocks() error {
- err := repo.createClock.Write()
- if err != nil {
- return err
- }
+func (repo *GitRepo) getClock(name string) (lamport.Clock, error) {
+ repo.clocksMutex.Lock()
+ defer repo.clocksMutex.Unlock()
- err = repo.editClock.Write()
- if err != nil {
- return err
+ if c, ok := repo.clocks[name]; ok {
+ return c, nil
}
- return nil
-}
-
-// CreateTime return the current value of the creation clock
-func (repo *GitRepo) CreateTime() lamport.Time {
- return repo.createClock.Time()
-}
-
-// CreateTimeIncrement increment the creation clock and return the new value.
-func (repo *GitRepo) CreateTimeIncrement() (lamport.Time, error) {
- return repo.createClock.Increment()
-}
+ p := path.Join(repo.path, clockPath, name+"-clock")
-// EditTime return the current value of the edit clock
-func (repo *GitRepo) EditTime() lamport.Time {
- return repo.editClock.Time()
-}
-
-// EditTimeIncrement increment the edit clock and return the new value.
-func (repo *GitRepo) EditTimeIncrement() (lamport.Time, error) {
- return repo.editClock.Increment()
+ c, err := lamport.LoadPersistedClock(p)
+ if err == nil {
+ repo.clocks[name] = c
+ return c, nil
+ }
+ if err == lamport.ErrClockNotExist {
+ return nil, ErrClockNotExist
+ }
+ return nil, err
}
-// WitnessCreate witness another create time and increment the corresponding clock
-// if needed.
-func (repo *GitRepo) WitnessCreate(time lamport.Time) error {
- return repo.createClock.Witness(time)
-}
+// AddRemote add a new remote to the repository
+// Not in the interface because it's only used for testing
+func (repo *GitRepo) AddRemote(name string, url string) error {
+ _, err := repo.runGitCommand("remote", "add", name, url)
-// WitnessEdit witness another edition time and increment the corresponding clock
-// if needed.
-func (repo *GitRepo) WitnessEdit(time lamport.Time) error {
- return repo.editClock.Witness(time)
+ return err
}
diff --git a/repository/config_git.go b/repository/git_config.go
index 987cf195..987cf195 100644
--- a/repository/config_git.go
+++ b/repository/git_config.go
diff --git a/repository/git_test.go b/repository/git_test.go
index de442e39..1b36fd4c 100644
--- a/repository/git_test.go
+++ b/repository/git_test.go
@@ -3,65 +3,8 @@ package repository
import (
"testing"
-
- "github.com/stretchr/testify/assert"
)
-func TestConfig(t *testing.T) {
- repo := CreateTestRepo(false)
- defer CleanupTestRepos(t, repo)
-
- err := repo.LocalConfig().StoreString("section.key", "value")
- assert.NoError(t, err)
-
- val, err := repo.LocalConfig().ReadString("section.key")
- assert.NoError(t, err)
- assert.Equal(t, "value", val)
-
- err = repo.LocalConfig().StoreString("section.true", "true")
- assert.NoError(t, err)
-
- val2, err := repo.LocalConfig().ReadBool("section.true")
- assert.NoError(t, err)
- assert.Equal(t, true, val2)
-
- configs, err := repo.LocalConfig().ReadAll("section")
- assert.NoError(t, err)
- assert.Equal(t, configs, map[string]string{
- "section.key": "value",
- "section.true": "true",
- })
-
- err = repo.LocalConfig().RemoveAll("section.true")
- assert.NoError(t, err)
-
- configs, err = repo.LocalConfig().ReadAll("section")
- assert.NoError(t, err)
- assert.Equal(t, configs, map[string]string{
- "section.key": "value",
- })
-
- _, err = repo.LocalConfig().ReadBool("section.true")
- assert.Equal(t, ErrNoConfigEntry, err)
-
- err = repo.LocalConfig().RemoveAll("section.nonexistingkey")
- assert.Error(t, err)
-
- err = repo.LocalConfig().RemoveAll("section.key")
- assert.NoError(t, err)
-
- _, err = repo.LocalConfig().ReadString("section.key")
- assert.Equal(t, ErrNoConfigEntry, err)
-
- err = repo.LocalConfig().RemoveAll("nonexistingsection")
- assert.Error(t, err)
-
- err = repo.LocalConfig().RemoveAll("section")
- assert.Error(t, err)
-
- _, err = repo.LocalConfig().ReadString("section.key")
- assert.Error(t, err)
-
- err = repo.LocalConfig().RemoveAll("section.key")
- assert.Error(t, err)
+func TestGitRepo(t *testing.T) {
+ RepoTest(t, CreateTestRepo, CleanupTestRepos)
}
diff --git a/repository/git_testing.go b/repository/git_testing.go
index 37a15d93..5ae4ccc9 100644
--- a/repository/git_testing.go
+++ b/repository/git_testing.go
@@ -3,21 +3,16 @@ package repository
import (
"io/ioutil"
"log"
- "os"
- "strings"
- "testing"
)
// This is intended for testing only
-func CreateTestRepo(bare bool) *GitRepo {
+func CreateTestRepo(bare bool) TestedRepo {
dir, err := ioutil.TempDir("", "")
if err != nil {
log.Fatal(err)
}
- // fmt.Println("Creating repo:", dir)
-
var creator func(string) (*GitRepo, error)
if bare {
@@ -42,38 +37,7 @@ func CreateTestRepo(bare bool) *GitRepo {
return repo
}
-func CleanupTestRepos(t testing.TB, repos ...Repo) {
- var firstErr error
- for _, repo := range repos {
- path := repo.GetPath()
- if strings.HasSuffix(path, "/.git") {
- // for a normal repository (not --bare), we want to remove everything
- // including the parent directory where files are checked out
- path = strings.TrimSuffix(path, "/.git")
-
- // Testing non-bare repo should also check path is
- // only .git (i.e. ./.git), but doing so, we should
- // try to remove the current directory and hav some
- // trouble. In the present case, this case should not
- // occur.
- // TODO consider warning or error when path == ".git"
- }
- // fmt.Println("Cleaning repo:", path)
- err := os.RemoveAll(path)
- if err != nil {
- log.Println(err)
- if firstErr == nil {
- firstErr = err
- }
- }
- }
-
- if firstErr != nil {
- t.Fatal(firstErr)
- }
-}
-
-func SetupReposAndRemote(t testing.TB) (repoA, repoB, remote *GitRepo) {
+func SetupReposAndRemote() (repoA, repoB, remote TestedRepo) {
repoA = CreateTestRepo(false)
repoB = CreateTestRepo(false)
remote = CreateTestRepo(true)
@@ -82,12 +46,12 @@ func SetupReposAndRemote(t testing.TB) (repoA, repoB, remote *GitRepo) {
err := repoA.AddRemote("origin", remoteAddr)
if err != nil {
- t.Fatal(err)
+ log.Fatal(err)
}
err = repoB.AddRemote("origin", remoteAddr)
if err != nil {
- t.Fatal(err)
+ log.Fatal(err)
}
return repoA, repoB, remote
diff --git a/repository/mock_repo.go b/repository/mock_repo.go
index 89d0f395..04d9355e 100644
--- a/repository/mock_repo.go
+++ b/repository/mock_repo.go
@@ -9,6 +9,7 @@ import (
)
var _ ClockedRepo = &mockRepoForTest{}
+var _ TestedRepo = &mockRepoForTest{}
// mockRepoForTest defines an instance of Repo that can be used for testing.
type mockRepoForTest struct {
@@ -18,8 +19,7 @@ type mockRepoForTest struct {
trees map[git.Hash]string
commits map[git.Hash]commit
refs map[string]git.Hash
- createClock lamport.Clock
- editClock lamport.Clock
+ clocks map[string]lamport.Clock
}
type commit struct {
@@ -35,8 +35,7 @@ func NewMockRepoForTest() *mockRepoForTest {
trees: make(map[git.Hash]string),
commits: make(map[git.Hash]commit),
refs: make(map[string]git.Hash),
- createClock: lamport.NewClock(),
- editClock: lamport.NewClock(),
+ clocks: make(map[string]lamport.Clock),
}
}
@@ -213,36 +212,16 @@ func (r *mockRepoForTest) GetTreeHash(commit git.Hash) (git.Hash, error) {
panic("implement me")
}
-func (r *mockRepoForTest) LoadClocks() error {
- return nil
-}
-
-func (r *mockRepoForTest) WriteClocks() error {
- return nil
-}
-
-func (r *mockRepoForTest) CreateTime() lamport.Time {
- return r.createClock.Time()
-}
-
-func (r *mockRepoForTest) CreateTimeIncrement() (lamport.Time, error) {
- return r.createClock.Increment(), nil
-}
-
-func (r *mockRepoForTest) EditTime() lamport.Time {
- return r.editClock.Time()
-}
-
-func (r *mockRepoForTest) EditTimeIncrement() (lamport.Time, error) {
- return r.editClock.Increment(), nil
-}
+func (r *mockRepoForTest) GetOrCreateClock(name string) (lamport.Clock, error) {
+ if c, ok := r.clocks[name]; ok {
+ return c, nil
+ }
-func (r *mockRepoForTest) WitnessCreate(time lamport.Time) error {
- r.createClock.Witness(time)
- return nil
+ c := lamport.NewMemClock()
+ r.clocks[name] = c
+ return c, nil
}
-func (r *mockRepoForTest) WitnessEdit(time lamport.Time) error {
- r.editClock.Witness(time)
- return nil
+func (r *mockRepoForTest) AddRemote(name string, url string) error {
+ panic("implement me")
}
diff --git a/repository/mock_repo_test.go b/repository/mock_repo_test.go
new file mode 100644
index 00000000..b56b94f2
--- /dev/null
+++ b/repository/mock_repo_test.go
@@ -0,0 +1,10 @@
+package repository
+
+import "testing"
+
+func TestMockRepo(t *testing.T) {
+ creator := func(bare bool) TestedRepo { return NewMockRepoForTest() }
+ cleaner := func(repos ...Repo) {}
+
+ RepoTest(t, creator, cleaner)
+}
diff --git a/repository/repo.go b/repository/repo.go
index e8517508..561fc1c5 100644
--- a/repository/repo.go
+++ b/repository/repo.go
@@ -13,6 +13,10 @@ import (
var (
ErrNoConfigEntry = errors.New("no config entry for the given key")
ErrMultipleConfigEntry = errors.New("multiple config entry for the given key")
+ // ErrNotARepo is the error returned when the git repo root wan't be found
+ ErrNotARepo = errors.New("not a git repository")
+ // ErrClockNotExist is the error returned when a clock can't be found
+ ErrClockNotExist = errors.New("clock doesn't exist")
)
// RepoConfig access the configuration of a repository
@@ -97,36 +101,22 @@ type Repo interface {
type ClockedRepo interface {
Repo
- // LoadClocks read the clocks values from the on-disk repo
- LoadClocks() error
-
- // WriteClocks write the clocks values into the repo
- WriteClocks() error
-
- // CreateTime return the current value of the creation clock
- CreateTime() lamport.Time
-
- // CreateTimeIncrement increment the creation clock and return the new value.
- CreateTimeIncrement() (lamport.Time, error)
-
- // EditTime return the current value of the edit clock
- EditTime() lamport.Time
-
- // EditTimeIncrement increment the edit clock and return the new value.
- EditTimeIncrement() (lamport.Time, error)
-
- // WitnessCreate witness another create time and increment the corresponding
- // clock if needed.
- WitnessCreate(time lamport.Time) error
-
- // WitnessEdit witness another edition time and increment the corresponding
- // clock if needed.
- WitnessEdit(time lamport.Time) error
+ // GetOrCreateClock return a Lamport clock stored in the Repo.
+ // If the clock doesn't exist, it's created.
+ GetOrCreateClock(name string) (lamport.Clock, error)
}
-// Witnesser is a function that will initialize the clocks of a repo
-// from scratch
-type Witnesser func(repo ClockedRepo) error
+// ClockLoader hold which logical clock need to exist for an entity and
+// how to create them if they don't.
+type ClockLoader struct {
+ // Clocks hold the name of all the clocks this loader deal with.
+ // Those clocks will be checked when the repo load. If not present or broken,
+ // Witnesser will be used to create them.
+ Clocks []string
+ // Witnesser is a function that will initialize the clocks of a repo
+ // from scratch
+ Witnesser func(repo ClockedRepo) error
+}
func prepareTreeEntries(entries []TreeEntry) bytes.Buffer {
var buffer bytes.Buffer
@@ -158,3 +148,11 @@ func readTreeEntries(s string) ([]TreeEntry, error) {
return casted, nil
}
+
+// TestedRepo is an extended ClockedRepo with function for testing only
+type TestedRepo interface {
+ ClockedRepo
+
+ // AddRemote add a new remote to the repository
+ AddRemote(name string, url string) error
+}
diff --git a/repository/repo_testing.go b/repository/repo_testing.go
new file mode 100644
index 00000000..43bbaf14
--- /dev/null
+++ b/repository/repo_testing.go
@@ -0,0 +1,70 @@
+package repository
+
+import (
+ "log"
+ "os"
+ "strings"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func CleanupTestRepos(repos ...Repo) {
+ var firstErr error
+ for _, repo := range repos {
+ path := repo.GetPath()
+ if strings.HasSuffix(path, "/.git") {
+ // for a normal repository (not --bare), we want to remove everything
+ // including the parent directory where files are checked out
+ path = strings.TrimSuffix(path, "/.git")
+
+ // Testing non-bare repo should also check path is
+ // only .git (i.e. ./.git), but doing so, we should
+ // try to remove the current directory and hav some
+ // trouble. In the present case, this case should not
+ // occur.
+ // TODO consider warning or error when path == ".git"
+ }
+ // fmt.Println("Cleaning repo:", path)
+ err := os.RemoveAll(path)
+ if err != nil {
+ log.Println(err)
+ if firstErr == nil {
+ firstErr = err
+ }
+ }
+ }
+
+ if firstErr != nil {
+ log.Fatal(firstErr)
+ }
+}
+
+type RepoCreator func(bare bool) TestedRepo
+type RepoCleaner func(repos ...Repo)
+
+// Test suite for a Repo implementation
+func RepoTest(t *testing.T, creator RepoCreator, cleaner RepoCleaner) {
+ t.Run("Read/Write data", func(t *testing.T) {
+ repo := creator(false)
+ defer cleaner(repo)
+
+ data := []byte("hello")
+
+ h, err := repo.StoreData(data)
+ require.NoError(t, err)
+ assert.NotEmpty(t, h)
+
+ read, err := repo.ReadData(h)
+ require.NoError(t, err)
+ assert.Equal(t, data, read)
+ })
+
+ t.Run("Local config", func(t *testing.T) {
+ repo := creator(false)
+ defer cleaner(repo)
+
+ testConfig(t, repo.LocalConfig())
+ })
+}
diff --git a/repository/tree_entry_test.go b/repository/tree_entry_test.go
index 58da075c..78f58ff6 100644
--- a/repository/tree_entry_test.go
+++ b/repository/tree_entry_test.go
@@ -3,11 +3,12 @@ package repository
import (
"testing"
+ "github.com/stretchr/testify/assert"
+
"github.com/MichaelMure/git-bug/util/git"
)
func TestTreeEntryFormat(t *testing.T) {
-
entries := []TreeEntry{
{Blob, git.Hash("a85730cf5287d40a1e32d3a671ba2296c73387cb"), "name"},
{Tree, git.Hash("a85730cf5287d40a1e32d3a671ba2296c73387cb"), "name"},
@@ -26,10 +27,7 @@ func TestTreeEntryParse(t *testing.T) {
for _, line := range lines {
_, err := ParseTreeEntry(line)
-
- if err != nil {
- t.Fatal(err)
- }
+ assert.NoError(t, err)
}
}
diff --git a/termui/show_bug.go b/termui/show_bug.go
index a5da519f..83d40ffa 100644
--- a/termui/show_bug.go
+++ b/termui/show_bug.go
@@ -222,7 +222,7 @@ func (sb *showBug) renderMain(g *gocui.Gui, mainView *gocui.View) error {
snap.CreateTime.Format(timeLayout),
edited,
)
- bugHeader, lines := text.Wrap(bugHeader, maxX)
+ bugHeader, lines := text.Wrap(bugHeader, maxX, text.WrapIndent(" "))
v, err := sb.createOpView(g, showBugHeaderView, x0, y0, maxX+1, lines, false)
if err != nil {
diff --git a/tests/read_bugs_test.go b/tests/read_bugs_test.go
index c510fdbe..4fd3943a 100644
--- a/tests/read_bugs_test.go
+++ b/tests/read_bugs_test.go
@@ -10,7 +10,7 @@ import (
func TestReadBugs(t *testing.T) {
repo := repository.CreateTestRepo(false)
- defer repository.CleanupTestRepos(t, repo)
+ defer repository.CleanupTestRepos(repo)
random_bugs.FillRepoWithSeed(repo, 15, 42)
@@ -24,7 +24,7 @@ func TestReadBugs(t *testing.T) {
func benchmarkReadBugs(bugNumber int, t *testing.B) {
repo := repository.CreateTestRepo(false)
- defer repository.CleanupTestRepos(t, repo)
+ defer repository.CleanupTestRepos(repo)
random_bugs.FillRepoWithSeed(repo, bugNumber, 42)
t.ResetTimer()
diff --git a/util/lamport/clock.go b/util/lamport/clock.go
new file mode 100644
index 00000000..53b0ac7a
--- /dev/null
+++ b/util/lamport/clock.go
@@ -0,0 +1,15 @@
+package lamport
+
+// Time is the value of a Clock.
+type Time uint64
+
+// Clock is a Lamport logical clock
+type Clock interface {
+ // Time is used to return the current value of the lamport clock
+ Time() Time
+ // Increment is used to return the value of the lamport clock and increment it afterwards
+ Increment() (Time, error)
+ // Witness is called to update our local clock if necessary after
+ // witnessing a clock value received from another process
+ Witness(time Time) error
+}
diff --git a/util/lamport/clock_testing.go b/util/lamport/clock_testing.go
new file mode 100644
index 00000000..fc59afb2
--- /dev/null
+++ b/util/lamport/clock_testing.go
@@ -0,0 +1,28 @@
+package lamport
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func testClock(t *testing.T, c Clock) {
+ assert.Equal(t, Time(1), c.Time())
+
+ val, err := c.Increment()
+ assert.NoError(t, err)
+ assert.Equal(t, Time(1), val)
+ assert.Equal(t, Time(2), c.Time())
+
+ err = c.Witness(41)
+ assert.NoError(t, err)
+ assert.Equal(t, Time(42), c.Time())
+
+ err = c.Witness(41)
+ assert.NoError(t, err)
+ assert.Equal(t, Time(42), c.Time())
+
+ err = c.Witness(30)
+ assert.NoError(t, err)
+ assert.Equal(t, Time(42), c.Time())
+}
diff --git a/util/lamport/lamport_test.go b/util/lamport/lamport_test.go
deleted file mode 100644
index c650fe6a..00000000
--- a/util/lamport/lamport_test.go
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
-
- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this file,
- You can obtain one at http://mozilla.org/MPL/2.0/.
-
- Copyright (c) 2013, Armon Dadgar armon.dadgar@gmail.com
- Copyright (c) 2013, Mitchell Hashimoto mitchell.hashimoto@gmail.com
-
- Alternatively, the contents of this file may be used under the terms
- of the GNU General Public License Version 3 or later, as described below:
-
- This file is free software: you may copy, redistribute and/or modify
- it under the terms of the GNU General Public License as published by the
- Free Software Foundation, either version 3 of the License, or (at your
- option) any later version.
-
- This file is distributed in the hope that it will be useful, but
- WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
- Public License for more details.
-
- You should have received a copy of the GNU General Public License
- along with this program. If not, see http://www.gnu.org/licenses/.
-
-*/
-
-package lamport
-
-import (
- "testing"
-)
-
-func TestClock(t *testing.T) {
- l := &Clock{}
-
- if l.Time() != 0 {
- t.Fatalf("bad time value")
- }
-
- if l.Increment() != 0 {
- t.Fatalf("bad time value")
- }
-
- if l.Time() != 1 {
- t.Fatalf("bad time value")
- }
-
- l.Witness(41)
-
- if l.Time() != 42 {
- t.Fatalf("bad time value")
- }
-
- l.Witness(41)
-
- if l.Time() != 42 {
- t.Fatalf("bad time value")
- }
-
- l.Witness(30)
-
- if l.Time() != 42 {
- t.Fatalf("bad time value")
- }
-}
diff --git a/util/lamport/lamport.go b/util/lamport/mem_clock.go
index 0372bb6f..ce6f2d4d 100644
--- a/util/lamport/lamport.go
+++ b/util/lamport/mem_clock.go
@@ -31,58 +31,59 @@ import (
"sync/atomic"
)
-// Clock is a thread safe implementation of a lamport clock. It
+var _ Clock = &MemClock{}
+
+// MemClock is a thread safe implementation of a lamport clock. It
// uses efficient atomic operations for all of its functions, falling back
// to a heavy lock only if there are enough CAS failures.
-type Clock struct {
+type MemClock struct {
counter uint64
}
-// Time is the value of a Clock.
-type Time uint64
-
-// NewClock create a new clock with the value 1.
+// NewMemClock create a new clock with the value 1.
// Value 0 is considered as invalid.
-func NewClock() Clock {
- return Clock{
+func NewMemClock() *MemClock {
+ return &MemClock{
counter: 1,
}
}
-// NewClockWithTime create a new clock with a value.
-func NewClockWithTime(time uint64) Clock {
- return Clock{
+// NewMemClockWithTime create a new clock with a value.
+func NewMemClockWithTime(time uint64) *MemClock {
+ return &MemClock{
counter: time,
}
}
// Time is used to return the current value of the lamport clock
-func (l *Clock) Time() Time {
- return Time(atomic.LoadUint64(&l.counter))
+func (mc *MemClock) Time() Time {
+ return Time(atomic.LoadUint64(&mc.counter))
}
// Increment is used to return the value of the lamport clock and increment it afterwards
-func (l *Clock) Increment() Time {
- return Time(atomic.AddUint64(&l.counter, 1) - 1)
+func (mc *MemClock) Increment() (Time, error) {
+ return Time(atomic.AddUint64(&mc.counter, 1) - 1), nil
}
// Witness is called to update our local clock if necessary after
// witnessing a clock value received from another process
-func (l *Clock) Witness(v Time) {
+func (mc *MemClock) Witness(v Time) error {
WITNESS:
// If the other value is old, we do not need to do anything
- cur := atomic.LoadUint64(&l.counter)
+ cur := atomic.LoadUint64(&mc.counter)
other := uint64(v)
if other < cur {
- return
+ return nil
}
// Ensure that our local clock is at least one ahead.
- if !atomic.CompareAndSwapUint64(&l.counter, cur, other+1) {
+ if !atomic.CompareAndSwapUint64(&mc.counter, cur, other+1) {
// CAS: CompareAndSwap
// The CAS failed, so we just retry. Eventually our CAS should
// succeed or a future witness will pass us by and our witness
// will end.
goto WITNESS
}
+
+ return nil
}
diff --git a/util/lamport/mem_clock_test.go b/util/lamport/mem_clock_test.go
new file mode 100644
index 00000000..e01d2ec0
--- /dev/null
+++ b/util/lamport/mem_clock_test.go
@@ -0,0 +1,8 @@
+package lamport
+
+import "testing"
+
+func TestMemClock(t *testing.T) {
+ c := NewMemClock()
+ testClock(t, c)
+}
diff --git a/util/lamport/persisted_clock.go b/util/lamport/persisted_clock.go
new file mode 100644
index 00000000..e70b01ef
--- /dev/null
+++ b/util/lamport/persisted_clock.go
@@ -0,0 +1,100 @@
+package lamport
+
+import (
+ "errors"
+ "fmt"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+)
+
+var ErrClockNotExist = errors.New("clock doesn't exist")
+
+type PersistedClock struct {
+ *MemClock
+ filePath string
+}
+
+// NewPersistedClock create a new persisted Lamport clock
+func NewPersistedClock(filePath string) (*PersistedClock, error) {
+ clock := &PersistedClock{
+ MemClock: NewMemClock(),
+ filePath: filePath,
+ }
+
+ dir := filepath.Dir(filePath)
+ err := os.MkdirAll(dir, 0777)
+ if err != nil {
+ return nil, err
+ }
+
+ err = clock.Write()
+ if err != nil {
+ return nil, err
+ }
+
+ return clock, nil
+}
+
+// LoadPersistedClock load a persisted Lamport clock from a file
+func LoadPersistedClock(filePath string) (*PersistedClock, error) {
+ clock := &PersistedClock{
+ filePath: filePath,
+ }
+
+ err := clock.read()
+ if err != nil {
+ return nil, err
+ }
+
+ return clock, nil
+}
+
+// Increment is used to return the value of the lamport clock and increment it afterwards
+func (pc *PersistedClock) Increment() (Time, error) {
+ time, err := pc.MemClock.Increment()
+ if err != nil {
+ return 0, err
+ }
+ return time, pc.Write()
+}
+
+// Witness is called to update our local clock if necessary after
+// witnessing a clock value received from another process
+func (pc *PersistedClock) Witness(time Time) error {
+ // TODO: rework so that we write only when the clock was actually updated
+ err := pc.MemClock.Witness(time)
+ if err != nil {
+ return err
+ }
+ return pc.Write()
+}
+
+func (pc *PersistedClock) read() error {
+ content, err := ioutil.ReadFile(pc.filePath)
+ if os.IsNotExist(err) {
+ return ErrClockNotExist
+ }
+ if err != nil {
+ return err
+ }
+
+ var value uint64
+ n, err := fmt.Sscanf(string(content), "%d", &value)
+ if err != nil {
+ return err
+ }
+
+ if n != 1 {
+ return fmt.Errorf("could not read the clock")
+ }
+
+ pc.MemClock = NewMemClockWithTime(value)
+
+ return nil
+}
+
+func (pc *PersistedClock) Write() error {
+ data := []byte(fmt.Sprintf("%d", pc.counter))
+ return ioutil.WriteFile(pc.filePath, data, 0644)
+}
diff --git a/util/lamport/persisted_clock_test.go b/util/lamport/persisted_clock_test.go
new file mode 100644
index 00000000..aacec3bf
--- /dev/null
+++ b/util/lamport/persisted_clock_test.go
@@ -0,0 +1,19 @@
+package lamport
+
+import (
+ "io/ioutil"
+ "path"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestPersistedClock(t *testing.T) {
+ dir, err := ioutil.TempDir("", "")
+ require.NoError(t, err)
+
+ c, err := NewPersistedClock(path.Join(dir, "test-clock"))
+ require.NoError(t, err)
+
+ testClock(t, c)
+}
diff --git a/util/lamport/persisted_lamport.go b/util/lamport/persisted_lamport.go
deleted file mode 100644
index ab4b93b1..00000000
--- a/util/lamport/persisted_lamport.go
+++ /dev/null
@@ -1,84 +0,0 @@
-package lamport
-
-import (
- "fmt"
- "io/ioutil"
- "os"
- "path/filepath"
-)
-
-type Persisted struct {
- Clock
- filePath string
-}
-
-// NewPersisted create a new persisted Lamport clock
-func NewPersisted(filePath string) (*Persisted, error) {
- clock := &Persisted{
- Clock: NewClock(),
- filePath: filePath,
- }
-
- dir := filepath.Dir(filePath)
- err := os.MkdirAll(dir, 0777)
- if err != nil {
- return nil, err
- }
-
- return clock, nil
-}
-
-// LoadPersisted load a persisted Lamport clock from a file
-func LoadPersisted(filePath string) (*Persisted, error) {
- clock := &Persisted{
- filePath: filePath,
- }
-
- err := clock.read()
- if err != nil {
- return nil, err
- }
-
- return clock, nil
-}
-
-// Increment is used to return the value of the lamport clock and increment it afterwards
-func (c *Persisted) Increment() (Time, error) {
- time := c.Clock.Increment()
- return time, c.Write()
-}
-
-// Witness is called to update our local clock if necessary after
-// witnessing a clock value received from another process
-func (c *Persisted) Witness(time Time) error {
- // TODO: rework so that we write only when the clock was actually updated
- c.Clock.Witness(time)
- return c.Write()
-}
-
-func (c *Persisted) read() error {
- content, err := ioutil.ReadFile(c.filePath)
- if err != nil {
- return err
- }
-
- var value uint64
- n, err := fmt.Sscanf(string(content), "%d", &value)
-
- if err != nil {
- return err
- }
-
- if n != 1 {
- return fmt.Errorf("could not read the clock")
- }
-
- c.Clock = NewClockWithTime(value)
-
- return nil
-}
-
-func (c *Persisted) Write() error {
- data := []byte(fmt.Sprintf("%d", c.counter))
- return ioutil.WriteFile(c.filePath, data, 0644)
-}