repository: config interface and implementation rework

This commit is contained in:
amine 2019-10-31 15:46:09 +01:00
parent 11b4a1beb7
commit ab935674a2
8 changed files with 334 additions and 190 deletions

23
repository/config.go Normal file
View File

@ -0,0 +1,23 @@
package repository
// Config represent the common function interacting with the repository config storage
type Config interface {
// Store writes a single key/value pair in the config of the repo
Store(key string, value string) 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)
// RemoveAll removes all key/value pair matching the key prefix
RemoveAll(keyPrefix string) error
}

185
repository/config_git.go Normal file
View File

@ -0,0 +1,185 @@
package repository
import (
"fmt"
"strconv"
"strings"
"github.com/blang/semver"
"github.com/pkg/errors"
)
type gitConfig struct {
version *semver.Version
execFn func(args ...string) (string, error)
}
func NewGitConfig(repo *GitRepo, global bool) *gitConfig {
version, _ := repo.GitVersion()
if global {
return &gitConfig{
execFn: func(args ...string) (string, error) {
args = append([]string{"config", "--global"}, args...)
return repo.runGitCommand(args...)
},
version: version,
}
}
return &gitConfig{
execFn: func(args ...string) (string, error) {
args = append([]string{"config"}, args...)
return repo.runGitCommand(args...)
},
version: version,
}
}
// StoreConfig store a single key/value pair in the config of the repo
func (gc *gitConfig) Store(key string, value string) error {
_, err := gc.execFn("--replace-all", key, value)
return err
}
// ReadConfigs read all key/value pair matching the key prefix
func (gc *gitConfig) ReadAll(keyPrefix string) (map[string]string, error) {
stdout, err := gc.execFn("--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.execFn("--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) rmSection(keyPrefix string) error {
_, err := gc.execFn("--remove-section", keyPrefix)
return err
}
func (gc *gitConfig) unsetAll(keyPrefix string) error {
_, err := gc.execFn("--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) gitVersionLT218() (bool, error) {
gitVersion218, err := semver.Make("2.18.0")
if err != nil {
return false, err
}
return gc.version.LT(gitVersion218), nil
}

View File

@ -0,0 +1,59 @@
package repository
import (
"strconv"
"strings"
)
type runtimeConfig struct {
config map[string]string
}
func newRuntimeConfig(config map[string]string) *runtimeConfig {
return &runtimeConfig{config: config}
}
func (rtc *runtimeConfig) Store(key, value string) error {
rtc.config[key] = value
return nil
}
func (rtc *runtimeConfig) ReadAll(keyPrefix string) (map[string]string, error) {
result := make(map[string]string)
for key, val := range rtc.config {
if strings.HasPrefix(key, keyPrefix) {
result[key] = val
}
}
return result, nil
}
func (rtc *runtimeConfig) ReadString(key string) (string, error) {
// unlike git, the mock can only store one value for the same key
val, ok := rtc.config[key]
if !ok {
return "", ErrNoConfigEntry
}
return val, nil
}
func (rtc *runtimeConfig) ReadBool(key string) (bool, error) {
// unlike git, the mock can only store one value for the same key
val, ok := rtc.config[key]
if !ok {
return false, ErrNoConfigEntry
}
return strconv.ParseBool(val)
}
// RmConfigs remove all key/value pair matching the key prefix
func (rtc *runtimeConfig) RemoveAll(keyPrefix string) error {
for key := range rtc.config {
if strings.HasPrefix(key, keyPrefix) {
delete(rtc.config, key)
}
}
return nil
}

View File

@ -8,7 +8,6 @@ import (
"os/exec"
"path"
"regexp"
"strconv"
"strings"
"github.com/blang/semver"
@ -33,16 +32,26 @@ type GitRepo struct {
editClock *lamport.Persisted
}
// LocalConfig .
func (repo *GitRepo) LocalConfig() Config {
return NewGitConfig(repo, false)
}
// GlobalConfig .
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 +134,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,149 +206,10 @@ 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) {
func (repo *GitRepo) GitVersion() (*semver.Version, error) {
versionOut, err := repo.runGitCommand("version")
if err != nil {
return false, err
return nil, err
}
// extract the version and truncate potential bad parts
@ -348,10 +218,19 @@ func (repo *GitRepo) gitVersionLT218() (bool, error) {
extracted := r.FindString(versionOut)
if extracted == "" {
return false, fmt.Errorf("unreadable git version %s", versionOut)
return nil, fmt.Errorf("unreadable git version %s", versionOut)
}
version, err := semver.Make(extracted)
if err != nil {
return nil, err
}
return &version, nil
}
func (repo *GitRepo) gitVersionLT218() (bool, error) {
version, err := repo.GitVersion()
if err != nil {
return false, err
}

View File

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

View File

@ -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.Store("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.Store("user.email", "testuser@example.com"); err != nil {
log.Fatal("failed to set user.email for test repository: ", err)
}

View File

@ -14,13 +14,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 +41,14 @@ func NewMockRepoForTest() *mockRepoForTest {
}
}
func (r *mockRepoForTest) LocalConfig() Config {
return newRuntimeConfig(r.config)
}
func (r *mockRepoForTest) GlobalConfig() Config {
return newRuntimeConfig(r.globalConfig)
}
// GetPath returns the path to the repo.
func (r *mockRepoForTest) GetPath() string {
return "~/mockRepo/"

View File

@ -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 .
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 .
GlobalConfig() Config
}
// Repo represents a source code repository.