2019-10-31 17:46:09 +03:00
|
|
|
package repository
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
2019-11-01 20:39:45 +03:00
|
|
|
"regexp"
|
2019-10-31 17:46:09 +03:00
|
|
|
"strconv"
|
|
|
|
"strings"
|
2019-10-31 21:05:50 +03:00
|
|
|
"time"
|
2019-10-31 17:46:09 +03:00
|
|
|
|
|
|
|
"github.com/blang/semver"
|
|
|
|
"github.com/pkg/errors"
|
|
|
|
)
|
|
|
|
|
2019-10-31 21:05:50 +03:00
|
|
|
var _ Config = &gitConfig{}
|
|
|
|
|
2019-10-31 17:46:09 +03:00
|
|
|
type gitConfig struct {
|
2019-11-01 23:37:16 +03:00
|
|
|
repo *GitRepo
|
|
|
|
localityFlag string
|
2019-10-31 17:46:09 +03:00
|
|
|
}
|
|
|
|
|
2019-10-31 21:05:50 +03:00
|
|
|
func newGitConfig(repo *GitRepo, global bool) *gitConfig {
|
2019-11-01 23:37:16 +03:00
|
|
|
localityFlag := "--local"
|
2019-10-31 17:46:09 +03:00
|
|
|
if global {
|
2019-11-01 23:37:16 +03:00
|
|
|
localityFlag = "--global"
|
2019-10-31 17:46:09 +03:00
|
|
|
}
|
|
|
|
return &gitConfig{
|
2019-11-01 23:37:16 +03:00
|
|
|
repo: repo,
|
|
|
|
localityFlag: localityFlag,
|
2019-10-31 17:46:09 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// StoreConfig store a single key/value pair in the config of the repo
|
2019-11-01 20:39:45 +03:00
|
|
|
func (gc *gitConfig) StoreString(key string, value string) error {
|
2019-11-01 23:37:16 +03:00
|
|
|
_, err := gc.repo.runGitCommand("config", "gc.localityFlag", "--replace-all", key, value)
|
2019-10-31 17:46:09 +03:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2019-11-01 20:39:45 +03:00
|
|
|
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())))
|
|
|
|
}
|
|
|
|
|
2019-10-31 17:46:09 +03:00
|
|
|
// ReadConfigs read all key/value pair matching the key prefix
|
|
|
|
func (gc *gitConfig) ReadAll(keyPrefix string) (map[string]string, error) {
|
2019-11-01 23:37:16 +03:00
|
|
|
stdout, err := gc.repo.runGitCommand("config", "gc.localityFlag", "--get-regexp", keyPrefix)
|
2019-10-31 17:46:09 +03:00
|
|
|
|
|
|
|
// / \
|
|
|
|
// / ! \
|
|
|
|
// -------
|
|
|
|
//
|
|
|
|
// 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) {
|
2019-11-01 23:37:16 +03:00
|
|
|
stdout, err := gc.repo.runGitCommand("config", "gc.localityFlag", "--get-all", key)
|
2019-10-31 17:46:09 +03:00
|
|
|
|
|
|
|
// / \
|
|
|
|
// / ! \
|
|
|
|
// -------
|
|
|
|
//
|
|
|
|
// 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)
|
|
|
|
}
|
|
|
|
|
2019-10-31 21:05:50 +03:00
|
|
|
func (gc *gitConfig) ReadTimestamp(key string) (*time.Time, error) {
|
|
|
|
value, err := gc.ReadString(key)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2019-11-01 20:39:45 +03:00
|
|
|
return parseTimestamp(value)
|
2019-10-31 21:05:50 +03:00
|
|
|
}
|
|
|
|
|
2019-10-31 17:46:09 +03:00
|
|
|
func (gc *gitConfig) rmSection(keyPrefix string) error {
|
2019-11-01 23:37:16 +03:00
|
|
|
_, err := gc.repo.runGitCommand("config", "gc.localityFlag", "--remove-section", keyPrefix)
|
2019-10-31 17:46:09 +03:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
func (gc *gitConfig) unsetAll(keyPrefix string) error {
|
2019-11-01 23:37:16 +03:00
|
|
|
_, err := gc.repo.runGitCommand("config", "gc.localityFlag", "--unset-all", keyPrefix)
|
2019-10-31 17:46:09 +03:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2019-11-01 20:39:45 +03:00
|
|
|
func (gc *gitConfig) gitVersion() (*semver.Version, error) {
|
2019-11-01 23:37:16 +03:00
|
|
|
versionOut, err := gc.repo.runGitCommand("version")
|
2019-11-01 20:39:45 +03:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2019-10-31 17:46:09 +03:00
|
|
|
func (gc *gitConfig) gitVersionLT218() (bool, error) {
|
2019-11-01 20:39:45 +03:00
|
|
|
version, err := gc.gitVersion()
|
|
|
|
if err != nil {
|
|
|
|
return false, err
|
|
|
|
}
|
|
|
|
|
|
|
|
version218string := "2.18.0"
|
|
|
|
gitVersion218, err := semver.Make(version218string)
|
2019-10-31 17:46:09 +03:00
|
|
|
if err != nil {
|
|
|
|
return false, err
|
|
|
|
}
|
|
|
|
|
2019-11-01 20:39:45 +03:00
|
|
|
return version.LT(gitVersion218), nil
|
2019-10-31 17:46:09 +03:00
|
|
|
}
|