aboutsummaryrefslogtreecommitdiffstats
path: root/repository
diff options
context:
space:
mode:
Diffstat (limited to 'repository')
-rw-r--r--repository/common.go67
-rw-r--r--repository/config_mem.go19
-rw-r--r--repository/config_testing.go39
-rw-r--r--repository/git.go500
-rw-r--r--repository/git_cli.go56
-rw-r--r--repository/git_config.go221
-rw-r--r--repository/git_test.go10
-rw-r--r--repository/git_testing.go72
-rw-r--r--repository/gogit.go193
-rw-r--r--repository/gogit_config.go2
-rw-r--r--repository/gogit_testing.go8
-rw-r--r--repository/keyring.go12
-rw-r--r--repository/mock_repo.go257
-rw-r--r--repository/mock_repo_test.go6
-rw-r--r--repository/repo.go60
-rw-r--r--repository/repo_testing.go84
-rw-r--r--repository/tree_entry.go10
17 files changed, 603 insertions, 1013 deletions
diff --git a/repository/common.go b/repository/common.go
new file mode 100644
index 00000000..4cefbd9e
--- /dev/null
+++ b/repository/common.go
@@ -0,0 +1,67 @@
+package repository
+
+import (
+ "io"
+
+ "golang.org/x/crypto/openpgp"
+ "golang.org/x/crypto/openpgp/armor"
+ "golang.org/x/crypto/openpgp/errors"
+)
+
+// nonNativeListCommits is an implementation for ListCommits, for the case where
+// the underlying git implementation doesn't support if natively.
+func nonNativeListCommits(repo RepoData, ref string) ([]Hash, error) {
+ var result []Hash
+
+ stack := make([]Hash, 0, 32)
+ visited := make(map[Hash]struct{})
+
+ hash, err := repo.ResolveRef(ref)
+ if err != nil {
+ return nil, err
+ }
+
+ stack = append(stack, hash)
+
+ for len(stack) > 0 {
+ // pop
+ hash := stack[len(stack)-1]
+ stack = stack[:len(stack)-1]
+
+ if _, ok := visited[hash]; ok {
+ continue
+ }
+
+ // mark as visited
+ visited[hash] = struct{}{}
+ result = append(result, hash)
+
+ commit, err := repo.ReadCommit(hash)
+ if err != nil {
+ return nil, err
+ }
+
+ for _, parent := range commit.Parents {
+ stack = append(stack, parent)
+ }
+ }
+
+ // reverse
+ for i, j := 0, len(result)-1; i < j; i, j = i+1, j-1 {
+ result[i], result[j] = result[j], result[i]
+ }
+
+ return result, nil
+}
+
+// deArmorSignature convert an armored (text serialized) signature into raw binary
+func deArmorSignature(armoredSig io.Reader) (io.Reader, error) {
+ block, err := armor.Decode(armoredSig)
+ if err != nil {
+ return nil, err
+ }
+ if block.Type != openpgp.SignatureType {
+ return nil, errors.InvalidArgumentError("expected '" + openpgp.SignatureType + "', got: " + block.Type)
+ }
+ return block.Body, nil
+}
diff --git a/repository/config_mem.go b/repository/config_mem.go
index 9725e8d5..019bc111 100644
--- a/repository/config_mem.go
+++ b/repository/config_mem.go
@@ -20,6 +20,7 @@ func NewMemConfig() *MemConfig {
}
func (mc *MemConfig) StoreString(key, value string) error {
+ key = normalizeKey(key)
mc.config[key] = value
return nil
}
@@ -33,6 +34,7 @@ func (mc *MemConfig) StoreTimestamp(key string, value time.Time) error {
}
func (mc *MemConfig) ReadAll(keyPrefix string) (map[string]string, error) {
+ keyPrefix = normalizeKey(keyPrefix)
result := make(map[string]string)
for key, val := range mc.config {
if strings.HasPrefix(key, keyPrefix) {
@@ -44,6 +46,7 @@ func (mc *MemConfig) ReadAll(keyPrefix string) (map[string]string, error) {
func (mc *MemConfig) ReadString(key string) (string, error) {
// unlike git, the mock can only store one value for the same key
+ key = normalizeKey(key)
val, ok := mc.config[key]
if !ok {
return "", ErrNoConfigEntry
@@ -54,9 +57,9 @@ func (mc *MemConfig) ReadString(key string) (string, error) {
func (mc *MemConfig) ReadBool(key string) (bool, error) {
// unlike git, the mock can only store one value for the same key
- val, ok := mc.config[key]
- if !ok {
- return false, ErrNoConfigEntry
+ val, err := mc.ReadString(key)
+ if err != nil {
+ return false, err
}
return strconv.ParseBool(val)
@@ -78,6 +81,7 @@ 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 {
+ keyPrefix = normalizeKey(keyPrefix)
found := false
for key := range mc.config {
if strings.HasPrefix(key, keyPrefix) {
@@ -92,3 +96,12 @@ func (mc *MemConfig) RemoveAll(keyPrefix string) error {
return nil
}
+
+func normalizeKey(key string) string {
+ // this feels so wrong, but that's apparently how git behave.
+ // only section and final segment are case insensitive, subsection in between are not.
+ s := strings.Split(key, ".")
+ s[0] = strings.ToLower(s[0])
+ s[len(s)-1] = strings.ToLower(s[len(s)-1])
+ return strings.Join(s, ".")
+}
diff --git a/repository/config_testing.go b/repository/config_testing.go
index 445f8721..f8a2762b 100644
--- a/repository/config_testing.go
+++ b/repository/config_testing.go
@@ -113,4 +113,43 @@ func testConfig(t *testing.T, config Config) {
"section.subsection.subsection.opt1": "foo5",
"section.subsection.subsection.opt2": "foo6",
}, all)
+
+ // missing section + case insensitive
+ val, err = config.ReadString("section2.opt1")
+ require.Error(t, err)
+
+ val, err = config.ReadString("section.opt1")
+ require.NoError(t, err)
+ require.Equal(t, "foo", val)
+
+ val, err = config.ReadString("SECTION.OPT1")
+ require.NoError(t, err)
+ require.Equal(t, "foo", val)
+
+ _, err = config.ReadString("SECTION2.OPT3")
+ require.Error(t, err)
+
+ // missing subsection + case insensitive
+ val, err = config.ReadString("section.subsection.opt1")
+ require.NoError(t, err)
+ require.Equal(t, "foo3", val)
+
+ // for some weird reason, subsection ARE case sensitive
+ _, err = config.ReadString("SECTION.SUBSECTION.OPT1")
+ require.Error(t, err)
+
+ _, err = config.ReadString("SECTION.SUBSECTION1.OPT1")
+ require.Error(t, err)
+
+ // missing sub-subsection + case insensitive
+ val, err = config.ReadString("section.subsection.subsection.opt1")
+ require.NoError(t, err)
+ require.Equal(t, "foo5", val)
+
+ // for some weird reason, subsection ARE case sensitive
+ _, err = config.ReadString("SECTION.SUBSECTION.SUBSECTION.OPT1")
+ require.Error(t, err)
+
+ _, err = config.ReadString("SECTION.SUBSECTION.SUBSECTION1.OPT1")
+ require.Error(t, err)
}
diff --git a/repository/git.go b/repository/git.go
deleted file mode 100644
index bc9d8772..00000000
--- a/repository/git.go
+++ /dev/null
@@ -1,500 +0,0 @@
-// Package repository contains helper methods for working with the Git repo.
-package repository
-
-import (
- "bytes"
- "fmt"
- "os"
- "path/filepath"
- "strings"
- "sync"
-
- "github.com/blevesearch/bleve"
- "github.com/go-git/go-billy/v5"
- "github.com/go-git/go-billy/v5/osfs"
-
- "github.com/MichaelMure/git-bug/util/lamport"
-)
-
-const (
- clockPath = "git-bug"
-)
-
-var _ ClockedRepo = &GitRepo{}
-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
-
- indexesMutex sync.Mutex
- indexes map[string]bleve.Index
-
- keyring Keyring
-}
-
-// OpenGitRepo determines if the given working directory is inside of a git repository,
-// and returns the corresponding GitRepo instance if it is.
-func OpenGitRepo(path string, clockLoaders []ClockLoader) (*GitRepo, error) {
- k, err := defaultKeyring()
- if err != nil {
- return nil, err
- }
-
- repo := &GitRepo{
- gitCli: gitCli{path: path},
- path: path,
- clocks: make(map[string]lamport.Clock),
- indexes: make(map[string]bleve.Index),
- keyring: k,
- }
-
- // Check the repo and retrieve the root path
- stdout, err := repo.runGitCommand("rev-parse", "--absolute-git-dir")
-
- // Now dir is fetched with "git rev-parse --git-dir". May be it can
- // still return nothing in some cases. Then empty stdout check is
- // kept.
- if err != nil || stdout == "" {
- return nil, ErrNotARepo
- }
-
- // Fix the path to be sure we are at the root
- repo.path = stdout
- repo.gitCli.path = stdout
-
- 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
-}
-
-// 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),
- indexes: make(map[string]bleve.Index),
- }
-
- _, err := repo.runGitCommand("init", path)
- if err != nil {
- return nil, err
- }
-
- return repo, nil
-}
-
-// 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),
- indexes: make(map[string]bleve.Index),
- }
-
- _, err := repo.runGitCommand("init", "--bare", path)
- if err != nil {
- return nil, err
- }
-
- return repo, nil
-}
-
-func (repo *GitRepo) Close() error {
- var firstErr error
- for _, index := range repo.indexes {
- err := index.Close()
- if err != nil && firstErr == nil {
- firstErr = err
- }
- }
- return firstErr
-}
-
-// 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
-}
-
-// GetUserName returns the name the the user has used to configure git
-func (repo *GitRepo) GetUserName() (string, error) {
- return repo.runGitCommand("config", "user.name")
-}
-
-// GetUserEmail returns the email address that the user has used to configure git.
-func (repo *GitRepo) GetUserEmail() (string, error) {
- return repo.runGitCommand("config", "user.email")
-}
-
-// GetCoreEditor returns the name of the editor that the user has used to configure git.
-func (repo *GitRepo) GetCoreEditor() (string, error) {
- return repo.runGitCommand("var", "GIT_EDITOR")
-}
-
-// GetRemotes returns the configured remotes repositories.
-func (repo *GitRepo) GetRemotes() (map[string]string, error) {
- stdout, err := repo.runGitCommand("remote", "--verbose")
- if err != nil {
- return nil, err
- }
-
- lines := strings.Split(stdout, "\n")
- remotes := make(map[string]string, len(lines))
-
- for _, line := range lines {
- if strings.TrimSpace(line) == "" {
- continue
- }
- elements := strings.Fields(line)
- if len(elements) != 3 {
- return nil, fmt.Errorf("git remote: unexpected output format: %s", line)
- }
-
- remotes[elements[0]] = elements[1]
- }
-
- return remotes, nil
-}
-
-// LocalStorage return a billy.Filesystem giving access to $RepoPath/.git/git-bug
-func (repo *GitRepo) LocalStorage() billy.Filesystem {
- return osfs.New(repo.path)
-}
-
-// GetBleveIndex return a bleve.Index that can be used to index documents
-func (repo *GitRepo) GetBleveIndex(name string) (bleve.Index, error) {
- repo.indexesMutex.Lock()
- defer repo.indexesMutex.Unlock()
-
- if index, ok := repo.indexes[name]; ok {
- return index, nil
- }
-
- path := filepath.Join(repo.path, "indexes", name)
-
- index, err := bleve.Open(path)
- if err == nil {
- repo.indexes[name] = index
- return index, nil
- }
-
- err = os.MkdirAll(path, os.ModeDir)
- if err != nil {
- return nil, err
- }
-
- mapping := bleve.NewIndexMapping()
- mapping.DefaultAnalyzer = "en"
-
- index, err = bleve.New(path, mapping)
- if err != nil {
- return nil, err
- }
-
- repo.indexes[name] = index
-
- return index, nil
-}
-
-// ClearBleveIndex will wipe the given index
-func (repo *GitRepo) ClearBleveIndex(name string) error {
- repo.indexesMutex.Lock()
- defer repo.indexesMutex.Unlock()
-
- path := filepath.Join(repo.path, "indexes", name)
-
- err := os.RemoveAll(path)
- if err != nil {
- return err
- }
-
- delete(repo.indexes, name)
-
- return nil
-}
-
-// FetchRefs fetch git refs from a remote
-func (repo *GitRepo) FetchRefs(remote, refSpec string) (string, error) {
- stdout, err := repo.runGitCommand("fetch", remote, refSpec)
-
- if err != nil {
- return stdout, fmt.Errorf("failed to fetch from the remote '%s': %v", remote, err)
- }
-
- return stdout, err
-}
-
-// PushRefs push git refs to a remote
-func (repo *GitRepo) PushRefs(remote string, refSpec string) (string, error) {
- stdout, stderr, err := repo.runGitCommandRaw(nil, "push", remote, refSpec)
-
- if err != nil {
- return stdout + stderr, fmt.Errorf("failed to push to the remote '%s': %v", remote, stderr)
- }
- return stdout + stderr, nil
-}
-
-// StoreData will store arbitrary data and return the corresponding hash
-func (repo *GitRepo) StoreData(data []byte) (Hash, error) {
- var stdin = bytes.NewReader(data)
-
- stdout, err := repo.runGitCommandWithStdin(stdin, "hash-object", "--stdin", "-w")
-
- return Hash(stdout), err
-}
-
-// ReadData will attempt to read arbitrary data from the given hash
-func (repo *GitRepo) ReadData(hash Hash) ([]byte, error) {
- var stdout bytes.Buffer
- var stderr bytes.Buffer
-
- err := repo.runGitCommandWithIO(nil, &stdout, &stderr, "cat-file", "-p", string(hash))
-
- if err != nil {
- return []byte{}, err
- }
-
- return stdout.Bytes(), nil
-}
-
-// StoreTree will store a mapping key-->Hash as a Git tree
-func (repo *GitRepo) StoreTree(entries []TreeEntry) (Hash, error) {
- buffer := prepareTreeEntries(entries)
-
- stdout, err := repo.runGitCommandWithStdin(&buffer, "mktree")
-
- if err != nil {
- return "", err
- }
-
- return Hash(stdout), nil
-}
-
-// StoreCommit will store a Git commit with the given Git tree
-func (repo *GitRepo) StoreCommit(treeHash Hash) (Hash, error) {
- stdout, err := repo.runGitCommand("commit-tree", string(treeHash))
-
- if err != nil {
- return "", err
- }
-
- return Hash(stdout), nil
-}
-
-// StoreCommitWithParent will store a Git commit with the given Git tree
-func (repo *GitRepo) StoreCommitWithParent(treeHash Hash, parent Hash) (Hash, error) {
- stdout, err := repo.runGitCommand("commit-tree", string(treeHash),
- "-p", string(parent))
-
- if err != nil {
- return "", err
- }
-
- return Hash(stdout), nil
-}
-
-// UpdateRef will create or update a Git reference
-func (repo *GitRepo) UpdateRef(ref string, hash Hash) error {
- _, err := repo.runGitCommand("update-ref", ref, string(hash))
-
- return err
-}
-
-// RemoveRef will remove a Git reference
-func (repo *GitRepo) RemoveRef(ref string) error {
- _, err := repo.runGitCommand("update-ref", "-d", ref)
-
- return err
-}
-
-// ListRefs will return a list of Git ref matching the given 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
- }
-
- split := strings.Split(stdout, "\n")
-
- if len(split) == 1 && split[0] == "" {
- return []string{}, nil
- }
-
- return split, nil
-}
-
-// RefExist will check if a reference exist in Git
-func (repo *GitRepo) RefExist(ref string) (bool, error) {
- stdout, err := repo.runGitCommand("for-each-ref", ref)
-
- if err != nil {
- return false, err
- }
-
- return stdout != "", nil
-}
-
-// CopyRef will create a new reference with the same value as another one
-func (repo *GitRepo) CopyRef(source string, dest string) error {
- _, err := repo.runGitCommand("update-ref", dest, source)
-
- return err
-}
-
-// ListCommits will return the list of commit hashes of a ref, in chronological order
-func (repo *GitRepo) ListCommits(ref string) ([]Hash, error) {
- stdout, err := repo.runGitCommand("rev-list", "--first-parent", "--reverse", ref)
-
- if err != nil {
- return nil, err
- }
-
- split := strings.Split(stdout, "\n")
-
- casted := make([]Hash, len(split))
- for i, line := range split {
- casted[i] = Hash(line)
- }
-
- return casted, nil
-
-}
-
-// ReadTree will return the list of entries in a Git tree
-func (repo *GitRepo) ReadTree(hash Hash) ([]TreeEntry, error) {
- stdout, err := repo.runGitCommand("ls-tree", string(hash))
-
- if err != nil {
- return nil, err
- }
-
- return readTreeEntries(stdout)
-}
-
-// FindCommonAncestor will return the last common ancestor of two chain of commit
-func (repo *GitRepo) FindCommonAncestor(hash1 Hash, hash2 Hash) (Hash, error) {
- stdout, err := repo.runGitCommand("merge-base", string(hash1), string(hash2))
-
- if err != nil {
- return "", err
- }
-
- return Hash(stdout), nil
-}
-
-// GetTreeHash return the git tree hash referenced in a commit
-func (repo *GitRepo) GetTreeHash(commit Hash) (Hash, error) {
- stdout, err := repo.runGitCommand("rev-parse", string(commit)+"^{tree}")
-
- if err != nil {
- return "", err
- }
-
- return Hash(stdout), nil
-}
-
-// 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) {
- repo.clocksMutex.Lock()
- defer repo.clocksMutex.Unlock()
-
- c, err := repo.getClock(name)
- if err == nil {
- return c, nil
- }
- if err != ErrClockNotExist {
- return nil, err
- }
-
- c, err = lamport.NewPersistedClock(repo.LocalStorage(), name+"-clock")
- if err != nil {
- return nil, err
- }
-
- repo.clocks[name] = c
- return c, nil
-}
-
-func (repo *GitRepo) getClock(name string) (lamport.Clock, error) {
- if c, ok := repo.clocks[name]; ok {
- return c, nil
- }
-
- c, err := lamport.LoadPersistedClock(repo.LocalStorage(), name+"-clock")
- 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 *GitRepo) AddRemote(name string, url string) error {
- _, err := repo.runGitCommand("remote", "add", name, url)
-
- return err
-}
-
-// GetLocalRemote return the URL to use to add this repo as a local remote
-func (repo *GitRepo) GetLocalRemote() string {
- return repo.path
-}
-
-// EraseFromDisk delete this repository entirely from the disk
-func (repo *GitRepo) EraseFromDisk() error {
- err := repo.Close()
- if err != nil {
- return err
- }
-
- path := filepath.Clean(strings.TrimSuffix(repo.path, string(filepath.Separator)+".git"))
-
- // fmt.Println("Cleaning repo:", path)
- return os.RemoveAll(path)
-}
diff --git a/repository/git_cli.go b/repository/git_cli.go
deleted file mode 100644
index 085b1cda..00000000
--- a/repository/git_cli.go
+++ /dev/null
@@ -1,56 +0,0 @@
-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
deleted file mode 100644
index b46cc69b..00000000
--- a/repository/git_config.go
+++ /dev/null
@@ -1,221 +0,0 @@
-package repository
-
-import (
- "fmt"
- "regexp"
- "strconv"
- "strings"
- "time"
-
- "github.com/blang/semver"
- "github.com/pkg/errors"
-)
-
-var _ Config = &gitConfig{}
-
-type gitConfig struct {
- cli gitCli
- localityFlag string
-}
-
-func newGitConfig(cli gitCli, global bool) *gitConfig {
- localityFlag := "--local"
- if global {
- localityFlag = "--global"
- }
- return &gitConfig{
- 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.cli.runGitCommand("config", gc.localityFlag, "--replace-all", key, value)
- return err
-}
-
-func (gc *gitConfig) StoreBool(key string, value bool) error {
- return gc.StoreString(key, strconv.FormatBool(value))
-}
-
-func (gc *gitConfig) StoreTimestamp(key string, value time.Time) error {
- return gc.StoreString(key, strconv.Itoa(int(value.Unix())))
-}
-
-// ReadAll read all key/value pair matching the key prefix
-func (gc *gitConfig) ReadAll(keyPrefix string) (map[string]string, error) {
- stdout, err := gc.cli.runGitCommand("config", gc.localityFlag, "--includes", "--get-regexp", keyPrefix)
-
- // / \
- // / ! \
- // -------
- //
- // There can be a legitimate error here, but I see no portable way to
- // distinguish them from the git error that say "no matching value exist"
- if err != nil {
- return nil, nil
- }
-
- lines := strings.Split(stdout, "\n")
-
- result := make(map[string]string, len(lines))
-
- for _, line := range lines {
- if strings.TrimSpace(line) == "" {
- continue
- }
-
- parts := strings.SplitN(line, " ", 2)
- result[parts[0]] = parts[1]
- }
-
- return result, nil
-}
-
-func (gc *gitConfig) ReadString(key string) (string, error) {
- stdout, err := gc.cli.runGitCommand("config", gc.localityFlag, "--includes", "--get-all", key)
-
- // / \
- // / ! \
- // -------
- //
- // There can be a legitimate error here, but I see no portable way to
- // distinguish them from the git error that say "no matching value exist"
- if err != nil {
- return "", ErrNoConfigEntry
- }
-
- lines := strings.Split(stdout, "\n")
-
- if len(lines) == 0 {
- return "", ErrNoConfigEntry
- }
- if len(lines) > 1 {
- return "", ErrMultipleConfigEntry
- }
-
- return lines[0], nil
-}
-
-func (gc *gitConfig) ReadBool(key string) (bool, error) {
- val, err := gc.ReadString(key)
- if err != nil {
- return false, err
- }
-
- return strconv.ParseBool(val)
-}
-
-func (gc *gitConfig) ReadTimestamp(key string) (time.Time, error) {
- value, err := gc.ReadString(key)
- if err != nil {
- return time.Time{}, err
- }
- return ParseTimestamp(value)
-}
-
-func (gc *gitConfig) rmSection(keyPrefix string) error {
- _, err := gc.cli.runGitCommand("config", gc.localityFlag, "--remove-section", keyPrefix)
- return err
-}
-
-func (gc *gitConfig) unsetAll(keyPrefix string) error {
- _, err := gc.cli.runGitCommand("config", gc.localityFlag, "--unset-all", keyPrefix)
- return err
-}
-
-// return keyPrefix section
-// example: sectionFromKey(a.b.c.d) return a.b.c
-func sectionFromKey(keyPrefix string) string {
- s := strings.Split(keyPrefix, ".")
- if len(s) == 1 {
- return keyPrefix
- }
-
- return strings.Join(s[:len(s)-1], ".")
-}
-
-// rmConfigs with git version lesser than 2.18
-func (gc *gitConfig) rmConfigsGitVersionLT218(keyPrefix string) error {
- // try to remove key/value pair by key
- err := gc.unsetAll(keyPrefix)
- if err != nil {
- return gc.rmSection(keyPrefix)
- }
-
- m, err := gc.ReadAll(sectionFromKey(keyPrefix))
- if err != nil {
- return err
- }
-
- // if section doesn't have any left key/value remove the section
- if len(m) == 0 {
- return gc.rmSection(sectionFromKey(keyPrefix))
- }
-
- return nil
-}
-
-// RmConfigs remove all key/value pair matching the key prefix
-func (gc *gitConfig) RemoveAll(keyPrefix string) error {
- // starting from git 2.18.0 sections are automatically deleted when the last existing
- // key/value is removed. Before 2.18.0 we should remove the section
- // see https://github.com/git/git/blob/master/Documentation/RelNotes/2.18.0.txt#L379
- lt218, err := gc.gitVersionLT218()
- if err != nil {
- return errors.Wrap(err, "getting git version")
- }
-
- if lt218 {
- return gc.rmConfigsGitVersionLT218(keyPrefix)
- }
-
- err = gc.unsetAll(keyPrefix)
- if err != nil {
- return gc.rmSection(keyPrefix)
- }
-
- return nil
-}
-
-func (gc *gitConfig) gitVersion() (*semver.Version, error) {
- versionOut, err := gc.cli.runGitCommand("version")
- if err != nil {
- return nil, err
- }
- return parseGitVersion(versionOut)
-}
-
-func parseGitVersion(versionOut string) (*semver.Version, error) {
- // extract the version and truncate potential bad parts
- // ex: 2.23.0.rc1 instead of 2.23.0-rc1
- r := regexp.MustCompile(`(\d+\.){1,2}\d+`)
-
- extracted := r.FindString(versionOut)
- if extracted == "" {
- return nil, fmt.Errorf("unreadable git version %s", versionOut)
- }
-
- version, err := semver.Make(extracted)
- if err != nil {
- return nil, err
- }
-
- return &version, nil
-}
-
-func (gc *gitConfig) gitVersionLT218() (bool, error) {
- version, err := gc.gitVersion()
- if err != nil {
- return false, err
- }
-
- version218string := "2.18.0"
- gitVersion218, err := semver.Make(version218string)
- if err != nil {
- return false, err
- }
-
- return version.LT(gitVersion218), nil
-}
diff --git a/repository/git_test.go b/repository/git_test.go
deleted file mode 100644
index 1b36fd4c..00000000
--- a/repository/git_test.go
+++ /dev/null
@@ -1,10 +0,0 @@
-// Package repository contains helper methods for working with the Git repo.
-package repository
-
-import (
- "testing"
-)
-
-func TestGitRepo(t *testing.T) {
- RepoTest(t, CreateTestRepo, CleanupTestRepos)
-}
diff --git a/repository/git_testing.go b/repository/git_testing.go
deleted file mode 100644
index 2168d53e..00000000
--- a/repository/git_testing.go
+++ /dev/null
@@ -1,72 +0,0 @@
-package repository
-
-import (
- "io/ioutil"
- "log"
-
- "github.com/99designs/keyring"
-)
-
-// This is intended for testing only
-
-func CreateTestRepo(bare bool) TestedRepo {
- dir, err := ioutil.TempDir("", "")
- if err != nil {
- log.Fatal(err)
- }
-
- var creator func(string) (*GitRepo, error)
-
- if bare {
- creator = InitBareGitRepo
- } else {
- creator = InitGitRepo
- }
-
- 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)
- }
-
- // 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) {
- repoA = CreateGoGitTestRepo(false)
- repoB = CreateGoGitTestRepo(false)
- remote = CreateGoGitTestRepo(true)
-
- err := repoA.AddRemote("origin", remote.GetLocalRemote())
- if err != nil {
- log.Fatal(err)
- }
-
- err = repoB.AddRemote("origin", remote.GetLocalRemote())
- if err != nil {
- log.Fatal(err)
- }
-
- 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
index bdac259d..20454bd7 100644
--- a/repository/gogit.go
+++ b/repository/gogit.go
@@ -5,7 +5,6 @@ import (
"fmt"
"io/ioutil"
"os"
- "os/exec"
"path/filepath"
"sort"
"strings"
@@ -20,10 +19,14 @@ import (
"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"
+ "golang.org/x/crypto/openpgp"
+ "golang.org/x/sys/execabs"
"github.com/MichaelMure/git-bug/util/lamport"
)
+const clockPath = "clocks"
+
var _ ClockedRepo = &GoGitRepo{}
var _ TestedRepo = &GoGitRepo{}
@@ -261,7 +264,7 @@ func (repo *GoGitRepo) GetCoreEditor() (string, error) {
}
for _, cmd := range priorities {
- if _, err = exec.LookPath(cmd); err == nil {
+ if _, err = execabs.LookPath(cmd); err == nil {
return cmd, nil
}
@@ -332,7 +335,7 @@ func (repo *GoGitRepo) ClearBleveIndex(name string) error {
repo.indexesMutex.Lock()
defer repo.indexesMutex.Unlock()
- path := filepath.Join(repo.path, "indexes", name)
+ path := filepath.Join(repo.path, "git-bug", "indexes", name)
err := os.RemoveAll(path)
if err != nil {
@@ -350,13 +353,17 @@ func (repo *GoGitRepo) ClearBleveIndex(name string) error {
return nil
}
-// FetchRefs fetch git refs from a remote
-func (repo *GoGitRepo) FetchRefs(remote string, refSpec string) (string, error) {
+// FetchRefs fetch git refs matching a directory prefix to a remote
+// Ex: prefix="foo" will fetch any remote refs matching "refs/foo/*" locally.
+// The equivalent git refspec would be "refs/foo/*:refs/remotes/<remote>/foo/*"
+func (repo *GoGitRepo) FetchRefs(remote string, prefix string) (string, error) {
+ refspec := fmt.Sprintf("refs/%s/*:refs/remotes/%s/%s/*", prefix, remote, prefix)
+
buf := bytes.NewBuffer(nil)
err := repo.r.Fetch(&gogit.FetchOptions{
RemoteName: remote,
- RefSpecs: []config.RefSpec{config.RefSpec(refSpec)},
+ RefSpecs: []config.RefSpec{config.RefSpec(refspec)},
Progress: buf,
})
if err == gogit.NoErrAlreadyUpToDate {
@@ -369,13 +376,41 @@ func (repo *GoGitRepo) FetchRefs(remote string, refSpec string) (string, error)
return buf.String(), nil
}
-// PushRefs push git refs to a remote
-func (repo *GoGitRepo) PushRefs(remote string, refSpec string) (string, error) {
+// PushRefs push git refs matching a directory prefix to a remote
+// Ex: prefix="foo" will push any local refs matching "refs/foo/*" to the remote.
+// The equivalent git refspec would be "refs/foo/*:refs/foo/*"
+//
+// Additionally, PushRefs will update the local references in refs/remotes/<remote>/foo to match
+// the remote state.
+func (repo *GoGitRepo) PushRefs(remote string, prefix string) (string, error) {
+ refspec := fmt.Sprintf("refs/%s/*:refs/%s/*", prefix, prefix)
+
+ remo, err := repo.r.Remote(remote)
+ if err != nil {
+ return "", err
+ }
+
+ // to make sure that the push also create the corresponding refs/remotes/<remote>/... references,
+ // we need to have a default fetch refspec configured on the remote, to make our refs "track" the remote ones.
+ // This does not change the config on disk, only on memory.
+ hasCustomFetch := false
+ fetchRefspec := fmt.Sprintf("refs/%s/*:refs/remotes/%s/%s/*", prefix, remote, prefix)
+ for _, r := range remo.Config().Fetch {
+ if string(r) == fetchRefspec {
+ hasCustomFetch = true
+ break
+ }
+ }
+
+ if !hasCustomFetch {
+ remo.Config().Fetch = append(remo.Config().Fetch, config.RefSpec(fetchRefspec))
+ }
+
buf := bytes.NewBuffer(nil)
- err := repo.r.Push(&gogit.PushOptions{
+ err = remo.Push(&gogit.PushOptions{
RemoteName: remote,
- RefSpecs: []config.RefSpec{config.RefSpec(refSpec)},
+ RefSpecs: []config.RefSpec{config.RefSpec(refspec)},
Progress: buf,
})
if err == gogit.NoErrAlreadyUpToDate {
@@ -519,12 +554,13 @@ func (repo *GoGitRepo) ReadTree(hash Hash) ([]TreeEntry, error) {
}
// StoreCommit will store a Git commit with the given Git tree
-func (repo *GoGitRepo) StoreCommit(treeHash Hash) (Hash, error) {
- return repo.StoreCommitWithParent(treeHash, "")
+func (repo *GoGitRepo) StoreCommit(treeHash Hash, parents ...Hash) (Hash, error) {
+ return repo.StoreSignedCommit(treeHash, nil, parents...)
}
-// StoreCommit will store a Git commit with the given Git tree
-func (repo *GoGitRepo) StoreCommitWithParent(treeHash Hash, parent Hash) (Hash, error) {
+// StoreCommit will store a Git commit with the given Git tree. If signKey is not nil, the commit
+// will be signed accordingly.
+func (repo *GoGitRepo) StoreSignedCommit(treeHash Hash, signKey *openpgp.Entity, parents ...Hash) (Hash, error) {
cfg, err := repo.r.Config()
if err != nil {
return "", err
@@ -545,8 +581,28 @@ func (repo *GoGitRepo) StoreCommitWithParent(treeHash Hash, parent Hash) (Hash,
TreeHash: plumbing.NewHash(treeHash.String()),
}
- if parent != "" {
- commit.ParentHashes = []plumbing.Hash{plumbing.NewHash(parent.String())}
+ for _, parent := range parents {
+ commit.ParentHashes = append(commit.ParentHashes, plumbing.NewHash(parent.String()))
+ }
+
+ // Compute the signature if needed
+ if signKey != nil {
+ // first get the serialized commit
+ encoded := &plumbing.MemoryObject{}
+ if err := commit.Encode(encoded); err != nil {
+ return "", err
+ }
+ r, err := encoded.Reader()
+ if err != nil {
+ return "", err
+ }
+
+ // sign the data
+ var sig bytes.Buffer
+ if err := openpgp.ArmoredDetachSign(&sig, signKey, r, nil); err != nil {
+ return "", err
+ }
+ commit.PGPSignature = sig.String()
}
obj := repo.r.Storer.NewEncodedObject()
@@ -593,6 +649,14 @@ func (repo *GoGitRepo) FindCommonAncestor(commit1 Hash, commit2 Hash) (Hash, err
return Hash(commits[0].Hash.String()), nil
}
+func (repo *GoGitRepo) ResolveRef(ref string) (Hash, error) {
+ r, err := repo.r.Reference(plumbing.ReferenceName(ref), false)
+ if err != nil {
+ return "", err
+ }
+ return Hash(r.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())))
@@ -647,34 +711,79 @@ func (repo *GoGitRepo) CopyRef(source string, dest string) error {
// 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)
+ return nonNativeListCommits(repo, ref)
+}
+
+func (repo *GoGitRepo) ReadCommit(hash Hash) (Commit, error) {
+ commit, err := repo.r.CommitObject(plumbing.NewHash(hash.String()))
if err != nil {
- return nil, err
+ return Commit{}, err
}
- commit, err := repo.r.CommitObject(r.Hash())
- if err != nil {
- return nil, err
+ parents := make([]Hash, len(commit.ParentHashes))
+ for i, parentHash := range commit.ParentHashes {
+ parents[i] = Hash(parentHash.String())
}
- hashes := []Hash{Hash(commit.Hash.String())}
- for {
- commit, err = commit.Parent(0)
- if err == object.ErrParentNotFound {
- break
+ result := Commit{
+ Hash: hash,
+ Parents: parents,
+ TreeHash: Hash(commit.TreeHash.String()),
+ }
+
+ if commit.PGPSignature != "" {
+ // I can't find a way to just remove the signature when reading the encoded commit so we need to
+ // re-encode the commit without signature.
+
+ encoded := &plumbing.MemoryObject{}
+ err := commit.EncodeWithoutSignature(encoded)
+ if err != nil {
+ return Commit{}, err
}
+
+ result.SignedData, err = encoded.Reader()
if err != nil {
- return nil, err
+ return Commit{}, err
}
- if commit.NumParents() > 1 {
- return nil, fmt.Errorf("multiple parents")
+ result.Signature, err = deArmorSignature(strings.NewReader(commit.PGPSignature))
+ if err != nil {
+ return Commit{}, err
}
+ }
- hashes = append([]Hash{Hash(commit.Hash.String())}, hashes...)
+ return result, nil
+}
+
+func (repo *GoGitRepo) AllClocks() (map[string]lamport.Clock, error) {
+ repo.clocksMutex.Lock()
+ defer repo.clocksMutex.Unlock()
+
+ result := make(map[string]lamport.Clock)
+
+ files, err := ioutil.ReadDir(filepath.Join(repo.path, "git-bug", clockPath))
+ if os.IsNotExist(err) {
+ return nil, nil
+ }
+ if err != nil {
+ return nil, err
}
- return hashes, nil
+ for _, file := range files {
+ name := file.Name()
+ if c, ok := repo.clocks[name]; ok {
+ result[name] = c
+ } else {
+ c, err := lamport.LoadPersistedClock(repo.LocalStorage(), filepath.Join(clockPath, name))
+ if err != nil {
+ return nil, err
+ }
+ repo.clocks[name] = c
+ result[name] = c
+ }
+ }
+
+ return result, nil
}
// GetOrCreateClock return a Lamport clock stored in the Repo.
@@ -691,7 +800,7 @@ func (repo *GoGitRepo) GetOrCreateClock(name string) (lamport.Clock, error) {
return nil, err
}
- c, err = lamport.NewPersistedClock(repo.localStorage, name+"-clock")
+ c, err = lamport.NewPersistedClock(repo.LocalStorage(), filepath.Join(clockPath, name))
if err != nil {
return nil, err
}
@@ -705,7 +814,7 @@ func (repo *GoGitRepo) getClock(name string) (lamport.Clock, error) {
return c, nil
}
- c, err := lamport.LoadPersistedClock(repo.localStorage, name+"-clock")
+ c, err := lamport.LoadPersistedClock(repo.LocalStorage(), filepath.Join(clockPath, name))
if err == nil {
repo.clocks[name] = c
return c, nil
@@ -716,6 +825,24 @@ func (repo *GoGitRepo) getClock(name string) (lamport.Clock, error) {
return nil, err
}
+// Increment is equivalent to c = GetOrCreateClock(name) + c.Increment()
+func (repo *GoGitRepo) Increment(name string) (lamport.Time, error) {
+ c, err := repo.GetOrCreateClock(name)
+ if err != nil {
+ return lamport.Time(0), err
+ }
+ return c.Increment()
+}
+
+// Witness is equivalent to c = GetOrCreateClock(name) + c.Witness(time)
+func (repo *GoGitRepo) Witness(name string, time lamport.Time) error {
+ c, err := repo.GetOrCreateClock(name)
+ if err != nil {
+ return err
+ }
+ return c.Witness(time)
+}
+
// 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 {
diff --git a/repository/gogit_config.go b/repository/gogit_config.go
index ba61adca..891e3ffb 100644
--- a/repository/gogit_config.go
+++ b/repository/gogit_config.go
@@ -134,7 +134,7 @@ func (cr *goGitConfigReader) ReadString(key string) (string, error) {
}
return section.Option(optionName), nil
default:
- subsectionName := strings.Join(split[1:len(split)-2], ".")
+ subsectionName := strings.Join(split[1:len(split)-1], ".")
optionName := split[len(split)-1]
if !section.HasSubsection(subsectionName) {
return "", ErrNoConfigEntry
diff --git a/repository/gogit_testing.go b/repository/gogit_testing.go
index a8bff41e..cad776b3 100644
--- a/repository/gogit_testing.go
+++ b/repository/gogit_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 CreateGoGitTestRepo(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 SetupGoGitReposAndRemote() (repoA, repoB, remote TestedRepo) {
diff --git a/repository/keyring.go b/repository/keyring.go
index 4cb3c9ff..6cba303e 100644
--- a/repository/keyring.go
+++ b/repository/keyring.go
@@ -15,7 +15,7 @@ var ErrKeyringKeyNotFound = keyring.ErrKeyNotFound
type Keyring interface {
// Returns an Item matching the key or ErrKeyringKeyNotFound
Get(key string) (Item, error)
- // Stores an Item on the keyring
+ // Stores an Item on the keyring. Set is idempotent.
Set(item Item) error
// Removes the item with matching key
Remove(key string) error
@@ -48,3 +48,13 @@ func defaultKeyring() (Keyring, error) {
},
})
}
+
+// 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/mock_repo.go b/repository/mock_repo.go
index 8a1724ef..2749bfbd 100644
--- a/repository/mock_repo.go
+++ b/repository/mock_repo.go
@@ -1,6 +1,7 @@
package repository
import (
+ "bytes"
"crypto/sha1"
"fmt"
"strings"
@@ -10,15 +11,16 @@ import (
"github.com/blevesearch/bleve"
"github.com/go-git/go-billy/v5"
"github.com/go-git/go-billy/v5/memfs"
+ "golang.org/x/crypto/openpgp"
"github.com/MichaelMure/git-bug/util/lamport"
)
-var _ ClockedRepo = &mockRepoForTest{}
-var _ TestedRepo = &mockRepoForTest{}
+var _ ClockedRepo = &mockRepo{}
+var _ TestedRepo = &mockRepo{}
-// mockRepoForTest defines an instance of Repo that can be used for testing.
-type mockRepoForTest struct {
+// mockRepo defines an instance of Repo that can be used for testing.
+type mockRepo struct {
*mockRepoConfig
*mockRepoKeyring
*mockRepoCommon
@@ -26,12 +28,13 @@ type mockRepoForTest struct {
*mockRepoBleve
*mockRepoData
*mockRepoClock
+ *mockRepoTest
}
-func (m *mockRepoForTest) Close() error { return nil }
+func (m *mockRepo) Close() error { return nil }
-func NewMockRepoForTest() *mockRepoForTest {
- return &mockRepoForTest{
+func NewMockRepo() *mockRepo {
+ return &mockRepo{
mockRepoConfig: NewMockRepoConfig(),
mockRepoKeyring: NewMockRepoKeyring(),
mockRepoCommon: NewMockRepoCommon(),
@@ -39,6 +42,7 @@ func NewMockRepoForTest() *mockRepoForTest {
mockRepoBleve: newMockRepoBleve(),
mockRepoData: NewMockRepoData(),
mockRepoClock: NewMockRepoClock(),
+ mockRepoTest: NewMockRepoTest(),
}
}
@@ -177,7 +181,8 @@ var _ RepoData = &mockRepoData{}
type commit struct {
treeHash Hash
- parent Hash
+ parents []Hash
+ sig string
}
type mockRepoData struct {
@@ -196,13 +201,13 @@ func NewMockRepoData() *mockRepoData {
}
}
-// PushRefs push git refs to a remote
-func (r *mockRepoData) PushRefs(remote string, refSpec string) (string, error) {
- return "", nil
+func (r *mockRepoData) FetchRefs(remote string, prefix string) (string, error) {
+ panic("implement me")
}
-func (r *mockRepoData) FetchRefs(remote string, refSpec string) (string, error) {
- return "", nil
+// PushRefs push git refs to a remote
+func (r *mockRepoData) PushRefs(remote string, prefix string) (string, error) {
+ panic("implement me")
}
func (r *mockRepoData) StoreData(data []byte) (Hash, error) {
@@ -214,7 +219,6 @@ func (r *mockRepoData) StoreData(data []byte) (Hash, error) {
func (r *mockRepoData) ReadData(hash Hash) ([]byte, error) {
data, ok := r.blobs[hash]
-
if !ok {
return nil, fmt.Errorf("unknown hash")
}
@@ -231,48 +235,103 @@ func (r *mockRepoData) StoreTree(entries []TreeEntry) (Hash, error) {
return hash, nil
}
-func (r *mockRepoData) StoreCommit(treeHash Hash) (Hash, error) {
- rawHash := sha1.Sum([]byte(treeHash))
- hash := Hash(fmt.Sprintf("%x", rawHash))
- r.commits[hash] = commit{
- treeHash: treeHash,
+func (r *mockRepoData) ReadTree(hash Hash) ([]TreeEntry, error) {
+ var data string
+
+ data, ok := r.trees[hash]
+
+ if !ok {
+ // Git will understand a commit hash to reach a tree
+ commit, ok := r.commits[hash]
+
+ if !ok {
+ return nil, fmt.Errorf("unknown hash")
+ }
+
+ data, ok = r.trees[commit.treeHash]
+
+ if !ok {
+ return nil, fmt.Errorf("unknown hash")
+ }
}
- return hash, nil
+
+ return readTreeEntries(data)
+}
+
+func (r *mockRepoData) StoreCommit(treeHash Hash, parents ...Hash) (Hash, error) {
+ return r.StoreSignedCommit(treeHash, nil, parents...)
}
-func (r *mockRepoData) StoreCommitWithParent(treeHash Hash, parent Hash) (Hash, error) {
- rawHash := sha1.Sum([]byte(treeHash + parent))
+func (r *mockRepoData) StoreSignedCommit(treeHash Hash, signKey *openpgp.Entity, parents ...Hash) (Hash, error) {
+ hasher := sha1.New()
+ hasher.Write([]byte(treeHash))
+ for _, parent := range parents {
+ hasher.Write([]byte(parent))
+ }
+ rawHash := hasher.Sum(nil)
hash := Hash(fmt.Sprintf("%x", rawHash))
- r.commits[hash] = commit{
+ c := commit{
treeHash: treeHash,
- parent: parent,
+ parents: parents,
+ }
+ if signKey != nil {
+ // unlike go-git, we only sign the tree hash for simplicity instead of all the fields (parents ...)
+ var sig bytes.Buffer
+ if err := openpgp.DetachSign(&sig, signKey, strings.NewReader(string(treeHash)), nil); err != nil {
+ return "", err
+ }
+ c.sig = sig.String()
}
+ r.commits[hash] = c
return hash, nil
}
-func (r *mockRepoData) UpdateRef(ref string, hash Hash) error {
- r.refs[ref] = hash
- return nil
-}
+func (r *mockRepoData) ReadCommit(hash Hash) (Commit, error) {
+ c, ok := r.commits[hash]
+ if !ok {
+ return Commit{}, fmt.Errorf("unknown commit")
+ }
-func (r *mockRepoData) RemoveRef(ref string) error {
- delete(r.refs, ref)
- return nil
-}
+ result := Commit{
+ Hash: hash,
+ Parents: c.parents,
+ TreeHash: c.treeHash,
+ }
-func (r *mockRepoData) RefExist(ref string) (bool, error) {
- _, exist := r.refs[ref]
- return exist, nil
+ if c.sig != "" {
+ // Note: this is actually incorrect as the signed data should be the full commit (+comment, +date ...)
+ // but only the tree hash work for our purpose here.
+ result.SignedData = strings.NewReader(string(c.treeHash))
+ result.Signature = strings.NewReader(c.sig)
+ }
+
+ return result, nil
}
-func (r *mockRepoData) CopyRef(source string, dest string) error {
- hash, exist := r.refs[source]
+func (r *mockRepoData) GetTreeHash(commit Hash) (Hash, error) {
+ c, ok := r.commits[commit]
+ if !ok {
+ return "", fmt.Errorf("unknown commit")
+ }
- if !exist {
- return fmt.Errorf("Unknown ref")
+ return c.treeHash, nil
+}
+
+func (r *mockRepoData) ResolveRef(ref string) (Hash, error) {
+ h, ok := r.refs[ref]
+ if !ok {
+ return "", fmt.Errorf("unknown ref")
}
+ return h, nil
+}
- r.refs[dest] = hash
+func (r *mockRepoData) UpdateRef(ref string, hash Hash) error {
+ r.refs[ref] = hash
+ return nil
+}
+
+func (r *mockRepoData) RemoveRef(ref string) error {
+ delete(r.refs, ref)
return nil
}
@@ -288,46 +347,20 @@ func (r *mockRepoData) ListRefs(refPrefix string) ([]string, error) {
return keys, nil
}
-func (r *mockRepoData) ListCommits(ref string) ([]Hash, error) {
- var hashes []Hash
-
- hash := r.refs[ref]
-
- for {
- commit, ok := r.commits[hash]
-
- if !ok {
- break
- }
-
- hashes = append([]Hash{hash}, hashes...)
- hash = commit.parent
- }
-
- return hashes, nil
+func (r *mockRepoData) RefExist(ref string) (bool, error) {
+ _, exist := r.refs[ref]
+ return exist, nil
}
-func (r *mockRepoData) ReadTree(hash Hash) ([]TreeEntry, error) {
- var data string
-
- data, ok := r.trees[hash]
-
- if !ok {
- // Git will understand a commit hash to reach a tree
- commit, ok := r.commits[hash]
-
- if !ok {
- return nil, fmt.Errorf("unknown hash")
- }
-
- data, ok = r.trees[commit.treeHash]
+func (r *mockRepoData) CopyRef(source string, dest string) error {
+ hash, exist := r.refs[source]
- if !ok {
- return nil, fmt.Errorf("unknown hash")
- }
+ if !exist {
+ return fmt.Errorf("Unknown ref")
}
- return readTreeEntries(data)
+ r.refs[dest] = hash
+ return nil
}
func (r *mockRepoData) FindCommonAncestor(hash1 Hash, hash2 Hash) (Hash, error) {
@@ -338,8 +371,11 @@ func (r *mockRepoData) FindCommonAncestor(hash1 Hash, hash2 Hash) (Hash, error)
if !ok {
return "", fmt.Errorf("unknown commit %v", hash1)
}
- ancestor1 = append(ancestor1, c.parent)
- hash1 = c.parent
+ if len(c.parents) == 0 {
+ break
+ }
+ ancestor1 = append(ancestor1, c.parents[0])
+ hash1 = c.parents[0]
}
for {
@@ -354,35 +390,19 @@ func (r *mockRepoData) FindCommonAncestor(hash1 Hash, hash2 Hash) (Hash, error)
return "", fmt.Errorf("unknown commit %v", hash1)
}
- if c.parent == "" {
+ if c.parents[0] == "" {
return "", fmt.Errorf("no ancestor found")
}
- hash2 = c.parent
- }
-}
-
-func (r *mockRepoData) GetTreeHash(commit Hash) (Hash, error) {
- c, ok := r.commits[commit]
- if !ok {
- return "", fmt.Errorf("unknown commit")
+ hash2 = c.parents[0]
}
-
- return c.treeHash, nil
}
-func (r *mockRepoData) AddRemote(name string, url string) error {
- panic("implement me")
-}
-
-func (m mockRepoForTest) GetLocalRemote() string {
- panic("implement me")
+func (r *mockRepoData) ListCommits(ref string) ([]Hash, error) {
+ return nonNativeListCommits(r, ref)
}
-func (m mockRepoForTest) EraseFromDisk() error {
- // nothing to do
- return nil
-}
+var _ RepoClock = &mockRepoClock{}
type mockRepoClock struct {
mu sync.Mutex
@@ -395,6 +415,10 @@ func NewMockRepoClock() *mockRepoClock {
}
}
+func (r *mockRepoClock) AllClocks() (map[string]lamport.Clock, error) {
+ return r.clocks, nil
+}
+
func (r *mockRepoClock) GetOrCreateClock(name string) (lamport.Clock, error) {
r.mu.Lock()
defer r.mu.Unlock()
@@ -407,3 +431,40 @@ func (r *mockRepoClock) GetOrCreateClock(name string) (lamport.Clock, error) {
r.clocks[name] = c
return c, nil
}
+
+func (r *mockRepoClock) Increment(name string) (lamport.Time, error) {
+ c, err := r.GetOrCreateClock(name)
+ if err != nil {
+ return lamport.Time(0), err
+ }
+ return c.Increment()
+}
+
+func (r *mockRepoClock) Witness(name string, time lamport.Time) error {
+ c, err := r.GetOrCreateClock(name)
+ if err != nil {
+ return err
+ }
+ return c.Witness(time)
+}
+
+var _ repoTest = &mockRepoTest{}
+
+type mockRepoTest struct{}
+
+func NewMockRepoTest() *mockRepoTest {
+ return &mockRepoTest{}
+}
+
+func (r *mockRepoTest) AddRemote(name string, url string) error {
+ panic("implement me")
+}
+
+func (r mockRepoTest) GetLocalRemote() string {
+ panic("implement me")
+}
+
+func (r mockRepoTest) EraseFromDisk() error {
+ // nothing to do
+ return nil
+}
diff --git a/repository/mock_repo_test.go b/repository/mock_repo_test.go
index b56b94f2..12851a80 100644
--- a/repository/mock_repo_test.go
+++ b/repository/mock_repo_test.go
@@ -1,9 +1,11 @@
package repository
-import "testing"
+import (
+ "testing"
+)
func TestMockRepo(t *testing.T) {
- creator := func(bare bool) TestedRepo { return NewMockRepoForTest() }
+ creator := func(bare bool) TestedRepo { return NewMockRepo() }
cleaner := func(repos ...Repo) {}
RepoTest(t, creator, cleaner)
diff --git a/repository/repo.go b/repository/repo.go
index eb9296d4..80bb7ce7 100644
--- a/repository/repo.go
+++ b/repository/repo.go
@@ -3,15 +3,17 @@ package repository
import (
"errors"
+ "io"
"github.com/blevesearch/bleve"
"github.com/go-git/go-billy/v5"
+ "golang.org/x/crypto/openpgp"
"github.com/MichaelMure/git-bug/util/lamport"
)
var (
- // ErrNotARepo is the error returned when the git repo root wan't be found
+ // ErrNotARepo is the error returned when the git repo root can'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")
@@ -22,9 +24,9 @@ type Repo interface {
RepoConfig
RepoKeyring
RepoCommon
- RepoData
RepoStorage
RepoBleve
+ RepoData
Close() error
}
@@ -88,13 +90,28 @@ type RepoBleve interface {
ClearBleveIndex(name string) error
}
+type Commit struct {
+ Hash Hash
+ Parents []Hash // hashes of the parents, if any
+ TreeHash Hash // hash of the git Tree
+ SignedData io.Reader // if signed, reader for the signed data (likely, the serialized commit)
+ Signature io.Reader // if signed, reader for the (non-armored) signature
+}
+
// 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)
-
- // PushRefs push git refs to a remote
- PushRefs(remote string, refSpec string) (string, error)
+ // FetchRefs fetch git refs matching a directory prefix to a remote
+ // Ex: prefix="foo" will fetch any remote refs matching "refs/foo/*" locally.
+ // The equivalent git refspec would be "refs/foo/*:refs/remotes/<remote>/foo/*"
+ FetchRefs(remote string, prefix string) (string, error)
+
+ // PushRefs push git refs matching a directory prefix to a remote
+ // Ex: prefix="foo" will push any local refs matching "refs/foo/*" to the remote.
+ // The equivalent git refspec would be "refs/foo/*:refs/foo/*"
+ //
+ // Additionally, PushRefs will update the local references in refs/remotes/<remote>/foo to match
+ // the remote state.
+ PushRefs(remote string, prefix string) (string, error)
// StoreData will store arbitrary data and return the corresponding hash
StoreData(data []byte) (Hash, error)
@@ -110,21 +127,27 @@ type RepoData interface {
ReadTree(hash Hash) ([]TreeEntry, error)
// StoreCommit will store a Git commit with the given Git tree
- StoreCommit(treeHash Hash) (Hash, error)
+ StoreCommit(treeHash Hash, parents ...Hash) (Hash, error)
- // StoreCommit will store a Git commit with the given Git tree
- StoreCommitWithParent(treeHash Hash, parent Hash) (Hash, error)
+ // StoreCommit will store a Git commit with the given Git tree. If signKey is not nil, the commit
+ // will be signed accordingly.
+ StoreSignedCommit(treeHash Hash, signKey *openpgp.Entity, parents ...Hash) (Hash, error)
+
+ // ReadCommit read a Git commit and returns some of its characteristic
+ ReadCommit(hash Hash) (Commit, error)
// GetTreeHash return the git tree hash referenced in a commit
+ // Deprecated
GetTreeHash(commit Hash) (Hash, error)
- // FindCommonAncestor will return the last common ancestor of two chain of commit
- FindCommonAncestor(commit1 Hash, commit2 Hash) (Hash, error)
+ // ResolveRef returns the hash of the target commit of the given ref
+ ResolveRef(ref string) (Hash, error)
// UpdateRef will create or update a Git reference
UpdateRef(ref string, hash Hash) error
// RemoveRef will remove a Git reference
+ // RemoveRef is idempotent.
RemoveRef(ref string) error
// ListRefs will return a list of Git ref matching the given refspec
@@ -136,15 +159,28 @@ type RepoData interface {
// CopyRef will create a new reference with the same value as another one
CopyRef(source string, dest string) error
+ // FindCommonAncestor will return the last common ancestor of two chain of commit
+ // Deprecated
+ FindCommonAncestor(commit1 Hash, commit2 Hash) (Hash, error)
+
// ListCommits will return the list of tree hashes of a ref, in chronological order
ListCommits(ref string) ([]Hash, error)
}
// RepoClock give access to Lamport clocks
type RepoClock interface {
+ // AllClocks return all the known clocks
+ AllClocks() (map[string]lamport.Clock, 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)
+
+ // Increment is equivalent to c = GetOrCreateClock(name) + c.Increment()
+ Increment(name string) (lamport.Time, error)
+
+ // Witness is equivalent to c = GetOrCreateClock(name) + c.Witness(time)
+ Witness(name string, time lamport.Time) error
}
// ClockLoader hold which logical clock need to exist for an entity and
diff --git a/repository/repo_testing.go b/repository/repo_testing.go
index c0e1fa79..1f80d898 100644
--- a/repository/repo_testing.go
+++ b/repository/repo_testing.go
@@ -6,10 +6,14 @@ import (
"testing"
"github.com/stretchr/testify/require"
+ "golang.org/x/crypto/openpgp"
"github.com/MichaelMure/git-bug/util/lamport"
)
+// TODO: add tests for RepoBleve
+// TODO: add tests for RepoStorage
+
func CleanupTestRepos(repos ...Repo) {
var firstErr error
for _, repo := range repos {
@@ -44,6 +48,7 @@ func RepoTest(t *testing.T, creator RepoCreator, cleaner RepoCleaner) {
t.Run("Data", func(t *testing.T) {
RepoDataTest(t, repo)
+ RepoDataSignatureTest(t, repo)
})
t.Run("Config", func(t *testing.T) {
@@ -135,7 +140,8 @@ func RepoDataTest(t *testing.T, repo RepoData) {
require.NoError(t, err)
require.Equal(t, treeHash1, treeHash1Read)
- commit2, err := repo.StoreCommitWithParent(treeHash2, commit1)
+ // commit with a parent
+ commit2, err := repo.StoreCommit(treeHash2, commit1)
require.NoError(t, err)
require.True(t, commit2.IsValid())
@@ -148,6 +154,11 @@ func RepoDataTest(t *testing.T, repo RepoData) {
require.NoError(t, err)
require.Equal(t, tree1read, tree1)
+ c2, err := repo.ReadCommit(commit2)
+ require.NoError(t, err)
+ c2expected := Commit{Hash: commit2, Parents: []Hash{commit1}, TreeHash: treeHash2}
+ require.Equal(t, c2expected, c2)
+
// Ref
exist1, err := repo.RefExist("refs/bugs/ref1")
@@ -161,6 +172,10 @@ func RepoDataTest(t *testing.T, repo RepoData) {
require.NoError(t, err)
require.True(t, exist1)
+ h, err := repo.ResolveRef("refs/bugs/ref1")
+ require.NoError(t, err)
+ require.Equal(t, commit2, h)
+
ls, err := repo.ListRefs("refs/bugs")
require.NoError(t, err)
require.ElementsMatch(t, []string{"refs/bugs/ref1"}, ls)
@@ -178,7 +193,7 @@ func RepoDataTest(t *testing.T, repo RepoData) {
// Graph
- commit3, err := repo.StoreCommitWithParent(treeHash1, commit1)
+ commit3, err := repo.StoreCommit(treeHash1, commit1)
require.NoError(t, err)
ancestorHash, err := repo.FindCommonAncestor(commit2, commit3)
@@ -187,17 +202,73 @@ func RepoDataTest(t *testing.T, repo RepoData) {
err = repo.RemoveRef("refs/bugs/ref1")
require.NoError(t, err)
+
+ // RemoveRef is idempotent
+ err = repo.RemoveRef("refs/bugs/ref1")
+ require.NoError(t, err)
+}
+
+func RepoDataSignatureTest(t *testing.T, repo RepoData) {
+ data := randomData()
+
+ blobHash, err := repo.StoreData(data)
+ require.NoError(t, err)
+
+ treeHash, err := repo.StoreTree([]TreeEntry{
+ {
+ ObjectType: Blob,
+ Hash: blobHash,
+ Name: "blob",
+ },
+ })
+ require.NoError(t, err)
+
+ pgpEntity1, err := openpgp.NewEntity("", "", "", nil)
+ require.NoError(t, err)
+ keyring1 := openpgp.EntityList{pgpEntity1}
+
+ pgpEntity2, err := openpgp.NewEntity("", "", "", nil)
+ require.NoError(t, err)
+ keyring2 := openpgp.EntityList{pgpEntity2}
+
+ commitHash1, err := repo.StoreSignedCommit(treeHash, pgpEntity1)
+ require.NoError(t, err)
+
+ commit1, err := repo.ReadCommit(commitHash1)
+ require.NoError(t, err)
+
+ _, err = openpgp.CheckDetachedSignature(keyring1, commit1.SignedData, commit1.Signature)
+ require.NoError(t, err)
+
+ _, err = openpgp.CheckDetachedSignature(keyring2, commit1.SignedData, commit1.Signature)
+ require.Error(t, err)
+
+ commitHash2, err := repo.StoreSignedCommit(treeHash, pgpEntity1, commitHash1)
+ require.NoError(t, err)
+
+ commit2, err := repo.ReadCommit(commitHash2)
+ require.NoError(t, err)
+
+ _, err = openpgp.CheckDetachedSignature(keyring1, commit2.SignedData, commit2.Signature)
+ require.NoError(t, err)
+
+ _, err = openpgp.CheckDetachedSignature(keyring2, commit2.SignedData, commit2.Signature)
+ require.Error(t, err)
}
// helper to test a RepoClock
func RepoClockTest(t *testing.T, repo RepoClock) {
+ allClocks, err := repo.AllClocks()
+ require.NoError(t, err)
+ require.Len(t, allClocks, 0)
+
clock, err := repo.GetOrCreateClock("foo")
require.NoError(t, err)
require.Equal(t, lamport.Time(1), clock.Time())
time, err := clock.Increment()
require.NoError(t, err)
- require.Equal(t, lamport.Time(1), time)
+ require.Equal(t, lamport.Time(2), time)
require.Equal(t, lamport.Time(2), clock.Time())
clock2, err := repo.GetOrCreateClock("foo")
@@ -207,6 +278,13 @@ func RepoClockTest(t *testing.T, repo RepoClock) {
clock3, err := repo.GetOrCreateClock("bar")
require.NoError(t, err)
require.Equal(t, lamport.Time(1), clock3.Time())
+
+ allClocks, err = repo.AllClocks()
+ require.NoError(t, err)
+ require.Equal(t, map[string]lamport.Clock{
+ "foo": clock,
+ "bar": clock3,
+ }, allClocks)
}
func randomData() []byte {
diff --git a/repository/tree_entry.go b/repository/tree_entry.go
index 6c5ec1a5..9d70814c 100644
--- a/repository/tree_entry.go
+++ b/repository/tree_entry.go
@@ -100,3 +100,13 @@ func readTreeEntries(s string) ([]TreeEntry, error) {
return casted, nil
}
+
+// SearchTreeEntry search a TreeEntry by name from an array
+func SearchTreeEntry(entries []TreeEntry, name string) (TreeEntry, bool) {
+ for _, entry := range entries {
+ if entry.Name == name {
+ return entry, true
+ }
+ }
+ return TreeEntry{}, false
+}