package repository
import (
"fmt"
"regexp"
"strconv"
"strings"
"time"
"github.com/blang/semver"
"github.com/pkg/errors"
)
var _ Config = &gitConfig{}
type gitConfig struct {
execFn func(args ...string) (string, error)
}
func newGitConfig(repo *GitRepo, global bool) *gitConfig {
configCmdFlag := "--local"
if global {
configCmdFlag = "--global"
}
return &gitConfig{
execFn: func(args ...string) (string, error) {
if len(args) > 0 && args[0] == "config" {
args = append([]string{args[0], configCmdFlag}, args[1:]...)
}
return repo.runGitCommand(args...)
},
}
}
// StoreConfig store a single key/value pair in the config of the repo
func (gc *gitConfig) StoreString(key string, value string) error {
_, err := gc.execFn("config", "--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())))
}
// ReadConfigs read all key/value pair matching the key prefix
func (gc *gitConfig) ReadAll(keyPrefix string) (map[string]string, error) {
stdout, err := gc.execFn("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 (gc *gitConfig) ReadString(key string) (string, error) {
stdout, err := gc.execFn("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 (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.execFn("config", "--remove-section", keyPrefix)
return err
}
func (gc *gitConfig) unsetAll(keyPrefix string) error {
_, err := gc.execFn("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 (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.execFn("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
}