aboutsummaryrefslogtreecommitdiffstats
path: root/repository
diff options
context:
space:
mode:
Diffstat (limited to 'repository')
-rw-r--r--repository/config.go113
-rw-r--r--repository/config_test.go54
-rw-r--r--repository/config_testing.go97
-rw-r--r--repository/git.go93
-rw-r--r--repository/git_cli.go56
-rw-r--r--repository/git_config.go18
-rw-r--r--repository/git_testing.go18
-rw-r--r--repository/gogit.go615
-rw-r--r--repository/gogit_config.go235
-rw-r--r--repository/gogit_test.go68
-rw-r--r--repository/gogit_testing.go58
-rw-r--r--repository/keyring.go50
-rw-r--r--repository/mock_repo.go166
-rw-r--r--repository/repo.go80
-rw-r--r--repository/repo_testing.go262
-rw-r--r--repository/tree_entry.go32
16 files changed, 1715 insertions, 300 deletions
diff --git a/repository/config.go b/repository/config.go
index 4fa5c69b..4db8d4be 100644
--- a/repository/config.go
+++ b/repository/config.go
@@ -1,21 +1,23 @@
package repository
import (
+ "errors"
"strconv"
"time"
)
+var (
+ ErrNoConfigEntry = errors.New("no config entry for the given key")
+ ErrMultipleConfigEntry = errors.New("multiple config entry for the given key")
+)
+
// Config represent the common function interacting with the repository config storage
type Config interface {
- // Store writes a single key/value pair in the config
- StoreString(key, value string) error
-
- // Store writes a key and timestamp value to the config
- StoreTimestamp(key string, value time.Time) error
-
- // Store writes a key and boolean value to the config
- StoreBool(key string, value bool) error
+ ConfigRead
+ ConfigWrite
+}
+type ConfigRead interface {
// ReadAll reads all key/value pair matching the key prefix
ReadAll(keyPrefix string) (map[string]string, error)
@@ -33,6 +35,17 @@ type Config interface {
// Return ErrNoConfigEntry or ErrMultipleConfigEntry if
// there is zero or more than one entry for this key
ReadTimestamp(key string) (time.Time, error)
+}
+
+type ConfigWrite interface {
+ // Store writes a single key/value pair in the config
+ StoreString(key, value string) error
+
+ // Store writes a key and timestamp value to the config
+ StoreTimestamp(key string, value time.Time) error
+
+ // Store writes a key and boolean value to the config
+ StoreBool(key string, value bool) error
// RemoveAll removes all key/value pair matching the key prefix
RemoveAll(keyPrefix string) error
@@ -46,3 +59,87 @@ func ParseTimestamp(s string) (time.Time, error) {
return time.Unix(int64(timestamp), 0), nil
}
+
+// mergeConfig is a helper to easily support RepoConfig.AnyConfig()
+// from two separate local and global Config
+func mergeConfig(local ConfigRead, global ConfigRead) *mergedConfig {
+ return &mergedConfig{
+ local: local,
+ global: global,
+ }
+}
+
+var _ ConfigRead = &mergedConfig{}
+
+type mergedConfig struct {
+ local ConfigRead
+ global ConfigRead
+}
+
+func (m *mergedConfig) ReadAll(keyPrefix string) (map[string]string, error) {
+ values, err := m.global.ReadAll(keyPrefix)
+ if err != nil {
+ return nil, err
+ }
+ locals, err := m.local.ReadAll(keyPrefix)
+ if err != nil {
+ return nil, err
+ }
+ for k, val := range locals {
+ values[k] = val
+ }
+ return values, nil
+}
+
+func (m *mergedConfig) ReadBool(key string) (bool, error) {
+ v, err := m.local.ReadBool(key)
+ if err == nil {
+ return v, nil
+ }
+ if err != ErrNoConfigEntry && err != ErrMultipleConfigEntry {
+ return false, err
+ }
+ return m.global.ReadBool(key)
+}
+
+func (m *mergedConfig) ReadString(key string) (string, error) {
+ val, err := m.local.ReadString(key)
+ if err == nil {
+ return val, nil
+ }
+ if err != ErrNoConfigEntry && err != ErrMultipleConfigEntry {
+ return "", err
+ }
+ return m.global.ReadString(key)
+}
+
+func (m *mergedConfig) ReadTimestamp(key string) (time.Time, error) {
+ val, err := m.local.ReadTimestamp(key)
+ if err == nil {
+ return val, nil
+ }
+ if err != ErrNoConfigEntry && err != ErrMultipleConfigEntry {
+ return time.Time{}, err
+ }
+ return m.global.ReadTimestamp(key)
+}
+
+var _ ConfigWrite = &configPanicWriter{}
+
+type configPanicWriter struct{}
+
+func (c configPanicWriter) StoreString(key, value string) error {
+ panic("not implemented")
+}
+
+func (c configPanicWriter) StoreTimestamp(key string, value time.Time) error {
+ panic("not implemented")
+}
+
+func (c configPanicWriter) StoreBool(key string, value bool) error {
+ panic("not implemented")
+}
+
+func (c configPanicWriter) RemoveAll(keyPrefix string) error {
+ panic("not implemented")
+}
diff --git a/repository/config_test.go b/repository/config_test.go
new file mode 100644
index 00000000..2a763540
--- /dev/null
+++ b/repository/config_test.go
@@ -0,0 +1,54 @@
+package repository
+
+import (
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestMergedConfig(t *testing.T) {
+ local := NewMemConfig()
+ global := NewMemConfig()
+ merged := mergeConfig(local, global)
+
+ require.NoError(t, global.StoreBool("bool", true))
+ require.NoError(t, global.StoreString("string", "foo"))
+ require.NoError(t, global.StoreTimestamp("timestamp", time.Unix(1234, 0)))
+
+ val1, err := merged.ReadBool("bool")
+ require.NoError(t, err)
+ require.Equal(t, val1, true)
+
+ val2, err := merged.ReadString("string")
+ require.NoError(t, err)
+ require.Equal(t, val2, "foo")
+
+ val3, err := merged.ReadTimestamp("timestamp")
+ require.NoError(t, err)
+ require.Equal(t, val3, time.Unix(1234, 0))
+
+ require.NoError(t, local.StoreBool("bool", false))
+ require.NoError(t, local.StoreString("string", "bar"))
+ require.NoError(t, local.StoreTimestamp("timestamp", time.Unix(5678, 0)))
+
+ val1, err = merged.ReadBool("bool")
+ require.NoError(t, err)
+ require.Equal(t, val1, false)
+
+ val2, err = merged.ReadString("string")
+ require.NoError(t, err)
+ require.Equal(t, val2, "bar")
+
+ val3, err = merged.ReadTimestamp("timestamp")
+ require.NoError(t, err)
+ require.Equal(t, val3, time.Unix(5678, 0))
+
+ all, err := merged.ReadAll("")
+ require.NoError(t, err)
+ require.Equal(t, all, map[string]string{
+ "bool": "false",
+ "string": "bar",
+ "timestamp": "5678",
+ })
+}
diff --git a/repository/config_testing.go b/repository/config_testing.go
index 25639d59..445f8721 100644
--- a/repository/config_testing.go
+++ b/repository/config_testing.go
@@ -2,62 +2,115 @@ package repository
import (
"testing"
+ "time"
- "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
)
func testConfig(t *testing.T, config Config) {
+ // string
err := config.StoreString("section.key", "value")
- assert.NoError(t, err)
+ require.NoError(t, err)
val, err := config.ReadString("section.key")
- assert.NoError(t, err)
- assert.Equal(t, "value", val)
+ require.NoError(t, err)
+ require.Equal(t, "value", val)
- err = config.StoreString("section.true", "true")
- assert.NoError(t, err)
+ // bool
+ err = config.StoreBool("section.true", true)
+ require.NoError(t, err)
val2, err := config.ReadBool("section.true")
- assert.NoError(t, err)
- assert.Equal(t, true, val2)
+ require.NoError(t, err)
+ require.Equal(t, true, val2)
+ // timestamp
+ err = config.StoreTimestamp("section.time", time.Unix(1234, 0))
+ require.NoError(t, err)
+
+ val3, err := config.ReadTimestamp("section.time")
+ require.NoError(t, err)
+ require.Equal(t, time.Unix(1234, 0), val3)
+
+ // ReadAll
configs, err := config.ReadAll("section")
- assert.NoError(t, err)
- assert.Equal(t, map[string]string{
+ require.NoError(t, err)
+ require.Equal(t, map[string]string{
"section.key": "value",
"section.true": "true",
+ "section.time": "1234",
}, configs)
+ // RemoveAll
err = config.RemoveAll("section.true")
- assert.NoError(t, err)
+ require.NoError(t, err)
configs, err = config.ReadAll("section")
- assert.NoError(t, err)
- assert.Equal(t, map[string]string{
- "section.key": "value",
+ require.NoError(t, err)
+ require.Equal(t, map[string]string{
+ "section.key": "value",
+ "section.time": "1234",
}, configs)
_, err = config.ReadBool("section.true")
- assert.Equal(t, ErrNoConfigEntry, err)
+ require.Equal(t, ErrNoConfigEntry, err)
err = config.RemoveAll("section.nonexistingkey")
- assert.Error(t, err)
+ require.Error(t, err)
err = config.RemoveAll("section.key")
- assert.NoError(t, err)
+ require.NoError(t, err)
_, err = config.ReadString("section.key")
- assert.Equal(t, ErrNoConfigEntry, err)
+ require.Equal(t, ErrNoConfigEntry, err)
err = config.RemoveAll("nonexistingsection")
- assert.Error(t, err)
+ require.Error(t, err)
+
+ err = config.RemoveAll("section.time")
+ require.NoError(t, err)
err = config.RemoveAll("section")
- assert.Error(t, err)
+ require.Error(t, err)
_, err = config.ReadString("section.key")
- assert.Error(t, err)
+ require.Error(t, err)
err = config.RemoveAll("section.key")
- assert.Error(t, err)
+ require.Error(t, err)
+
+ // section + subsections
+ require.NoError(t, config.StoreString("section.opt1", "foo"))
+ require.NoError(t, config.StoreString("section.opt2", "foo2"))
+ require.NoError(t, config.StoreString("section.subsection.opt1", "foo3"))
+ require.NoError(t, config.StoreString("section.subsection.opt2", "foo4"))
+ require.NoError(t, config.StoreString("section.subsection.subsection.opt1", "foo5"))
+ require.NoError(t, config.StoreString("section.subsection.subsection.opt2", "foo6"))
+
+ all, err := config.ReadAll("section")
+ require.NoError(t, err)
+ require.Equal(t, map[string]string{
+ "section.opt1": "foo",
+ "section.opt2": "foo2",
+ "section.subsection.opt1": "foo3",
+ "section.subsection.opt2": "foo4",
+ "section.subsection.subsection.opt1": "foo5",
+ "section.subsection.subsection.opt2": "foo6",
+ }, all)
+
+ all, err = config.ReadAll("section.subsection")
+ require.NoError(t, err)
+ require.Equal(t, map[string]string{
+ "section.subsection.opt1": "foo3",
+ "section.subsection.opt2": "foo4",
+ "section.subsection.subsection.opt1": "foo5",
+ "section.subsection.subsection.opt2": "foo6",
+ }, all)
+
+ all, err = config.ReadAll("section.subsection.subsection")
+ require.NoError(t, err)
+ require.Equal(t, map[string]string{
+ "section.subsection.subsection.opt1": "foo5",
+ "section.subsection.subsection.opt2": "foo6",
+ }, all)
}
diff --git a/repository/git.go b/repository/git.go
index 3d756324..dba2d29d 100644
--- a/repository/git.go
+++ b/repository/git.go
@@ -4,8 +4,6 @@ package repository
import (
"bytes"
"fmt"
- "io"
- "os/exec"
"path"
"strings"
"sync"
@@ -22,70 +20,28 @@ var _ TestedRepo = &GitRepo{}
// GitRepo represents an instance of a (local) git repository.
type GitRepo struct {
+ gitCli
path string
clocksMutex sync.Mutex
clocks map[string]lamport.Clock
-}
-
-// LocalConfig give access to the repository scoped configuration
-func (repo *GitRepo) LocalConfig() Config {
- return newGitConfig(repo, false)
-}
-
-// GlobalConfig give access to the git global configuration
-func (repo *GitRepo) GlobalConfig() Config {
- return newGitConfig(repo, true)
-}
-
-// 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 {
- // 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 = path
- cmd.Stdin = stdin
- cmd.Stdout = stdout
- cmd.Stderr = stderr
-
- return cmd.Run()
-}
-
-// Run the given git command and return its stdout, or an error if the command fails.
-func (repo *GitRepo) runGitCommandRaw(stdin io.Reader, args ...string) (string, string, error) {
- var stdout bytes.Buffer
- var stderr bytes.Buffer
- err := repo.runGitCommandWithIO(stdin, &stdout, &stderr, args...)
- return strings.TrimSpace(stdout.String()), strings.TrimSpace(stderr.String()), err
-}
-
-// Run the given git command and return its stdout, or an error if the command fails.
-func (repo *GitRepo) runGitCommandWithStdin(stdin io.Reader, args ...string) (string, error) {
- stdout, stderr, err := repo.runGitCommandRaw(stdin, args...)
- if err != nil {
- if stderr == "" {
- stderr = "Error running git command: " + strings.Join(args, " ")
- }
- err = fmt.Errorf(stderr)
- }
- return stdout, err
-}
-// Run the given git command and return its stdout, or an error if the command fails.
-func (repo *GitRepo) runGitCommand(args ...string) (string, error) {
- return repo.runGitCommandWithStdin(nil, args...)
+ keyring Keyring
}
// 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, clockLoaders []ClockLoader) (*GitRepo, error) {
+ k, err := defaultKeyring()
+ if err != nil {
+ return nil, err
+ }
+
repo := &GitRepo{
- path: path,
- clocks: make(map[string]lamport.Clock),
+ gitCli: gitCli{path: path},
+ path: path,
+ clocks: make(map[string]lamport.Clock),
+ keyring: k,
}
// Check the repo and retrieve the root path
@@ -100,6 +56,7 @@ func NewGitRepo(path string, clockLoaders []ClockLoader) (*GitRepo, error) {
// Fix the path to be sure we are at the root
repo.path = stdout
+ repo.gitCli.path = stdout
for _, loader := range clockLoaders {
allExist := true
@@ -123,6 +80,7 @@ func NewGitRepo(path string, clockLoaders []ClockLoader) (*GitRepo, error) {
// InitGitRepo create a new empty git repo at the given path
func InitGitRepo(path string) (*GitRepo, error) {
repo := &GitRepo{
+ gitCli: gitCli{path: path},
path: path + "/.git",
clocks: make(map[string]lamport.Clock),
}
@@ -138,6 +96,7 @@ 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{
+ gitCli: gitCli{path: path},
path: path,
clocks: make(map[string]lamport.Clock),
}
@@ -150,6 +109,26 @@ func InitBareGitRepo(path string) (*GitRepo, error) {
return repo, nil
}
+// LocalConfig give access to the repository scoped configuration
+func (repo *GitRepo) LocalConfig() Config {
+ return newGitConfig(repo.gitCli, false)
+}
+
+// GlobalConfig give access to the global scoped configuration
+func (repo *GitRepo) GlobalConfig() Config {
+ return newGitConfig(repo.gitCli, true)
+}
+
+// AnyConfig give access to a merged local/global configuration
+func (repo *GitRepo) AnyConfig() ConfigRead {
+ return mergeConfig(repo.LocalConfig(), repo.GlobalConfig())
+}
+
+// Keyring give access to a user-wide storage for secrets
+func (repo *GitRepo) Keyring() Keyring {
+ return repo.keyring
+}
+
// GetPath returns the path to the repo.
func (repo *GitRepo) GetPath() string {
return repo.path
@@ -290,8 +269,8 @@ func (repo *GitRepo) RemoveRef(ref string) error {
}
// ListRefs will return a list of Git ref matching the given refspec
-func (repo *GitRepo) ListRefs(refspec string) ([]string, error) {
- stdout, err := repo.runGitCommand("for-each-ref", "--format=%(refname)", refspec)
+func (repo *GitRepo) ListRefs(refPrefix string) ([]string, error) {
+ stdout, err := repo.runGitCommand("for-each-ref", "--format=%(refname)", refPrefix)
if err != nil {
return nil, err
diff --git a/repository/git_cli.go b/repository/git_cli.go
new file mode 100644
index 00000000..085b1cda
--- /dev/null
+++ b/repository/git_cli.go
@@ -0,0 +1,56 @@
+package repository
+
+import (
+ "bytes"
+ "fmt"
+ "io"
+ "os/exec"
+ "strings"
+)
+
+// gitCli is a helper to launch CLI git commands
+type gitCli struct {
+ path string
+}
+
+// Run the given git command with the given I/O reader/writers, returning an error if it fails.
+func (cli gitCli) runGitCommandWithIO(stdin io.Reader, stdout, stderr io.Writer, args ...string) error {
+ // make sure that the working directory for the command
+ // always exist, in particular when running "git init".
+ path := strings.TrimSuffix(cli.path, ".git")
+
+ // fmt.Printf("[%s] Running git %s\n", path, strings.Join(args, " "))
+
+ cmd := exec.Command("git", args...)
+ cmd.Dir = path
+ cmd.Stdin = stdin
+ cmd.Stdout = stdout
+ cmd.Stderr = stderr
+
+ return cmd.Run()
+}
+
+// Run the given git command and return its stdout, or an error if the command fails.
+func (cli gitCli) runGitCommandRaw(stdin io.Reader, args ...string) (string, string, error) {
+ var stdout bytes.Buffer
+ var stderr bytes.Buffer
+ err := cli.runGitCommandWithIO(stdin, &stdout, &stderr, args...)
+ return strings.TrimSpace(stdout.String()), strings.TrimSpace(stderr.String()), err
+}
+
+// Run the given git command and return its stdout, or an error if the command fails.
+func (cli gitCli) runGitCommandWithStdin(stdin io.Reader, args ...string) (string, error) {
+ stdout, stderr, err := cli.runGitCommandRaw(stdin, args...)
+ if err != nil {
+ if stderr == "" {
+ stderr = "Error running git command: " + strings.Join(args, " ")
+ }
+ err = fmt.Errorf(stderr)
+ }
+ return stdout, err
+}
+
+// Run the given git command and return its stdout, or an error if the command fails.
+func (cli gitCli) runGitCommand(args ...string) (string, error) {
+ return cli.runGitCommandWithStdin(nil, args...)
+}
diff --git a/repository/git_config.go b/repository/git_config.go
index 987cf195..b46cc69b 100644
--- a/repository/git_config.go
+++ b/repository/git_config.go
@@ -14,24 +14,24 @@ import (
var _ Config = &gitConfig{}
type gitConfig struct {
- repo *GitRepo
+ cli gitCli
localityFlag string
}
-func newGitConfig(repo *GitRepo, global bool) *gitConfig {
+func newGitConfig(cli gitCli, global bool) *gitConfig {
localityFlag := "--local"
if global {
localityFlag = "--global"
}
return &gitConfig{
- repo: repo,
+ cli: cli,
localityFlag: localityFlag,
}
}
// StoreString store a single key/value pair in the config of the repo
func (gc *gitConfig) StoreString(key string, value string) error {
- _, err := gc.repo.runGitCommand("config", gc.localityFlag, "--replace-all", key, value)
+ _, err := gc.cli.runGitCommand("config", gc.localityFlag, "--replace-all", key, value)
return err
}
@@ -45,7 +45,7 @@ func (gc *gitConfig) StoreTimestamp(key string, value time.Time) error {
// ReadAll read all key/value pair matching the key prefix
func (gc *gitConfig) ReadAll(keyPrefix string) (map[string]string, error) {
- stdout, err := gc.repo.runGitCommand("config", gc.localityFlag, "--includes", "--get-regexp", keyPrefix)
+ stdout, err := gc.cli.runGitCommand("config", gc.localityFlag, "--includes", "--get-regexp", keyPrefix)
// / \
// / ! \
@@ -74,7 +74,7 @@ func (gc *gitConfig) ReadAll(keyPrefix string) (map[string]string, error) {
}
func (gc *gitConfig) ReadString(key string) (string, error) {
- stdout, err := gc.repo.runGitCommand("config", gc.localityFlag, "--includes", "--get-all", key)
+ stdout, err := gc.cli.runGitCommand("config", gc.localityFlag, "--includes", "--get-all", key)
// / \
// / ! \
@@ -116,12 +116,12 @@ func (gc *gitConfig) ReadTimestamp(key string) (time.Time, error) {
}
func (gc *gitConfig) rmSection(keyPrefix string) error {
- _, err := gc.repo.runGitCommand("config", gc.localityFlag, "--remove-section", keyPrefix)
+ _, err := gc.cli.runGitCommand("config", gc.localityFlag, "--remove-section", keyPrefix)
return err
}
func (gc *gitConfig) unsetAll(keyPrefix string) error {
- _, err := gc.repo.runGitCommand("config", gc.localityFlag, "--unset-all", keyPrefix)
+ _, err := gc.cli.runGitCommand("config", gc.localityFlag, "--unset-all", keyPrefix)
return err
}
@@ -180,7 +180,7 @@ func (gc *gitConfig) RemoveAll(keyPrefix string) error {
}
func (gc *gitConfig) gitVersion() (*semver.Version, error) {
- versionOut, err := gc.repo.runGitCommand("version")
+ versionOut, err := gc.cli.runGitCommand("version")
if err != nil {
return nil, err
}
diff --git a/repository/git_testing.go b/repository/git_testing.go
index 5ae4ccc9..7d40bf1f 100644
--- a/repository/git_testing.go
+++ b/repository/git_testing.go
@@ -3,6 +3,8 @@ package repository
import (
"io/ioutil"
"log"
+
+ "github.com/99designs/keyring"
)
// This is intended for testing only
@@ -34,7 +36,11 @@ func CreateTestRepo(bare bool) TestedRepo {
log.Fatal("failed to set user.email for test repository: ", err)
}
- return repo
+ // make sure we use a mock keyring for testing to not interact with the global system
+ return &replaceKeyring{
+ TestedRepo: repo,
+ keyring: keyring.NewArrayKeyring(nil),
+ }
}
func SetupReposAndRemote() (repoA, repoB, remote TestedRepo) {
@@ -56,3 +62,13 @@ func SetupReposAndRemote() (repoA, repoB, remote TestedRepo) {
return repoA, repoB, remote
}
+
+// replaceKeyring allow to replace the Keyring of the underlying repo
+type replaceKeyring struct {
+ TestedRepo
+ keyring Keyring
+}
+
+func (rk replaceKeyring) Keyring() Keyring {
+ return rk.keyring
+}
diff --git a/repository/gogit.go b/repository/gogit.go
new file mode 100644
index 00000000..09f714ea
--- /dev/null
+++ b/repository/gogit.go
@@ -0,0 +1,615 @@
+package repository
+
+import (
+ "bytes"
+ "fmt"
+ "io/ioutil"
+ "os"
+ stdpath "path"
+ "path/filepath"
+ "strings"
+ "sync"
+ "time"
+
+ gogit "github.com/go-git/go-git/v5"
+ "github.com/go-git/go-git/v5/config"
+ "github.com/go-git/go-git/v5/plumbing"
+ "github.com/go-git/go-git/v5/plumbing/filemode"
+ "github.com/go-git/go-git/v5/plumbing/object"
+
+ "github.com/MichaelMure/git-bug/util/lamport"
+)
+
+var _ ClockedRepo = &GoGitRepo{}
+
+type GoGitRepo struct {
+ r *gogit.Repository
+ path string
+
+ clocksMutex sync.Mutex
+ clocks map[string]lamport.Clock
+
+ keyring Keyring
+}
+
+func NewGoGitRepo(path string, clockLoaders []ClockLoader) (*GoGitRepo, error) {
+ path, err := detectGitPath(path)
+ if err != nil {
+ return nil, err
+ }
+
+ r, err := gogit.PlainOpen(path)
+ if err != nil {
+ return nil, err
+ }
+
+ k, err := defaultKeyring()
+ if err != nil {
+ return nil, err
+ }
+
+ repo := &GoGitRepo{
+ r: r,
+ path: path,
+ clocks: make(map[string]lamport.Clock),
+ keyring: k,
+ }
+
+ for _, loader := range clockLoaders {
+ allExist := true
+ for _, name := range loader.Clocks {
+ if _, err := repo.getClock(name); err != nil {
+ allExist = false
+ }
+ }
+
+ if !allExist {
+ err = loader.Witnesser(repo)
+ if err != nil {
+ return nil, err
+ }
+ }
+ }
+
+ return repo, nil
+}
+
+func detectGitPath(path string) (string, error) {
+ // normalize the path
+ path, err := filepath.Abs(path)
+ if err != nil {
+ return "", err
+ }
+
+ for {
+ fi, err := os.Stat(stdpath.Join(path, ".git"))
+ if err == nil {
+ if !fi.IsDir() {
+ return "", fmt.Errorf(".git exist but is not a directory")
+ }
+ return stdpath.Join(path, ".git"), nil
+ }
+ if !os.IsNotExist(err) {
+ // unknown error
+ return "", err
+ }
+
+ // detect bare repo
+ ok, err := isGitDir(path)
+ if err != nil {
+ return "", err
+ }
+ if ok {
+ return path, nil
+ }
+
+ if parent := filepath.Dir(path); parent == path {
+ return "", fmt.Errorf(".git not found")
+ } else {
+ path = parent
+ }
+ }
+}
+
+func isGitDir(path string) (bool, error) {
+ markers := []string{"HEAD", "objects", "refs"}
+
+ for _, marker := range markers {
+ _, err := os.Stat(stdpath.Join(path, marker))
+ if err == nil {
+ continue
+ }
+ if !os.IsNotExist(err) {
+ // unknown error
+ return false, err
+ } else {
+ return false, nil
+ }
+ }
+
+ return true, nil
+}
+
+// InitGoGitRepo create a new empty git repo at the given path
+func InitGoGitRepo(path string) (*GoGitRepo, error) {
+ r, err := gogit.PlainInit(path, false)
+ if err != nil {
+ return nil, err
+ }
+
+ return &GoGitRepo{
+ r: r,
+ path: path + "/.git",
+ clocks: make(map[string]lamport.Clock),
+ }, nil
+}
+
+// InitBareGoGitRepo create a new --bare empty git repo at the given path
+func InitBareGoGitRepo(path string) (*GoGitRepo, error) {
+ r, err := gogit.PlainInit(path, true)
+ if err != nil {
+ return nil, err
+ }
+
+ return &GoGitRepo{
+ r: r,
+ path: path,
+ clocks: make(map[string]lamport.Clock),
+ }, nil
+}
+
+// LocalConfig give access to the repository scoped configuration
+func (repo *GoGitRepo) LocalConfig() Config {
+ return newGoGitLocalConfig(repo.r)
+}
+
+// GlobalConfig give access to the global scoped configuration
+func (repo *GoGitRepo) GlobalConfig() Config {
+ // TODO: replace that with go-git native implementation once it's supported
+ // see: https://github.com/go-git/go-git
+ // see: https://github.com/src-d/go-git/issues/760
+ return newGoGitGlobalConfig(repo.r)
+}
+
+// AnyConfig give access to a merged local/global configuration
+func (repo *GoGitRepo) AnyConfig() ConfigRead {
+ return mergeConfig(repo.LocalConfig(), repo.GlobalConfig())
+}
+
+// Keyring give access to a user-wide storage for secrets
+func (repo *GoGitRepo) Keyring() Keyring {
+ return repo.keyring
+}
+
+// GetPath returns the path to the repo.
+func (repo *GoGitRepo) GetPath() string {
+ return repo.path
+}
+
+// GetUserName returns the name the the user has used to configure git
+func (repo *GoGitRepo) GetUserName() (string, error) {
+ cfg, err := repo.r.Config()
+ if err != nil {
+ return "", err
+ }
+
+ return cfg.User.Name, nil
+}
+
+// GetUserEmail returns the email address that the user has used to configure git.
+func (repo *GoGitRepo) GetUserEmail() (string, error) {
+ cfg, err := repo.r.Config()
+ if err != nil {
+ return "", err
+ }
+
+ return cfg.User.Email, nil
+}
+
+// GetCoreEditor returns the name of the editor that the user has used to configure git.
+func (repo *GoGitRepo) GetCoreEditor() (string, error) {
+ // See https://git-scm.com/docs/git-var
+ // The order of preference is the $GIT_EDITOR environment variable, then core.editor configuration, then $VISUAL, then $EDITOR, and then the default chosen at compile time, which is usually vi.
+
+ if val, ok := os.LookupEnv("GIT_EDITOR"); ok {
+ return val, nil
+ }
+
+ val, err := repo.AnyConfig().ReadString("core.editor")
+ if err == nil && val != "" {
+ return val, nil
+ }
+ if err != nil && err != ErrNoConfigEntry {
+ return "", err
+ }
+
+ if val, ok := os.LookupEnv("VISUAL"); ok {
+ return val, nil
+ }
+
+ if val, ok := os.LookupEnv("EDITOR"); ok {
+ return val, nil
+ }
+
+ return "vi", nil
+}
+
+// GetRemotes returns the configured remotes repositories.
+func (repo *GoGitRepo) GetRemotes() (map[string]string, error) {
+ cfg, err := repo.r.Config()
+ if err != nil {
+ return nil, err
+ }
+
+ result := make(map[string]string, len(cfg.Remotes))
+ for name, remote := range cfg.Remotes {
+ if len(remote.URLs) > 0 {
+ result[name] = remote.URLs[0]
+ }
+ }
+
+ return result, nil
+}
+
+// FetchRefs fetch git refs from a remote
+func (repo *GoGitRepo) FetchRefs(remote string, refSpec string) (string, error) {
+ buf := bytes.NewBuffer(nil)
+
+ err := repo.r.Fetch(&gogit.FetchOptions{
+ RemoteName: remote,
+ RefSpecs: []config.RefSpec{config.RefSpec(refSpec)},
+ Progress: buf,
+ })
+ if err != nil {
+ return "", err
+ }
+
+ return buf.String(), nil
+}
+
+// PushRefs push git refs to a remote
+func (repo *GoGitRepo) PushRefs(remote string, refSpec string) (string, error) {
+ buf := bytes.NewBuffer(nil)
+
+ err := repo.r.Push(&gogit.PushOptions{
+ RemoteName: remote,
+ RefSpecs: []config.RefSpec{config.RefSpec(refSpec)},
+ Progress: buf,
+ })
+ if err != nil {
+ return "", err
+ }
+
+ return buf.String(), nil
+}
+
+// StoreData will store arbitrary data and return the corresponding hash
+func (repo *GoGitRepo) StoreData(data []byte) (Hash, error) {
+ obj := repo.r.Storer.NewEncodedObject()
+ obj.SetType(plumbing.BlobObject)
+
+ w, err := obj.Writer()
+ if err != nil {
+ return "", err
+ }
+
+ _, err = w.Write(data)
+ if err != nil {
+ return "", err
+ }
+
+ h, err := repo.r.Storer.SetEncodedObject(obj)
+ if err != nil {
+ return "", err
+ }
+
+ return Hash(h.String()), nil
+}
+
+// ReadData will attempt to read arbitrary data from the given hash
+func (repo *GoGitRepo) ReadData(hash Hash) ([]byte, error) {
+ obj, err := repo.r.BlobObject(plumbing.NewHash(hash.String()))
+ if err != nil {
+ return nil, err
+ }
+
+ r, err := obj.Reader()
+ if err != nil {
+ return nil, err
+ }
+
+ return ioutil.ReadAll(r)
+}
+
+// StoreTree will store a mapping key-->Hash as a Git tree
+func (repo *GoGitRepo) StoreTree(mapping []TreeEntry) (Hash, error) {
+ var tree object.Tree
+
+ for _, entry := range mapping {
+ mode := filemode.Regular
+ if entry.ObjectType == Tree {
+ mode = filemode.Dir
+ }
+
+ tree.Entries = append(tree.Entries, object.TreeEntry{
+ Name: entry.Name,
+ Mode: mode,
+ Hash: plumbing.NewHash(entry.Hash.String()),
+ })
+ }
+
+ obj := repo.r.Storer.NewEncodedObject()
+ obj.SetType(plumbing.TreeObject)
+ err := tree.Encode(obj)
+ if err != nil {
+ return "", err
+ }
+
+ hash, err := repo.r.Storer.SetEncodedObject(obj)
+ if err != nil {
+ return "", err
+ }
+
+ return Hash(hash.String()), nil
+}
+
+// ReadTree will return the list of entries in a Git tree
+func (repo *GoGitRepo) ReadTree(hash Hash) ([]TreeEntry, error) {
+ h := plumbing.NewHash(hash.String())
+
+ // the given hash could be a tree or a commit
+ obj, err := repo.r.Storer.EncodedObject(plumbing.AnyObject, h)
+ if err != nil {
+ return nil, err
+ }
+
+ var tree *object.Tree
+ switch obj.Type() {
+ case plumbing.TreeObject:
+ tree, err = object.DecodeTree(repo.r.Storer, obj)
+ case plumbing.CommitObject:
+ var commit *object.Commit
+ commit, err = object.DecodeCommit(repo.r.Storer, obj)
+ if err != nil {
+ return nil, err
+ }
+ tree, err = commit.Tree()
+ default:
+ return nil, fmt.Errorf("given hash is not a tree")
+ }
+ if err != nil {
+ return nil, err
+ }
+
+ treeEntries := make([]TreeEntry, len(tree.Entries))
+ for i, entry := range tree.Entries {
+ objType := Blob
+ if entry.Mode == filemode.Dir {
+ objType = Tree
+ }
+
+ treeEntries[i] = TreeEntry{
+ ObjectType: objType,
+ Hash: Hash(entry.Hash.String()),
+ Name: entry.Name,
+ }
+ }
+
+ return treeEntries, nil
+}
+
+// StoreCommit will store a Git commit with the given Git tree
+func (repo *GoGitRepo) StoreCommit(treeHash Hash) (Hash, error) {
+ return repo.StoreCommitWithParent(treeHash, "")
+}
+
+// StoreCommit will store a Git commit with the given Git tree
+func (repo *GoGitRepo) StoreCommitWithParent(treeHash Hash, parent Hash) (Hash, error) {
+ cfg, err := repo.r.Config()
+ if err != nil {
+ return "", err
+ }
+
+ commit := object.Commit{
+ Author: object.Signature{
+ cfg.Author.Name,
+ cfg.Author.Email,
+ time.Now(),
+ },
+ Committer: object.Signature{
+ cfg.Committer.Name,
+ cfg.Committer.Email,
+ time.Now(),
+ },
+ Message: "",
+ TreeHash: plumbing.NewHash(treeHash.String()),
+ }
+
+ if parent != "" {
+ commit.ParentHashes = []plumbing.Hash{plumbing.NewHash(parent.String())}
+ }
+
+ obj := repo.r.Storer.NewEncodedObject()
+ obj.SetType(plumbing.CommitObject)
+ err = commit.Encode(obj)
+ if err != nil {
+ return "", err
+ }
+
+ hash, err := repo.r.Storer.SetEncodedObject(obj)
+ if err != nil {
+ return "", err
+ }
+
+ return Hash(hash.String()), nil
+}
+
+// GetTreeHash return the git tree hash referenced in a commit
+func (repo *GoGitRepo) GetTreeHash(commit Hash) (Hash, error) {
+ obj, err := repo.r.CommitObject(plumbing.NewHash(commit.String()))
+ if err != nil {
+ return "", err
+ }
+
+ return Hash(obj.TreeHash.String()), nil
+}
+
+// FindCommonAncestor will return the last common ancestor of two chain of commit
+func (repo *GoGitRepo) FindCommonAncestor(commit1 Hash, commit2 Hash) (Hash, error) {
+ obj1, err := repo.r.CommitObject(plumbing.NewHash(commit1.String()))
+ if err != nil {
+ return "", err
+ }
+ obj2, err := repo.r.CommitObject(plumbing.NewHash(commit2.String()))
+ if err != nil {
+ return "", err
+ }
+
+ commits, err := obj1.MergeBase(obj2)
+ if err != nil {
+ return "", err
+ }
+
+ return Hash(commits[0].Hash.String()), nil
+}
+
+// UpdateRef will create or update a Git reference
+func (repo *GoGitRepo) UpdateRef(ref string, hash Hash) error {
+ return repo.r.Storer.SetReference(plumbing.NewHashReference(plumbing.ReferenceName(ref), plumbing.NewHash(hash.String())))
+}
+
+// RemoveRef will remove a Git reference
+func (repo *GoGitRepo) RemoveRef(ref string) error {
+ return repo.r.Storer.RemoveReference(plumbing.ReferenceName(ref))
+}
+
+// ListRefs will return a list of Git ref matching the given refspec
+func (repo *GoGitRepo) ListRefs(refPrefix string) ([]string, error) {
+ refIter, err := repo.r.References()
+ if err != nil {
+ return nil, err
+ }
+
+ refs := make([]string, 0)
+
+ err = refIter.ForEach(func(ref *plumbing.Reference) error {
+ if strings.HasPrefix(ref.Name().String(), refPrefix) {
+ refs = append(refs, ref.Name().String())
+ }
+ return nil
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ return refs, nil
+}
+
+// RefExist will check if a reference exist in Git
+func (repo *GoGitRepo) RefExist(ref string) (bool, error) {
+ _, err := repo.r.Reference(plumbing.ReferenceName(ref), false)
+ if err == nil {
+ return true, nil
+ } else if err == plumbing.ErrReferenceNotFound {
+ return false, nil
+ }
+ return false, err
+}
+
+// CopyRef will create a new reference with the same value as another one
+func (repo *GoGitRepo) CopyRef(source string, dest string) error {
+ r, err := repo.r.Reference(plumbing.ReferenceName(source), false)
+ if err != nil {
+ return err
+ }
+ return repo.r.Storer.SetReference(plumbing.NewHashReference(plumbing.ReferenceName(dest), r.Hash()))
+}
+
+// ListCommits will return the list of tree hashes of a ref, in chronological order
+func (repo *GoGitRepo) ListCommits(ref string) ([]Hash, error) {
+ r, err := repo.r.Reference(plumbing.ReferenceName(ref), false)
+ if err != nil {
+ return nil, err
+ }
+
+ commit, err := repo.r.CommitObject(r.Hash())
+ if err != nil {
+ return nil, err
+ }
+ hashes := []Hash{Hash(commit.Hash.String())}
+
+ for {
+ commit, err = commit.Parent(0)
+ if err == object.ErrParentNotFound {
+ break
+ }
+ if err != nil {
+ return nil, err
+ }
+
+ if commit.NumParents() > 1 {
+ return nil, fmt.Errorf("multiple parents")
+ }
+
+ hashes = append([]Hash{Hash(commit.Hash.String())}, hashes...)
+ }
+
+ return hashes, nil
+}
+
+// GetOrCreateClock return a Lamport clock stored in the Repo.
+// If the clock doesn't exist, it's created.
+func (repo *GoGitRepo) GetOrCreateClock(name string) (lamport.Clock, error) {
+ c, err := repo.getClock(name)
+ if err == nil {
+ return c, nil
+ }
+ if err != ErrClockNotExist {
+ return nil, err
+ }
+
+ repo.clocksMutex.Lock()
+ defer repo.clocksMutex.Unlock()
+
+ p := stdpath.Join(repo.path, clockPath, name+"-clock")
+
+ c, err = lamport.NewPersistedClock(p)
+ if err != nil {
+ return nil, err
+ }
+
+ repo.clocks[name] = c
+ return c, nil
+}
+
+func (repo *GoGitRepo) getClock(name string) (lamport.Clock, error) {
+ repo.clocksMutex.Lock()
+ defer repo.clocksMutex.Unlock()
+
+ if c, ok := repo.clocks[name]; ok {
+ return c, nil
+ }
+
+ p := stdpath.Join(repo.path, clockPath, name+"-clock")
+
+ 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
+}
+
+// AddRemote add a new remote to the repository
+// Not in the interface because it's only used for testing
+func (repo *GoGitRepo) AddRemote(name string, url string) error {
+ _, err := repo.r.CreateRemote(&config.RemoteConfig{
+ Name: name,
+ URLs: []string{url},
+ })
+
+ return err
+}
diff --git a/repository/gogit_config.go b/repository/gogit_config.go
new file mode 100644
index 00000000..7812de76
--- /dev/null
+++ b/repository/gogit_config.go
@@ -0,0 +1,235 @@
+package repository
+
+import (
+ "fmt"
+ "strconv"
+ "strings"
+ "time"
+
+ gogit "github.com/go-git/go-git/v5"
+ "github.com/go-git/go-git/v5/config"
+)
+
+var _ Config = &goGitConfig{}
+
+type goGitConfig struct {
+ ConfigRead
+ ConfigWrite
+}
+
+func newGoGitLocalConfig(repo *gogit.Repository) *goGitConfig {
+ return &goGitConfig{
+ ConfigRead: &goGitConfigReader{getConfig: repo.Config},
+ ConfigWrite: &goGitConfigWriter{repo: repo},
+ }
+}
+
+func newGoGitGlobalConfig(repo *gogit.Repository) *goGitConfig {
+ return &goGitConfig{
+ ConfigRead: &goGitConfigReader{getConfig: func() (*config.Config, error) {
+ return config.LoadConfig(config.GlobalScope)
+ }},
+ ConfigWrite: &configPanicWriter{},
+ }
+}
+
+var _ ConfigRead = &goGitConfigReader{}
+
+type goGitConfigReader struct {
+ getConfig func() (*config.Config, error)
+}
+
+func (cr *goGitConfigReader) ReadAll(keyPrefix string) (map[string]string, error) {
+ cfg, err := cr.getConfig()
+ if err != nil {
+ return nil, err
+ }
+
+ split := strings.Split(keyPrefix, ".")
+ result := make(map[string]string)
+
+ switch {
+ case keyPrefix == "":
+ for _, section := range cfg.Raw.Sections {
+ for _, option := range section.Options {
+ result[fmt.Sprintf("%s.%s", section.Name, option.Key)] = option.Value
+ }
+ for _, subsection := range section.Subsections {
+ for _, option := range subsection.Options {
+ result[fmt.Sprintf("%s.%s.%s", section.Name, subsection.Name, option.Key)] = option.Value
+ }
+ }
+ }
+ case len(split) == 1:
+ if !cfg.Raw.HasSection(split[0]) {
+ return nil, nil
+ }
+ section := cfg.Raw.Section(split[0])
+ for _, option := range section.Options {
+ result[fmt.Sprintf("%s.%s", section.Name, option.Key)] = option.Value
+ }
+ for _, subsection := range section.Subsections {
+ for _, option := range subsection.Options {
+ result[fmt.Sprintf("%s.%s.%s", section.Name, subsection.Name, option.Key)] = option.Value
+ }
+ }
+ default:
+ if !cfg.Raw.HasSection(split[0]) {
+ return nil, nil
+ }
+ section := cfg.Raw.Section(split[0])
+ rest := strings.Join(split[1:], ".")
+ for _, subsection := range section.Subsections {
+ if strings.HasPrefix(subsection.Name, rest) {
+ for _, option := range subsection.Options {
+ result[fmt.Sprintf("%s.%s.%s", section.Name, subsection.Name, option.Key)] = option.Value
+ }
+ }
+ }
+ }
+
+ return result, nil
+}
+
+func (cr *goGitConfigReader) ReadBool(key string) (bool, error) {
+ val, err := cr.ReadString(key)
+ if err != nil {
+ return false, err
+ }
+
+ return strconv.ParseBool(val)
+}
+
+func (cr *goGitConfigReader) ReadString(key string) (string, error) {
+ cfg, err := cr.getConfig()
+ if err != nil {
+ return "", err
+ }
+
+ split := strings.Split(key, ".")
+
+ if len(split) <= 1 {
+ return "", fmt.Errorf("invalid key")
+ }
+
+ sectionName := split[0]
+ if !cfg.Raw.HasSection(sectionName) {
+ return "", ErrNoConfigEntry
+ }
+ section := cfg.Raw.Section(sectionName)
+
+ switch {
+ case len(split) == 2:
+ optionName := split[1]
+ if !section.HasOption(optionName) {
+ return "", ErrNoConfigEntry
+ }
+ if len(section.OptionAll(optionName)) > 1 {
+ return "", ErrMultipleConfigEntry
+ }
+ return section.Option(optionName), nil
+ default:
+ subsectionName := strings.Join(split[1:len(split)-2], ".")
+ optionName := split[len(split)-1]
+ if !section.HasSubsection(subsectionName) {
+ return "", ErrNoConfigEntry
+ }
+ subsection := section.Subsection(subsectionName)
+ if !subsection.HasOption(optionName) {
+ return "", ErrNoConfigEntry
+ }
+ if len(subsection.OptionAll(optionName)) > 1 {
+ return "", ErrMultipleConfigEntry
+ }
+ return subsection.Option(optionName), nil
+ }
+}
+
+func (cr *goGitConfigReader) ReadTimestamp(key string) (time.Time, error) {
+ value, err := cr.ReadString(key)
+ if err != nil {
+ return time.Time{}, err
+ }
+ return ParseTimestamp(value)
+}
+
+var _ ConfigWrite = &goGitConfigWriter{}
+
+// Only works for the local config as go-git only support that
+type goGitConfigWriter struct {
+ repo *gogit.Repository
+}
+
+func (cw *goGitConfigWriter) StoreString(key, value string) error {
+ cfg, err := cw.repo.Config()
+ if err != nil {
+ return err
+ }
+
+ split := strings.Split(key, ".")
+
+ switch {
+ case len(split) <= 1:
+ return fmt.Errorf("invalid key")
+ case len(split) == 2:
+ cfg.Raw.Section(split[0]).SetOption(split[1], value)
+ default:
+ section := split[0]
+ subsection := strings.Join(split[1:len(split)-1], ".")
+ option := split[len(split)-1]
+ cfg.Raw.Section(section).Subsection(subsection).SetOption(option, value)
+ }
+
+ return cw.repo.SetConfig(cfg)
+}
+
+func (cw *goGitConfigWriter) StoreTimestamp(key string, value time.Time) error {
+ return cw.StoreString(key, strconv.Itoa(int(value.Unix())))
+}
+
+func (cw *goGitConfigWriter) StoreBool(key string, value bool) error {
+ return cw.StoreString(key, strconv.FormatBool(value))
+}
+
+func (cw *goGitConfigWriter) RemoveAll(keyPrefix string) error {
+ cfg, err := cw.repo.Config()
+ if err != nil {
+ return err
+ }
+
+ split := strings.Split(keyPrefix, ".")
+
+ switch {
+ case keyPrefix == "":
+ cfg.Raw.Sections = nil
+ // warning: this does not actually remove everything as go-git config hold
+ // some entries in multiple places (cfg.User ...)
+ case len(split) == 1:
+ if cfg.Raw.HasSection(split[0]) {
+ cfg.Raw.RemoveSection(split[0])
+ } else {
+ return fmt.Errorf("invalid key prefix")
+ }
+ default:
+ if !cfg.Raw.HasSection(split[0]) {
+ return fmt.Errorf("invalid key prefix")
+ }
+ section := cfg.Raw.Section(split[0])
+ rest := strings.Join(split[1:], ".")
+
+ ok := false
+ if section.HasSubsection(rest) {
+ section.RemoveSubsection(rest)
+ ok = true
+ }
+ if section.HasOption(rest) {
+ section.RemoveOption(rest)
+ ok = true
+ }
+ if !ok {
+ return fmt.Errorf("invalid key prefix")
+ }
+ }
+
+ return cw.repo.SetConfig(cfg)
+}
diff --git a/repository/gogit_test.go b/repository/gogit_test.go
new file mode 100644
index 00000000..fba990d3
--- /dev/null
+++ b/repository/gogit_test.go
@@ -0,0 +1,68 @@
+package repository
+
+import (
+ "io/ioutil"
+ "os"
+ "path"
+ "path/filepath"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestNewGoGitRepo(t *testing.T) {
+ // Plain
+ plainRoot, err := ioutil.TempDir("", "")
+ require.NoError(t, err)
+ defer os.RemoveAll(plainRoot)
+
+ _, err = InitGoGitRepo(plainRoot)
+ require.NoError(t, err)
+ plainGitDir := path.Join(plainRoot, ".git")
+
+ // Bare
+ bareRoot, err := ioutil.TempDir("", "")
+ require.NoError(t, err)
+ defer os.RemoveAll(bareRoot)
+
+ _, err = InitBareGoGitRepo(bareRoot)
+ require.NoError(t, err)
+ bareGitDir := bareRoot
+
+ tests := []struct {
+ inPath string
+ outPath string
+ err bool
+ }{
+ // errors
+ {"/", "", true},
+ // parent dir of a repo
+ {filepath.Dir(plainRoot), "", true},
+
+ // Plain repo
+ {plainRoot, plainGitDir, false},
+ {plainGitDir, plainGitDir, false},
+ {path.Join(plainGitDir, "objects"), plainGitDir, false},
+
+ // Bare repo
+ {bareRoot, bareGitDir, false},
+ {bareGitDir, bareGitDir, false},
+ {path.Join(bareGitDir, "objects"), bareGitDir, false},
+ }
+
+ for i, tc := range tests {
+ r, err := NewGoGitRepo(tc.inPath, nil)
+
+ if tc.err {
+ require.Error(t, err, i)
+ } else {
+ require.NoError(t, err, i)
+ assert.Equal(t, filepath.ToSlash(tc.outPath), filepath.ToSlash(r.GetPath()), i)
+ }
+ }
+}
+
+func TestGoGitRepo(t *testing.T) {
+ RepoTest(t, CreateGoGitTestRepo, CleanupTestRepos)
+}
diff --git a/repository/gogit_testing.go b/repository/gogit_testing.go
new file mode 100644
index 00000000..f20ff6be
--- /dev/null
+++ b/repository/gogit_testing.go
@@ -0,0 +1,58 @@
+package repository
+
+import (
+ "io/ioutil"
+ "log"
+)
+
+// This is intended for testing only
+
+func CreateGoGitTestRepo(bare bool) TestedRepo {
+ dir, err := ioutil.TempDir("", "")
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ var creator func(string) (*GoGitRepo, error)
+
+ if bare {
+ creator = InitBareGoGitRepo
+ } else {
+ creator = InitGoGitRepo
+ }
+
+ repo, err := creator(dir)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ config := repo.LocalConfig()
+ if err := config.StoreString("user.name", "testuser"); err != nil {
+ log.Fatal("failed to set user.name for test repository: ", err)
+ }
+ if err := config.StoreString("user.email", "testuser@example.com"); err != nil {
+ log.Fatal("failed to set user.email for test repository: ", err)
+ }
+
+ return repo
+}
+
+func SetupGoGitReposAndRemote() (repoA, repoB, remote TestedRepo) {
+ repoA = CreateGoGitTestRepo(false)
+ repoB = CreateGoGitTestRepo(false)
+ remote = CreateGoGitTestRepo(true)
+
+ remoteAddr := "file://" + remote.GetPath()
+
+ err := repoA.AddRemote("origin", remoteAddr)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ err = repoB.AddRemote("origin", remoteAddr)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ return repoA, repoB, remote
+}
diff --git a/repository/keyring.go b/repository/keyring.go
new file mode 100644
index 00000000..f690b0b3
--- /dev/null
+++ b/repository/keyring.go
@@ -0,0 +1,50 @@
+package repository
+
+import (
+ "os"
+ "path"
+
+ "github.com/99designs/keyring"
+)
+
+type Item = keyring.Item
+
+var ErrKeyringKeyNotFound = keyring.ErrKeyNotFound
+
+// Keyring provides the uniform interface over the underlying backends
+type Keyring interface {
+ // Returns an Item matching the key or ErrKeyringKeyNotFound
+ Get(key string) (Item, error)
+ // Stores an Item on the keyring
+ Set(item Item) error
+ // Removes the item with matching key
+ Remove(key string) error
+ // Provides a slice of all keys stored on the keyring
+ Keys() ([]string, error)
+}
+
+func defaultKeyring() (Keyring, error) {
+ ucd, err := os.UserConfigDir()
+ if err != nil {
+ return nil, err
+ }
+
+ return keyring.Open(keyring.Config{
+ // only use the file backend until https://github.com/99designs/keyring/issues/74 is resolved
+ AllowedBackends: []keyring.BackendType{
+ keyring.FileBackend,
+ },
+
+ ServiceName: "git-bug",
+
+ // Fallback encrypted file
+ FileDir: path.Join(ucd, "git-bug", "keyring"),
+ // As we write the file in the user's config directory, this file should already be protected by the OS against
+ // other user's access. We actually don't terribly need to protect it further and a password prompt across all
+ // UI's would be a pain. Therefore we use here a constant password so the file will be unreadable by generic file
+ // scanners if the user's machine get compromised.
+ FilePasswordFunc: func(string) (string, error) {
+ return "git-bug", nil
+ },
+ })
+}
diff --git a/repository/mock_repo.go b/repository/mock_repo.go
index 576e984e..628939aa 100644
--- a/repository/mock_repo.go
+++ b/repository/mock_repo.go
@@ -5,6 +5,8 @@ import (
"fmt"
"strings"
+ "github.com/99designs/keyring"
+
"github.com/MichaelMure/git-bug/util/lamport"
)
@@ -13,85 +15,143 @@ var _ TestedRepo = &mockRepoForTest{}
// mockRepoForTest defines an instance of Repo that can be used for testing.
type mockRepoForTest struct {
- config *MemConfig
- globalConfig *MemConfig
- blobs map[Hash][]byte
- trees map[Hash]string
- commits map[Hash]commit
- refs map[string]Hash
- clocks map[string]lamport.Clock
-}
-
-type commit struct {
- treeHash Hash
- parent Hash
+ *mockRepoConfig
+ *mockRepoKeyring
+ *mockRepoCommon
+ *mockRepoData
+ *mockRepoClock
}
func NewMockRepoForTest() *mockRepoForTest {
return &mockRepoForTest{
- config: NewMemConfig(),
+ mockRepoConfig: NewMockRepoConfig(),
+ mockRepoKeyring: NewMockRepoKeyring(),
+ mockRepoCommon: NewMockRepoCommon(),
+ mockRepoData: NewMockRepoData(),
+ mockRepoClock: NewMockRepoClock(),
+ }
+}
+
+var _ RepoConfig = &mockRepoConfig{}
+
+type mockRepoConfig struct {
+ localConfig *MemConfig
+ globalConfig *MemConfig
+}
+
+func NewMockRepoConfig() *mockRepoConfig {
+ return &mockRepoConfig{
+ localConfig: NewMemConfig(),
globalConfig: NewMemConfig(),
- blobs: make(map[Hash][]byte),
- trees: make(map[Hash]string),
- commits: make(map[Hash]commit),
- refs: make(map[string]Hash),
- clocks: make(map[string]lamport.Clock),
}
}
// LocalConfig give access to the repository scoped configuration
-func (r *mockRepoForTest) LocalConfig() Config {
- return r.config
+func (r *mockRepoConfig) LocalConfig() Config {
+ return r.localConfig
}
// GlobalConfig give access to the git global configuration
-func (r *mockRepoForTest) GlobalConfig() Config {
+func (r *mockRepoConfig) GlobalConfig() Config {
return r.globalConfig
}
+// AnyConfig give access to a merged local/global configuration
+func (r *mockRepoConfig) AnyConfig() ConfigRead {
+ return mergeConfig(r.localConfig, r.globalConfig)
+}
+
+var _ RepoKeyring = &mockRepoKeyring{}
+
+type mockRepoKeyring struct {
+ keyring *keyring.ArrayKeyring
+}
+
+func NewMockRepoKeyring() *mockRepoKeyring {
+ return &mockRepoKeyring{
+ keyring: keyring.NewArrayKeyring(nil),
+ }
+}
+
+// Keyring give access to a user-wide storage for secrets
+func (r *mockRepoKeyring) Keyring() Keyring {
+ return r.keyring
+}
+
+var _ RepoCommon = &mockRepoCommon{}
+
+type mockRepoCommon struct{}
+
+func NewMockRepoCommon() *mockRepoCommon {
+ return &mockRepoCommon{}
+}
+
// GetPath returns the path to the repo.
-func (r *mockRepoForTest) GetPath() string {
+func (r *mockRepoCommon) GetPath() string {
return "~/mockRepo/"
}
-func (r *mockRepoForTest) GetUserName() (string, error) {
+func (r *mockRepoCommon) GetUserName() (string, error) {
return "René Descartes", nil
}
// GetUserEmail returns the email address that the user has used to configure git.
-func (r *mockRepoForTest) GetUserEmail() (string, error) {
+func (r *mockRepoCommon) GetUserEmail() (string, error) {
return "user@example.com", nil
}
// GetCoreEditor returns the name of the editor that the user has used to configure git.
-func (r *mockRepoForTest) GetCoreEditor() (string, error) {
+func (r *mockRepoCommon) GetCoreEditor() (string, error) {
return "vi", nil
}
// GetRemotes returns the configured remotes repositories.
-func (r *mockRepoForTest) GetRemotes() (map[string]string, error) {
+func (r *mockRepoCommon) GetRemotes() (map[string]string, error) {
return map[string]string{
"origin": "git://github.com/MichaelMure/git-bug",
}, nil
}
+var _ RepoData = &mockRepoData{}
+
+type commit struct {
+ treeHash Hash
+ parent Hash
+}
+
+type mockRepoData struct {
+ blobs map[Hash][]byte
+ trees map[Hash]string
+ commits map[Hash]commit
+ refs map[string]Hash
+}
+
+func NewMockRepoData() *mockRepoData {
+ return &mockRepoData{
+ blobs: make(map[Hash][]byte),
+ trees: make(map[Hash]string),
+ commits: make(map[Hash]commit),
+ refs: make(map[string]Hash),
+ }
+}
+
// PushRefs push git refs to a remote
-func (r *mockRepoForTest) PushRefs(remote string, refSpec string) (string, error) {
+func (r *mockRepoData) PushRefs(remote string, refSpec string) (string, error) {
return "", nil
}
-func (r *mockRepoForTest) FetchRefs(remote string, refSpec string) (string, error) {
+func (r *mockRepoData) FetchRefs(remote string, refSpec string) (string, error) {
return "", nil
}
-func (r *mockRepoForTest) StoreData(data []byte) (Hash, error) {
+func (r *mockRepoData) StoreData(data []byte) (Hash, error) {
rawHash := sha1.Sum(data)
hash := Hash(fmt.Sprintf("%x", rawHash))
r.blobs[hash] = data
return hash, nil
}
-func (r *mockRepoForTest) ReadData(hash Hash) ([]byte, error) {
+func (r *mockRepoData) ReadData(hash Hash) ([]byte, error) {
data, ok := r.blobs[hash]
if !ok {
@@ -101,7 +161,7 @@ func (r *mockRepoForTest) ReadData(hash Hash) ([]byte, error) {
return data, nil
}
-func (r *mockRepoForTest) StoreTree(entries []TreeEntry) (Hash, error) {
+func (r *mockRepoData) StoreTree(entries []TreeEntry) (Hash, error) {
buffer := prepareTreeEntries(entries)
rawHash := sha1.Sum(buffer.Bytes())
hash := Hash(fmt.Sprintf("%x", rawHash))
@@ -110,7 +170,7 @@ func (r *mockRepoForTest) StoreTree(entries []TreeEntry) (Hash, error) {
return hash, nil
}
-func (r *mockRepoForTest) StoreCommit(treeHash Hash) (Hash, error) {
+func (r *mockRepoData) StoreCommit(treeHash Hash) (Hash, error) {
rawHash := sha1.Sum([]byte(treeHash))
hash := Hash(fmt.Sprintf("%x", rawHash))
r.commits[hash] = commit{
@@ -119,7 +179,7 @@ func (r *mockRepoForTest) StoreCommit(treeHash Hash) (Hash, error) {
return hash, nil
}
-func (r *mockRepoForTest) StoreCommitWithParent(treeHash Hash, parent Hash) (Hash, error) {
+func (r *mockRepoData) StoreCommitWithParent(treeHash Hash, parent Hash) (Hash, error) {
rawHash := sha1.Sum([]byte(treeHash + parent))
hash := Hash(fmt.Sprintf("%x", rawHash))
r.commits[hash] = commit{
@@ -129,22 +189,22 @@ func (r *mockRepoForTest) StoreCommitWithParent(treeHash Hash, parent Hash) (Has
return hash, nil
}
-func (r *mockRepoForTest) UpdateRef(ref string, hash Hash) error {
+func (r *mockRepoData) UpdateRef(ref string, hash Hash) error {
r.refs[ref] = hash
return nil
}
-func (r *mockRepoForTest) RemoveRef(ref string) error {
+func (r *mockRepoData) RemoveRef(ref string) error {
delete(r.refs, ref)
return nil
}
-func (r *mockRepoForTest) RefExist(ref string) (bool, error) {
+func (r *mockRepoData) RefExist(ref string) (bool, error) {
_, exist := r.refs[ref]
return exist, nil
}
-func (r *mockRepoForTest) CopyRef(source string, dest string) error {
+func (r *mockRepoData) CopyRef(source string, dest string) error {
hash, exist := r.refs[source]
if !exist {
@@ -155,11 +215,11 @@ func (r *mockRepoForTest) CopyRef(source string, dest string) error {
return nil
}
-func (r *mockRepoForTest) ListRefs(refspec string) ([]string, error) {
+func (r *mockRepoData) ListRefs(refPrefix string) ([]string, error) {
var keys []string
for k := range r.refs {
- if strings.HasPrefix(k, refspec) {
+ if strings.HasPrefix(k, refPrefix) {
keys = append(keys, k)
}
}
@@ -167,7 +227,7 @@ func (r *mockRepoForTest) ListRefs(refspec string) ([]string, error) {
return keys, nil
}
-func (r *mockRepoForTest) ListCommits(ref string) ([]Hash, error) {
+func (r *mockRepoData) ListCommits(ref string) ([]Hash, error) {
var hashes []Hash
hash := r.refs[ref]
@@ -186,7 +246,7 @@ func (r *mockRepoForTest) ListCommits(ref string) ([]Hash, error) {
return hashes, nil
}
-func (r *mockRepoForTest) ReadTree(hash Hash) ([]TreeEntry, error) {
+func (r *mockRepoData) ReadTree(hash Hash) ([]TreeEntry, error) {
var data string
data, ok := r.trees[hash]
@@ -209,7 +269,7 @@ func (r *mockRepoForTest) ReadTree(hash Hash) ([]TreeEntry, error) {
return readTreeEntries(data)
}
-func (r *mockRepoForTest) FindCommonAncestor(hash1 Hash, hash2 Hash) (Hash, error) {
+func (r *mockRepoData) FindCommonAncestor(hash1 Hash, hash2 Hash) (Hash, error) {
ancestor1 := []Hash{hash1}
for hash1 != "" {
@@ -241,7 +301,7 @@ func (r *mockRepoForTest) FindCommonAncestor(hash1 Hash, hash2 Hash) (Hash, erro
}
}
-func (r *mockRepoForTest) GetTreeHash(commit Hash) (Hash, error) {
+func (r *mockRepoData) GetTreeHash(commit Hash) (Hash, error) {
c, ok := r.commits[commit]
if !ok {
return "", fmt.Errorf("unknown commit")
@@ -250,7 +310,21 @@ func (r *mockRepoForTest) GetTreeHash(commit Hash) (Hash, error) {
return c.treeHash, nil
}
-func (r *mockRepoForTest) GetOrCreateClock(name string) (lamport.Clock, error) {
+func (r *mockRepoData) AddRemote(name string, url string) error {
+ panic("implement me")
+}
+
+type mockRepoClock struct {
+ clocks map[string]lamport.Clock
+}
+
+func NewMockRepoClock() *mockRepoClock {
+ return &mockRepoClock{
+ clocks: make(map[string]lamport.Clock),
+ }
+}
+
+func (r *mockRepoClock) GetOrCreateClock(name string) (lamport.Clock, error) {
if c, ok := r.clocks[name]; ok {
return c, nil
}
@@ -259,7 +333,3 @@ func (r *mockRepoForTest) GetOrCreateClock(name string) (lamport.Clock, error) {
r.clocks[name] = c
return c, nil
}
-
-func (r *mockRepoForTest) AddRemote(name string, url string) error {
- panic("implement me")
-}
diff --git a/repository/repo.go b/repository/repo.go
index 30d95806..4b45a1c5 100644
--- a/repository/repo.go
+++ b/repository/repo.go
@@ -2,29 +2,48 @@
package repository
import (
- "bytes"
"errors"
- "strings"
"github.com/MichaelMure/git-bug/util/lamport"
)
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")
)
+// Repo represents a source code repository.
+type Repo interface {
+ RepoConfig
+ RepoKeyring
+ RepoCommon
+ RepoData
+}
+
+// ClockedRepo is a Repo that also has Lamport clocks
+type ClockedRepo interface {
+ Repo
+ RepoClock
+}
+
// RepoConfig access the configuration of a repository
type RepoConfig interface {
// LocalConfig give access to the repository scoped configuration
LocalConfig() Config
- // GlobalConfig give access to the git global configuration
+ // GlobalConfig give access to the global scoped configuration
GlobalConfig() Config
+
+ // AnyConfig give access to a merged local/global configuration
+ AnyConfig() ConfigRead
+}
+
+// RepoKeyring give access to a user-wide storage for secrets
+type RepoKeyring interface {
+ // Keyring give access to a user-wide storage for secrets
+ Keyring() Keyring
}
// RepoCommon represent the common function the we want all the repo to implement
@@ -45,11 +64,8 @@ type RepoCommon interface {
GetRemotes() (map[string]string, error)
}
-// Repo represents a source code repository.
-type Repo interface {
- RepoConfig
- RepoCommon
-
+// RepoData give access to the git data storage
+type RepoData interface {
// FetchRefs fetch git refs from a remote
FetchRefs(remote string, refSpec string) (string, error)
@@ -66,6 +82,7 @@ type Repo interface {
StoreTree(mapping []TreeEntry) (Hash, error)
// ReadTree will return the list of entries in a Git tree
+ // The given hash could be from either a commit or a tree
ReadTree(hash Hash) ([]TreeEntry, error)
// StoreCommit will store a Git commit with the given Git tree
@@ -87,7 +104,7 @@ type Repo interface {
RemoveRef(ref string) error
// ListRefs will return a list of Git ref matching the given refspec
- ListRefs(refspec string) ([]string, error)
+ ListRefs(refPrefix string) ([]string, error)
// RefExist will check if a reference exist in Git
RefExist(ref string) (bool, error)
@@ -99,10 +116,8 @@ type Repo interface {
ListCommits(ref string) ([]Hash, error)
}
-// ClockedRepo is a Repo that also has Lamport clocks
-type ClockedRepo interface {
- Repo
-
+// RepoClock give access to Lamport clocks
+type RepoClock interface {
// GetOrCreateClock return a Lamport clock stored in the Repo.
// If the clock doesn't exist, it's created.
GetOrCreateClock(name string) (lamport.Clock, error)
@@ -120,41 +135,14 @@ type ClockLoader struct {
Witnesser func(repo ClockedRepo) error
}
-func prepareTreeEntries(entries []TreeEntry) bytes.Buffer {
- var buffer bytes.Buffer
-
- for _, entry := range entries {
- buffer.WriteString(entry.Format())
- }
-
- return buffer
-}
-
-func readTreeEntries(s string) ([]TreeEntry, error) {
- split := strings.Split(strings.TrimSpace(s), "\n")
-
- casted := make([]TreeEntry, len(split))
- for i, line := range split {
- if line == "" {
- continue
- }
-
- entry, err := ParseTreeEntry(line)
-
- if err != nil {
- return nil, err
- }
-
- casted[i] = entry
- }
-
- return casted, nil
-}
-
// TestedRepo is an extended ClockedRepo with function for testing only
type TestedRepo interface {
ClockedRepo
+ repoTest
+}
+// repoTest give access to test only functions
+type repoTest interface {
// 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
index 28eb9a21..41b3609e 100644
--- a/repository/repo_testing.go
+++ b/repository/repo_testing.go
@@ -7,8 +7,9 @@ import (
"strings"
"testing"
- "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
+
+ "github.com/MichaelMure/git-bug/util/lamport"
)
func CleanupTestRepos(repos ...Repo) {
@@ -47,136 +48,179 @@ type RepoCleaner func(repos ...Repo)
// Test suite for a Repo implementation
func RepoTest(t *testing.T, creator RepoCreator, cleaner RepoCleaner) {
- t.Run("Blob-Tree-Commit-Ref", func(t *testing.T) {
- repo := creator(false)
- defer cleaner(repo)
-
- // Blob
-
- data := randomData()
-
- blobHash1, err := repo.StoreData(data)
- require.NoError(t, err)
- assert.True(t, blobHash1.IsValid())
-
- blob1Read, err := repo.ReadData(blobHash1)
- require.NoError(t, err)
- assert.Equal(t, data, blob1Read)
-
- // Tree
-
- blobHash2, err := repo.StoreData(randomData())
- require.NoError(t, err)
- blobHash3, err := repo.StoreData(randomData())
- require.NoError(t, err)
-
- tree1 := []TreeEntry{
- {
- ObjectType: Blob,
- Hash: blobHash1,
- Name: "blob1",
- },
- {
- ObjectType: Blob,
- Hash: blobHash2,
- Name: "blob2",
- },
- }
+ for bare, name := range map[bool]string{
+ false: "Plain",
+ true: "Bare",
+ } {
+ t.Run(name, func(t *testing.T) {
+ repo := creator(bare)
+ defer cleaner(repo)
+
+ t.Run("Data", func(t *testing.T) {
+ RepoDataTest(t, repo)
+ })
+
+ t.Run("Config", func(t *testing.T) {
+ RepoConfigTest(t, repo)
+ })
+
+ t.Run("Clocks", func(t *testing.T) {
+ RepoClockTest(t, repo)
+ })
+ })
+ }
+}
- treeHash1, err := repo.StoreTree(tree1)
- require.NoError(t, err)
- assert.True(t, treeHash1.IsValid())
-
- tree1Read, err := repo.ReadTree(treeHash1)
- require.NoError(t, err)
- assert.ElementsMatch(t, tree1, tree1Read)
-
- tree2 := []TreeEntry{
- {
- ObjectType: Tree,
- Hash: treeHash1,
- Name: "tree1",
- },
- {
- ObjectType: Blob,
- Hash: blobHash3,
- Name: "blob3",
- },
- }
+// helper to test a RepoConfig
+func RepoConfigTest(t *testing.T, repo RepoConfig) {
+ testConfig(t, repo.LocalConfig())
+}
+
+// helper to test a RepoData
+func RepoDataTest(t *testing.T, repo RepoData) {
+ // Blob
+
+ data := randomData()
+
+ blobHash1, err := repo.StoreData(data)
+ require.NoError(t, err)
+ require.True(t, blobHash1.IsValid())
+
+ blob1Read, err := repo.ReadData(blobHash1)
+ require.NoError(t, err)
+ require.Equal(t, data, blob1Read)
+
+ // Tree
+
+ blobHash2, err := repo.StoreData(randomData())
+ require.NoError(t, err)
+ blobHash3, err := repo.StoreData(randomData())
+ require.NoError(t, err)
+
+ tree1 := []TreeEntry{
+ {
+ ObjectType: Blob,
+ Hash: blobHash1,
+ Name: "blob1",
+ },
+ {
+ ObjectType: Blob,
+ Hash: blobHash2,
+ Name: "blob2",
+ },
+ }
+
+ treeHash1, err := repo.StoreTree(tree1)
+ require.NoError(t, err)
+ require.True(t, treeHash1.IsValid())
+
+ tree1Read, err := repo.ReadTree(treeHash1)
+ require.NoError(t, err)
+ require.ElementsMatch(t, tree1, tree1Read)
+
+ tree2 := []TreeEntry{
+ {
+ ObjectType: Tree,
+ Hash: treeHash1,
+ Name: "tree1",
+ },
+ {
+ ObjectType: Blob,
+ Hash: blobHash3,
+ Name: "blob3",
+ },
+ }
+
+ treeHash2, err := repo.StoreTree(tree2)
+ require.NoError(t, err)
+ require.True(t, treeHash2.IsValid())
- treeHash2, err := repo.StoreTree(tree2)
- require.NoError(t, err)
- assert.True(t, treeHash2.IsValid())
+ tree2Read, err := repo.ReadTree(treeHash2)
+ require.NoError(t, err)
+ require.ElementsMatch(t, tree2, tree2Read)
- tree2Read, err := repo.ReadTree(treeHash2)
- require.NoError(t, err)
- assert.ElementsMatch(t, tree2, tree2Read)
+ // Commit
- // Commit
+ commit1, err := repo.StoreCommit(treeHash1)
+ require.NoError(t, err)
+ require.True(t, commit1.IsValid())
- commit1, err := repo.StoreCommit(treeHash1)
- require.NoError(t, err)
- assert.True(t, commit1.IsValid())
+ treeHash1Read, err := repo.GetTreeHash(commit1)
+ require.NoError(t, err)
+ require.Equal(t, treeHash1, treeHash1Read)
- treeHash1Read, err := repo.GetTreeHash(commit1)
- require.NoError(t, err)
- assert.Equal(t, treeHash1, treeHash1Read)
+ commit2, err := repo.StoreCommitWithParent(treeHash2, commit1)
+ require.NoError(t, err)
+ require.True(t, commit2.IsValid())
- commit2, err := repo.StoreCommitWithParent(treeHash2, commit1)
- require.NoError(t, err)
- assert.True(t, commit2.IsValid())
+ treeHash2Read, err := repo.GetTreeHash(commit2)
+ require.NoError(t, err)
+ require.Equal(t, treeHash2, treeHash2Read)
- treeHash2Read, err := repo.GetTreeHash(commit2)
- require.NoError(t, err)
- assert.Equal(t, treeHash2, treeHash2Read)
+ // ReadTree should accept tree and commit hashes
+ tree1read, err := repo.ReadTree(commit1)
+ require.NoError(t, err)
+ require.Equal(t, tree1read, tree1)
- // Ref
+ // Ref
- exist1, err := repo.RefExist("refs/bugs/ref1")
- require.NoError(t, err)
- assert.False(t, exist1)
+ exist1, err := repo.RefExist("refs/bugs/ref1")
+ require.NoError(t, err)
+ require.False(t, exist1)
- err = repo.UpdateRef("refs/bugs/ref1", commit2)
- require.NoError(t, err)
+ err = repo.UpdateRef("refs/bugs/ref1", commit2)
+ require.NoError(t, err)
- exist1, err = repo.RefExist("refs/bugs/ref1")
- require.NoError(t, err)
- assert.True(t, exist1)
+ exist1, err = repo.RefExist("refs/bugs/ref1")
+ require.NoError(t, err)
+ require.True(t, exist1)
- ls, err := repo.ListRefs("refs/bugs")
- require.NoError(t, err)
- assert.ElementsMatch(t, []string{"refs/bugs/ref1"}, ls)
+ ls, err := repo.ListRefs("refs/bugs")
+ require.NoError(t, err)
+ require.ElementsMatch(t, []string{"refs/bugs/ref1"}, ls)
- err = repo.CopyRef("refs/bugs/ref1", "refs/bugs/ref2")
- require.NoError(t, err)
+ err = repo.CopyRef("refs/bugs/ref1", "refs/bugs/ref2")
+ require.NoError(t, err)
- ls, err = repo.ListRefs("refs/bugs")
- require.NoError(t, err)
- assert.ElementsMatch(t, []string{"refs/bugs/ref1", "refs/bugs/ref2"}, ls)
+ ls, err = repo.ListRefs("refs/bugs")
+ require.NoError(t, err)
+ require.ElementsMatch(t, []string{"refs/bugs/ref1", "refs/bugs/ref2"}, ls)
- commits, err := repo.ListCommits("refs/bugs/ref2")
- require.NoError(t, err)
- assert.ElementsMatch(t, []Hash{commit1, commit2}, commits)
+ commits, err := repo.ListCommits("refs/bugs/ref2")
+ require.NoError(t, err)
+ require.Equal(t, []Hash{commit1, commit2}, commits)
- // Graph
+ // Graph
- commit3, err := repo.StoreCommitWithParent(treeHash1, commit1)
- require.NoError(t, err)
+ commit3, err := repo.StoreCommitWithParent(treeHash1, commit1)
+ require.NoError(t, err)
+
+ ancestorHash, err := repo.FindCommonAncestor(commit2, commit3)
+ require.NoError(t, err)
+ require.Equal(t, commit1, ancestorHash)
+
+ err = repo.RemoveRef("refs/bugs/ref1")
+ require.NoError(t, err)
+}
- ancestorHash, err := repo.FindCommonAncestor(commit2, commit3)
- require.NoError(t, err)
- assert.Equal(t, commit1, ancestorHash)
+// helper to test a RepoClock
+func RepoClockTest(t *testing.T, repo RepoClock) {
+ clock, err := repo.GetOrCreateClock("foo")
+ require.NoError(t, err)
+ require.Equal(t, lamport.Time(1), clock.Time())
- err = repo.RemoveRef("refs/bugs/ref1")
- require.NoError(t, err)
- })
+ time, err := clock.Increment()
+ require.NoError(t, err)
+ require.Equal(t, lamport.Time(1), time)
+ require.Equal(t, lamport.Time(2), clock.Time())
- t.Run("Local config", func(t *testing.T) {
- repo := creator(false)
- defer cleaner(repo)
+ clock2, err := repo.GetOrCreateClock("foo")
+ require.NoError(t, err)
+ require.Equal(t, lamport.Time(2), clock2.Time())
- testConfig(t, repo.LocalConfig())
- })
+ clock3, err := repo.GetOrCreateClock("bar")
+ require.NoError(t, err)
+ require.Equal(t, lamport.Time(1), clock3.Time())
}
func randomData() []byte {
diff --git a/repository/tree_entry.go b/repository/tree_entry.go
index 8b3de8e2..6c5ec1a5 100644
--- a/repository/tree_entry.go
+++ b/repository/tree_entry.go
@@ -1,6 +1,7 @@
package repository
import (
+ "bytes"
"fmt"
"strings"
)
@@ -68,3 +69,34 @@ func ParseObjectType(mode, objType string) (ObjectType, error) {
return Unknown, fmt.Errorf("Unknown git object type %s %s", mode, objType)
}
}
+
+func prepareTreeEntries(entries []TreeEntry) bytes.Buffer {
+ var buffer bytes.Buffer
+
+ for _, entry := range entries {
+ buffer.WriteString(entry.Format())
+ }
+
+ return buffer
+}
+
+func readTreeEntries(s string) ([]TreeEntry, error) {
+ split := strings.Split(strings.TrimSpace(s), "\n")
+
+ casted := make([]TreeEntry, len(split))
+ for i, line := range split {
+ if line == "" {
+ continue
+ }
+
+ entry, err := ParseTreeEntry(line)
+
+ if err != nil {
+ return nil, err
+ }
+
+ casted[i] = entry
+ }
+
+ return casted, nil
+}