aboutsummaryrefslogtreecommitdiffstats
path: root/repository
diff options
context:
space:
mode:
authorMichael Muré <batolettre@gmail.com>2020-06-26 19:25:17 +0200
committerMichael Muré <batolettre@gmail.com>2020-09-29 20:42:20 +0200
commitd171e11028f5993137a5f83beb7fe002bed866f5 (patch)
tree3333e98b12bc47473c38a77142bf4fbccd72488d /repository
parent9f3a56b1f34a8b4a7a75357986e967afc4b96611 (diff)
downloadgit-bug-d171e11028f5993137a5f83beb7fe002bed866f5.tar.gz
repository: partial impl of a go-git backed Repo
Diffstat (limited to 'repository')
-rw-r--r--repository/gogit.go378
-rw-r--r--repository/gogit_config.go175
-rw-r--r--repository/gogit_test.go68
-rw-r--r--repository/gogit_testing.go58
4 files changed, 679 insertions, 0 deletions
diff --git a/repository/gogit.go b/repository/gogit.go
new file mode 100644
index 00000000..71a7e6d0
--- /dev/null
+++ b/repository/gogit.go
@@ -0,0 +1,378 @@
+package repository
+
+import (
+ "bytes"
+ "fmt"
+ "io/ioutil"
+ "os"
+ stdpath "path"
+ "path/filepath"
+ "sync"
+
+ 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/MichaelMure/git-bug/util/lamport"
+)
+
+var _ ClockedRepo = &GoGitRepo{}
+
+type GoGitRepo struct {
+ r *gogit.Repository
+ path string
+
+ clocksMutex sync.Mutex
+ clocks map[string]lamport.Clock
+}
+
+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
+ }
+
+ repo := &GoGitRepo{
+ r: r,
+ path: path,
+ clocks: make(map[string]lamport.Clock),
+ }
+
+ 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
+}
+
+func (repo *GoGitRepo) LocalConfig() Config {
+ return newGoGitConfig(repo.r)
+}
+
+func (repo *GoGitRepo) GlobalConfig() Config {
+ panic("go-git doesn't support writing global config")
+}
+
+// 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) {
+
+ panic("implement me")
+}
+
+// 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)
+}
+
+func (repo *GoGitRepo) StoreTree(mapping []TreeEntry) (Hash, error) {
+ panic("implement me")
+}
+
+func (repo *GoGitRepo) ReadTree(hash Hash) ([]TreeEntry, error) {
+ panic("implement me")
+}
+
+func (repo *GoGitRepo) StoreCommit(treeHash Hash) (Hash, error) {
+ panic("implement me")
+}
+
+func (repo *GoGitRepo) StoreCommitWithParent(treeHash Hash, parent Hash) (Hash, error) {
+ panic("implement me")
+}
+
+func (repo *GoGitRepo) GetTreeHash(commit Hash) (Hash, error) {
+ panic("implement me")
+}
+
+func (repo *GoGitRepo) FindCommonAncestor(commit1 Hash, commit2 Hash) (Hash, error) {
+ panic("implement me")
+}
+
+func (repo *GoGitRepo) UpdateRef(ref string, hash Hash) error {
+ panic("implement me")
+}
+
+func (repo *GoGitRepo) RemoveRef(ref string) error {
+ panic("implement me")
+}
+
+func (repo *GoGitRepo) ListRefs(refspec string) ([]string, error) {
+ panic("implement me")
+}
+
+func (repo *GoGitRepo) RefExist(ref string) (bool, error) {
+ panic("implement me")
+}
+
+func (repo *GoGitRepo) CopyRef(source string, dest string) error {
+ panic("implement me")
+}
+
+func (repo *GoGitRepo) ListCommits(ref string) ([]Hash, error) {
+ panic("implement me")
+}
+
+// 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 := 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 := 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..0f91b092
--- /dev/null
+++ b/repository/gogit_config.go
@@ -0,0 +1,175 @@
+package repository
+
+import (
+ "fmt"
+ "strconv"
+ "strings"
+ "time"
+
+ gogit "github.com/go-git/go-git/v5"
+ "github.com/go-git/go-git/v5/plumbing/format/config"
+)
+
+var _ Config = &goGitConfig{}
+
+type goGitConfig struct {
+ repo *gogit.Repository
+}
+
+func newGoGitConfig(repo *gogit.Repository) *goGitConfig {
+ return &goGitConfig{repo: repo}
+}
+
+func (ggc *goGitConfig) StoreString(key, value string) error {
+ cfg, err := ggc.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)-2], ".")
+ option := split[len(split)-1]
+ cfg.Raw.Section(section).Subsection(subsection).SetOption(option, value)
+ }
+
+ return ggc.repo.SetConfig(cfg)
+}
+
+func (ggc *goGitConfig) StoreTimestamp(key string, value time.Time) error {
+ return ggc.StoreString(key, strconv.Itoa(int(value.Unix())))
+}
+
+func (ggc *goGitConfig) StoreBool(key string, value bool) error {
+ return ggc.StoreString(key, strconv.FormatBool(value))
+}
+
+func (ggc *goGitConfig) ReadAll(keyPrefix string) (map[string]string, error) {
+ cfg, err := ggc.repo.Config()
+ if err != nil {
+ return nil, err
+ }
+
+ split := strings.Split(keyPrefix, ".")
+
+ var opts config.Options
+
+ switch {
+ case len(split) < 1:
+ return nil, fmt.Errorf("invalid key prefix")
+ case len(split) == 1:
+ opts = cfg.Raw.Section(split[0]).Options
+ default:
+ section := split[0]
+ subsection := strings.Join(split[1:len(split)-1], ".")
+ opts = cfg.Raw.Section(section).Subsection(subsection).Options
+ }
+
+ if len(opts) == 0 {
+ return nil, fmt.Errorf("invalid section")
+ }
+
+ if keyPrefix[len(keyPrefix)-1:] != "." {
+ keyPrefix += "."
+ }
+
+ result := make(map[string]string, len(opts))
+ for _, opt := range opts {
+ result[keyPrefix+opt.Key] = opt.Value
+ }
+
+ return result, nil
+}
+
+func (ggc *goGitConfig) ReadBool(key string) (bool, error) {
+ val, err := ggc.ReadString(key)
+ if err != nil {
+ return false, err
+ }
+
+ return strconv.ParseBool(val)
+}
+
+func (ggc *goGitConfig) ReadString(key string) (string, error) {
+ cfg, err := ggc.repo.Config()
+ if err != nil {
+ return "", err
+ }
+
+ split := strings.Split(key, ".")
+
+ // TODO: return ErrNoConfigEntry and ErrMultipleConfigEntry
+ // Can use forked go-git: https://github.com/go-git/go-git/pull/112
+
+ switch {
+ case len(split) <= 1:
+ return "", fmt.Errorf("invalid key")
+ case len(split) == 2:
+ return cfg.Raw.Section(split[0]).Option(split[1]), nil
+ default:
+ section := split[0]
+ subsection := strings.Join(split[1:len(split)-2], ".")
+ option := split[len(split)-1]
+ return cfg.Raw.Section(section).Subsection(subsection).Option(option), nil
+ }
+}
+
+func (ggc *goGitConfig) ReadTimestamp(key string) (time.Time, error) {
+ value, err := ggc.ReadString(key)
+ if err != nil {
+ return time.Time{}, err
+ }
+ return ParseTimestamp(value)
+}
+
+func (ggc *goGitConfig) RemoveAll(keyPrefix string) error {
+ cfg, err := ggc.repo.Config()
+ if err != nil {
+ return err
+ }
+
+ split := strings.Split(keyPrefix, ".")
+
+ // missing in go-git
+ hasOption := func(options config.Options, key string) bool {
+ for _, option := range options {
+ if option.IsKey(key) {
+ return true
+ }
+ }
+ return false
+ }
+
+ switch {
+ case len(split) < 1:
+ return fmt.Errorf("invalid key prefix")
+ case len(split) == 1:
+ if len(cfg.Raw.Section(split[0]).Options) > 0 {
+ cfg.Raw.RemoveSection(split[0])
+ } else {
+ return fmt.Errorf("invalid key prefix")
+ }
+ default:
+ section := split[0]
+ rest := strings.Join(split[1:], ".")
+
+ if cfg.Raw.Section(section).HasSubsection(rest) {
+ cfg.Raw.RemoveSubsection(section, rest)
+ } else {
+ if hasOption(cfg.Raw.Section(section).Options, rest) {
+ cfg.Raw.Section(section).RemoveOption(rest)
+ } else {
+ return fmt.Errorf("invalid key prefix")
+ }
+ }
+ }
+
+ return ggc.repo.SetConfig(cfg)
+}
diff --git a/repository/gogit_test.go b/repository/gogit_test.go
new file mode 100644
index 00000000..9dcf109f
--- /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, tc.outPath, 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
+}