Merge pull request #240 from MichaelMure/repo-config

Michael Muré created

repository config interface and implementation rework

Change summary

bridge/core/bridge.go     |  10 
cache/repo_cache.go       |  35 +----
commands/webui.go         |   2 
identity/identity.go      |   8 
repository/config.go      |  49 ++++++++
repository/config_git.go  | 225 +++++++++++++++++++++++++++++++++++++++++
repository/config_mem.go  |  84 +++++++++++++++
repository/git.go         | 191 ++--------------------------------
repository/git_test.go    |  33 +++--
repository/git_testing.go |   5 
repository/mock_repo.go   |  74 +++----------
repository/repo.go        |  21 ---
12 files changed, 435 insertions(+), 302 deletions(-)

Detailed changes

bridge/core/bridge.go 🔗

@@ -134,7 +134,7 @@ func DefaultBridge(repo *cache.RepoCache) (*Bridge, error) {
 // ConfiguredBridges return the list of bridge that are configured for the given
 // repo
 func ConfiguredBridges(repo repository.RepoCommon) ([]string, error) {
-	configs, err := repo.ReadConfigs(bridgeConfigKeyPrefix + ".")
+	configs, err := repo.LocalConfig().ReadAll(bridgeConfigKeyPrefix + ".")
 	if err != nil {
 		return nil, errors.Wrap(err, "can't read configured bridges")
 	}
@@ -171,7 +171,7 @@ func ConfiguredBridges(repo repository.RepoCommon) ([]string, error) {
 func BridgeExist(repo repository.RepoCommon, name string) bool {
 	keyPrefix := fmt.Sprintf("git-bug.bridge.%s.", name)
 
-	conf, err := repo.ReadConfigs(keyPrefix)
+	conf, err := repo.LocalConfig().ReadAll(keyPrefix)
 
 	return err == nil && len(conf) > 0
 }
@@ -188,7 +188,7 @@ func RemoveBridge(repo repository.RepoCommon, name string) error {
 	}
 
 	keyPrefix := fmt.Sprintf("git-bug.bridge.%s", name)
-	return repo.RmConfigs(keyPrefix)
+	return repo.LocalConfig().RemoveAll(keyPrefix)
 }
 
 // Configure run the target specific configuration process
@@ -211,7 +211,7 @@ func (b *Bridge) storeConfig(conf Configuration) error {
 	for key, val := range conf {
 		storeKey := fmt.Sprintf("git-bug.bridge.%s.%s", b.Name, key)
 
-		err := b.repo.StoreConfig(storeKey, val)
+		err := b.repo.LocalConfig().StoreString(storeKey, val)
 		if err != nil {
 			return errors.Wrap(err, "error while storing bridge configuration")
 		}
@@ -235,7 +235,7 @@ func (b *Bridge) ensureConfig() error {
 func loadConfig(repo repository.RepoCommon, name string) (Configuration, error) {
 	keyPrefix := fmt.Sprintf("git-bug.bridge.%s.", name)
 
-	pairs, err := repo.ReadConfigs(keyPrefix)
+	pairs, err := repo.LocalConfig().ReadAll(keyPrefix)
 	if err != nil {
 		return nil, errors.Wrap(err, "error while reading bridge configuration")
 	}

cache/repo_cache.go 🔗

@@ -99,6 +99,16 @@ func NewRepoCache(r repository.ClockedRepo) (*RepoCache, error) {
 	return c, c.write()
 }
 
+// LocalConfig give access to the repository scoped configuration
+func (c *RepoCache) LocalConfig() repository.Config {
+	return c.repo.LocalConfig()
+}
+
+// GlobalConfig give access to the git global configuration
+func (c *RepoCache) GlobalConfig() repository.Config {
+	return c.repo.GlobalConfig()
+}
+
 // GetPath returns the path to the repo.
 func (c *RepoCache) GetPath() string {
 	return c.repo.GetPath()
@@ -124,31 +134,6 @@ func (c *RepoCache) GetUserEmail() (string, error) {
 	return c.repo.GetUserEmail()
 }
 
-// StoreConfig store a single key/value pair in the config of the repo
-func (c *RepoCache) StoreConfig(key string, value string) error {
-	return c.repo.StoreConfig(key, value)
-}
-
-// ReadConfigs read all key/value pair matching the key prefix
-func (c *RepoCache) ReadConfigs(keyPrefix string) (map[string]string, error) {
-	return c.repo.ReadConfigs(keyPrefix)
-}
-
-// ReadConfigBool read a single boolean value from the config
-func (c *RepoCache) ReadConfigBool(key string) (bool, error) {
-	return c.repo.ReadConfigBool(key)
-}
-
-// ReadConfigBool read a single string value from the config
-func (c *RepoCache) ReadConfigString(key string) (string, error) {
-	return c.repo.ReadConfigString(key)
-}
-
-// RmConfigs remove all key/value pair matching the key prefix
-func (c *RepoCache) RmConfigs(keyPrefix string) error {
-	return c.repo.RmConfigs(keyPrefix)
-}
-
 func (c *RepoCache) lock() error {
 	lockPath := repoLockFilePath(c.repo)
 

commands/webui.go 🔗

@@ -100,7 +100,7 @@ func runWebUI(cmd *cobra.Command, args []string) error {
 	fmt.Printf("Graphql Playground: http://%s/playground\n", addr)
 	fmt.Println("Press Ctrl+c to quit")
 
-	configOpen, err := repo.ReadConfigBool(webUIOpenConfigKey)
+	configOpen, err := repo.LocalConfig().ReadBool(webUIOpenConfigKey)
 	if err == repository.ErrNoConfigEntry {
 		// default to true
 		configOpen = true

identity/identity.go 🔗

@@ -220,7 +220,7 @@ func NewFromGitUser(repo repository.Repo) (*Identity, error) {
 
 // IsUserIdentitySet tell if the user identity is correctly set.
 func IsUserIdentitySet(repo repository.RepoCommon) (bool, error) {
-	configs, err := repo.ReadConfigs(identityConfigKey)
+	configs, err := repo.LocalConfig().ReadAll(identityConfigKey)
 	if err != nil {
 		return false, err
 	}
@@ -234,12 +234,12 @@ func IsUserIdentitySet(repo repository.RepoCommon) (bool, error) {
 
 // SetUserIdentity store the user identity's id in the git config
 func SetUserIdentity(repo repository.RepoCommon, identity *Identity) error {
-	return repo.StoreConfig(identityConfigKey, identity.Id().String())
+	return repo.LocalConfig().StoreString(identityConfigKey, identity.Id().String())
 }
 
 // GetUserIdentity read the current user identity, set with a git config entry
 func GetUserIdentity(repo repository.Repo) (*Identity, error) {
-	configs, err := repo.ReadConfigs(identityConfigKey)
+	configs, err := repo.LocalConfig().ReadAll(identityConfigKey)
 	if err != nil {
 		return nil, err
 	}
@@ -263,7 +263,7 @@ func GetUserIdentity(repo repository.Repo) (*Identity, error) {
 
 	i, err := ReadLocal(repo, id)
 	if err == ErrIdentityNotExist {
-		innerErr := repo.RmConfigs(identityConfigKey)
+		innerErr := repo.LocalConfig().RemoveAll(identityConfigKey)
 		if innerErr != nil {
 			_, _ = fmt.Fprintln(os.Stderr, errors.Wrap(innerErr, "can't clear user identity").Error())
 		}

repository/config.go 🔗

@@ -0,0 +1,49 @@
+package repository
+
+import (
+	"strconv"
+	"time"
+)
+
+// 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
+
+	// ReadAll reads all key/value pair matching the key prefix
+	ReadAll(keyPrefix string) (map[string]string, error)
+
+	// ReadBool read a single boolean value from the config
+	// Return ErrNoConfigEntry or ErrMultipleConfigEntry if
+	// there is zero or more than one entry for this key
+	ReadBool(key string) (bool, error)
+
+	// ReadBool read a single string value from the config
+	// Return ErrNoConfigEntry or ErrMultipleConfigEntry if
+	// there is zero or more than one entry for this key
+	ReadString(key string) (string, error)
+
+	// ReadTimestamp read a single timestamp value from the config
+	// Return ErrNoConfigEntry or ErrMultipleConfigEntry if
+	// there is zero or more than one entry for this key
+	ReadTimestamp(key string) (*time.Time, error)
+
+	// RemoveAll removes all key/value pair matching the key prefix
+	RemoveAll(keyPrefix string) error
+}
+
+func parseTimestamp(s string) (*time.Time, error) {
+	timestamp, err := strconv.Atoi(s)
+	if err != nil {
+		return nil, err
+	}
+
+	t := time.Unix(int64(timestamp), 0)
+	return &t, nil
+}

repository/config_git.go 🔗

@@ -0,0 +1,225 @@
+package repository
+
+import (
+	"fmt"
+	"regexp"
+	"strconv"
+	"strings"
+	"time"
+
+	"github.com/blang/semver"
+	"github.com/pkg/errors"
+)
+
+var _ Config = &gitConfig{}
+
+type gitConfig struct {
+	repo         *GitRepo
+	localityFlag string
+}
+
+func newGitConfig(repo *GitRepo, global bool) *gitConfig {
+	localityFlag := "--local"
+	if global {
+		localityFlag = "--global"
+	}
+	return &gitConfig{
+		repo:         repo,
+		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)
+	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.repo.runGitCommand("config", gc.localityFlag, "--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.Fields(line)
+		if len(parts) != 2 {
+			return nil, fmt.Errorf("bad git config: %s", line)
+		}
+
+		result[parts[0]] = parts[1]
+	}
+
+	return result, nil
+}
+
+func (gc *gitConfig) ReadString(key string) (string, error) {
+	stdout, err := gc.repo.runGitCommand("config", gc.localityFlag, "--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 nil, err
+	}
+	return parseTimestamp(value)
+}
+
+func (gc *gitConfig) rmSection(keyPrefix string) error {
+	_, err := gc.repo.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)
+	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.repo.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
+}

repository/config_mem.go 🔗

@@ -0,0 +1,84 @@
+package repository
+
+import (
+	"strconv"
+	"strings"
+	"time"
+)
+
+var _ Config = &memConfig{}
+
+type memConfig struct {
+	config map[string]string
+}
+
+func newMemConfig(config map[string]string) *memConfig {
+	return &memConfig{config: config}
+}
+
+func (mc *memConfig) StoreString(key, value string) error {
+	mc.config[key] = value
+	return nil
+}
+
+func (mc *memConfig) StoreBool(key string, value bool) error {
+	return mc.StoreString(key, strconv.FormatBool(value))
+}
+
+func (mc *memConfig) StoreTimestamp(key string, value time.Time) error {
+	return mc.StoreString(key, strconv.Itoa(int(value.Unix())))
+}
+
+func (mc *memConfig) ReadAll(keyPrefix string) (map[string]string, error) {
+	result := make(map[string]string)
+	for key, val := range mc.config {
+		if strings.HasPrefix(key, keyPrefix) {
+			result[key] = val
+		}
+	}
+	return result, nil
+}
+
+func (mc *memConfig) ReadString(key string) (string, error) {
+	// unlike git, the mock can only store one value for the same key
+	val, ok := mc.config[key]
+	if !ok {
+		return "", ErrNoConfigEntry
+	}
+
+	return val, nil
+}
+
+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
+	}
+
+	return strconv.ParseBool(val)
+}
+
+func (mc *memConfig) ReadTimestamp(key string) (*time.Time, error) {
+	value, err := mc.ReadString(key)
+	if err != nil {
+		return nil, err
+	}
+	timestamp, err := strconv.Atoi(value)
+	if err != nil {
+		return nil, err
+	}
+
+	t := time.Unix(int64(timestamp), 0)
+	return &t, nil
+}
+
+// RmConfigs remove all key/value pair matching the key prefix
+func (mc *memConfig) RemoveAll(keyPrefix string) error {
+	for key := range mc.config {
+		if strings.HasPrefix(key, keyPrefix) {
+			delete(mc.config, key)
+		}
+	}
+	return nil
+}

repository/git.go 🔗

@@ -7,11 +7,8 @@ import (
 	"io"
 	"os/exec"
 	"path"
-	"regexp"
-	"strconv"
 	"strings"
 
-	"github.com/blang/semver"
 	"github.com/pkg/errors"
 
 	"github.com/MichaelMure/git-bug/util/git"
@@ -33,16 +30,26 @@ type GitRepo struct {
 	editClock   *lamport.Persisted
 }
 
+// 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 {
-	repopath:=repo.Path
-	if repopath==".git" {
+	repopath := repo.Path
+	if repopath == ".git" {
 		// seeduvax> trangely the git command sometimes fail for very unknown
 		// reason wihtout this replacement.
 		// observed with rev-list command when git-bug is called from git
 		// hook script, even the same command with same args runs perfectly
-		// when called directly from the same hook script. 
-		repopath=""
+		// when called directly from the same hook script.
+		repopath = ""
 	}
 	// fmt.Printf("[%s] Running git %s\n", repopath, strings.Join(args, " "))
 
@@ -125,7 +132,7 @@ func NewGitRepo(path string, witnesser Witnesser) (*GitRepo, error) {
 
 // InitGitRepo create a new empty git repo at the given path
 func InitGitRepo(path string) (*GitRepo, error) {
-	repo := &GitRepo{Path: path+"/.git"}
+	repo := &GitRepo{Path: path + "/.git"}
 	err := repo.createClocks()
 	if err != nil {
 		return nil, err
@@ -197,174 +204,6 @@ func (repo *GitRepo) GetRemotes() (map[string]string, error) {
 	return remotes, nil
 }
 
-// StoreConfig store a single key/value pair in the config of the repo
-func (repo *GitRepo) StoreConfig(key string, value string) error {
-	_, err := repo.runGitCommand("config", "--replace-all", key, value)
-
-	return err
-}
-
-// ReadConfigs read all key/value pair matching the key prefix
-func (repo *GitRepo) ReadConfigs(keyPrefix string) (map[string]string, error) {
-	stdout, err := repo.runGitCommand("config", "--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.Fields(line)
-		if len(parts) != 2 {
-			return nil, fmt.Errorf("bad git config: %s", line)
-		}
-
-		result[parts[0]] = parts[1]
-	}
-
-	return result, nil
-}
-
-func (repo *GitRepo) ReadConfigBool(key string) (bool, error) {
-	val, err := repo.ReadConfigString(key)
-	if err != nil {
-		return false, err
-	}
-
-	return strconv.ParseBool(val)
-}
-
-func (repo *GitRepo) ReadConfigString(key string) (string, error) {
-	stdout, err := repo.runGitCommand("config", "--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 (repo *GitRepo) rmSection(keyPrefix string) error {
-	_, err := repo.runGitCommand("config", "--remove-section", keyPrefix)
-	return err
-}
-
-func (repo *GitRepo) unsetAll(keyPrefix string) error {
-	_, err := repo.runGitCommand("config", "--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 (repo *GitRepo) rmConfigsGitVersionLT218(keyPrefix string) error {
-	// try to remove key/value pair by key
-	err := repo.unsetAll(keyPrefix)
-	if err != nil {
-		return repo.rmSection(keyPrefix)
-	}
-
-	m, err := repo.ReadConfigs(sectionFromKey(keyPrefix))
-	if err != nil {
-		return err
-	}
-
-	// if section doesn't have any left key/value remove the section
-	if len(m) == 0 {
-		return repo.rmSection(sectionFromKey(keyPrefix))
-	}
-
-	return nil
-}
-
-// RmConfigs remove all key/value pair matching the key prefix
-func (repo *GitRepo) RmConfigs(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 := repo.gitVersionLT218()
-	if err != nil {
-		return errors.Wrap(err, "getting git version")
-	}
-
-	if lt218 {
-		return repo.rmConfigsGitVersionLT218(keyPrefix)
-	}
-
-	err = repo.unsetAll(keyPrefix)
-	if err != nil {
-		return repo.rmSection(keyPrefix)
-	}
-
-	return nil
-}
-
-func (repo *GitRepo) gitVersionLT218() (bool, error) {
-	versionOut, err := repo.runGitCommand("version")
-	if err != nil {
-		return false, err
-	}
-
-	// 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 false, fmt.Errorf("unreadable git version %s", versionOut)
-	}
-
-	version, err := semver.Make(extracted)
-	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
-}
-
 // FetchRefs fetch git refs from a remote
 func (repo *GitRepo) FetchRefs(remote, refSpec string) (string, error) {
 	stdout, err := repo.runGitCommand("fetch", remote, refSpec)

repository/git_test.go 🔗

@@ -11,56 +11,57 @@ func TestConfig(t *testing.T) {
 	repo := CreateTestRepo(false)
 	defer CleanupTestRepos(t, repo)
 
-	err := repo.StoreConfig("section.key", "value")
+	err := repo.LocalConfig().StoreString("section.key", "value")
 	assert.NoError(t, err)
 
-	val, err := repo.ReadConfigString("section.key")
+	val, err := repo.LocalConfig().ReadString("section.key")
+	assert.NoError(t, err)
 	assert.Equal(t, "value", val)
 
-	err = repo.StoreConfig("section.true", "true")
+	err = repo.LocalConfig().StoreString("section.true", "true")
 	assert.NoError(t, err)
 
-	val2, err := repo.ReadConfigBool("section.true")
+	val2, err := repo.LocalConfig().ReadBool("section.true")
+	assert.NoError(t, err)
 	assert.Equal(t, true, val2)
 
-	configs, err := repo.ReadConfigs("section")
+	configs, err := repo.LocalConfig().ReadAll("section")
 	assert.NoError(t, err)
 	assert.Equal(t, configs, map[string]string{
 		"section.key":  "value",
 		"section.true": "true",
 	})
 
-	err = repo.RmConfigs("section.true")
+	err = repo.LocalConfig().RemoveAll("section.true")
 	assert.NoError(t, err)
 
-	configs, err = repo.ReadConfigs("section")
+	configs, err = repo.LocalConfig().ReadAll("section")
 	assert.NoError(t, err)
 	assert.Equal(t, configs, map[string]string{
 		"section.key": "value",
 	})
 
-	_, err = repo.ReadConfigBool("section.true")
+	_, err = repo.LocalConfig().ReadBool("section.true")
 	assert.Equal(t, ErrNoConfigEntry, err)
 
-	err = repo.RmConfigs("section.nonexistingkey")
+	err = repo.LocalConfig().RemoveAll("section.nonexistingkey")
 	assert.Error(t, err)
 
-	err = repo.RmConfigs("section.key")
+	err = repo.LocalConfig().RemoveAll("section.key")
 	assert.NoError(t, err)
 
-	_, err = repo.ReadConfigString("section.key")
+	_, err = repo.LocalConfig().ReadString("section.key")
 	assert.Equal(t, ErrNoConfigEntry, err)
 
-	err = repo.RmConfigs("nonexistingsection")
+	err = repo.LocalConfig().RemoveAll("nonexistingsection")
 	assert.Error(t, err)
 
-	err = repo.RmConfigs("section")
+	err = repo.LocalConfig().RemoveAll("section")
 	assert.Error(t, err)
 
-	_, err = repo.ReadConfigString("section.key")
+	_, err = repo.LocalConfig().ReadString("section.key")
 	assert.Error(t, err)
 
-	err = repo.RmConfigs("section.key")
+	err = repo.LocalConfig().RemoveAll("section.key")
 	assert.Error(t, err)
-
 }

repository/git_testing.go 🔗

@@ -31,10 +31,11 @@ func CreateTestRepo(bare bool) *GitRepo {
 		log.Fatal(err)
 	}
 
-	if err := repo.StoreConfig("user.name", "testuser"); err != nil {
+	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 := repo.StoreConfig("user.email", "testuser@example.com"); err != nil {
+	if err := config.StoreString("user.email", "testuser@example.com"); err != nil {
 		log.Fatal("failed to set user.email for test repository: ", err)
 	}
 

repository/mock_repo.go 🔗

@@ -3,8 +3,6 @@ package repository
 import (
 	"crypto/sha1"
 	"fmt"
-	"strconv"
-	"strings"
 
 	"github.com/MichaelMure/git-bug/util/git"
 	"github.com/MichaelMure/git-bug/util/lamport"
@@ -14,13 +12,14 @@ var _ ClockedRepo = &mockRepoForTest{}
 
 // mockRepoForTest defines an instance of Repo that can be used for testing.
 type mockRepoForTest struct {
-	config      map[string]string
-	blobs       map[git.Hash][]byte
-	trees       map[git.Hash]string
-	commits     map[git.Hash]commit
-	refs        map[string]git.Hash
-	createClock lamport.Clock
-	editClock   lamport.Clock
+	config       map[string]string
+	globalConfig map[string]string
+	blobs        map[git.Hash][]byte
+	trees        map[git.Hash]string
+	commits      map[git.Hash]commit
+	refs         map[string]git.Hash
+	createClock  lamport.Clock
+	editClock    lamport.Clock
 }
 
 type commit struct {
@@ -40,6 +39,16 @@ func NewMockRepoForTest() *mockRepoForTest {
 	}
 }
 
+// LocalConfig give access to the repository scoped configuration
+func (r *mockRepoForTest) LocalConfig() Config {
+	return newMemConfig(r.config)
+}
+
+// GlobalConfig give access to the git global configuration
+func (r *mockRepoForTest) GlobalConfig() Config {
+	return newMemConfig(r.globalConfig)
+}
+
 // GetPath returns the path to the repo.
 func (r *mockRepoForTest) GetPath() string {
 	return "~/mockRepo/"
@@ -66,53 +75,6 @@ func (r *mockRepoForTest) GetRemotes() (map[string]string, error) {
 	}, nil
 }
 
-func (r *mockRepoForTest) StoreConfig(key string, value string) error {
-	r.config[key] = value
-	return nil
-}
-
-func (r *mockRepoForTest) ReadConfigs(keyPrefix string) (map[string]string, error) {
-	result := make(map[string]string)
-
-	for key, val := range r.config {
-		if strings.HasPrefix(key, keyPrefix) {
-			result[key] = val
-		}
-	}
-
-	return result, nil
-}
-
-func (r *mockRepoForTest) ReadConfigBool(key string) (bool, error) {
-	// unlike git, the mock can only store one value for the same key
-	val, ok := r.config[key]
-	if !ok {
-		return false, ErrNoConfigEntry
-	}
-
-	return strconv.ParseBool(val)
-}
-
-func (r *mockRepoForTest) ReadConfigString(key string) (string, error) {
-	// unlike git, the mock can only store one value for the same key
-	val, ok := r.config[key]
-	if !ok {
-		return "", ErrNoConfigEntry
-	}
-
-	return val, nil
-}
-
-// RmConfigs remove all key/value pair matching the key prefix
-func (r *mockRepoForTest) RmConfigs(keyPrefix string) error {
-	for key := range r.config {
-		if strings.HasPrefix(key, keyPrefix) {
-			delete(r.config, key)
-		}
-	}
-	return nil
-}
-
 // PushRefs push git refs to a remote
 func (r *mockRepoForTest) PushRefs(remote string, refSpec string) (string, error) {
 	return "", nil

repository/repo.go 🔗

@@ -30,24 +30,11 @@ type RepoCommon interface {
 	// GetRemotes returns the configured remotes repositories.
 	GetRemotes() (map[string]string, error)
 
-	// StoreConfig store a single key/value pair in the config of the repo
-	StoreConfig(key string, value string) error
+	// LocalConfig give access to the repository scoped configuration
+	LocalConfig() Config
 
-	// ReadConfigs read all key/value pair matching the key prefix
-	ReadConfigs(keyPrefix string) (map[string]string, error)
-
-	// ReadConfigBool read a single boolean value from the config
-	// Return ErrNoConfigEntry or ErrMultipleConfigEntry if there is zero or more than one entry
-	// for this key
-	ReadConfigBool(key string) (bool, error)
-
-	// ReadConfigBool read a single string value from the config
-	// Return ErrNoConfigEntry or ErrMultipleConfigEntry if there is zero or more than one entry
-	// for this key
-	ReadConfigString(key string) (string, error)
-
-	// RmConfigs remove all key/value pair matching the key prefix
-	RmConfigs(keyPrefix string) error
+	// GlobalConfig give access to the git global configuration
+	GlobalConfig() Config
 }
 
 // Repo represents a source code repository.