config_git.go

  1package repository
  2
  3import (
  4	"fmt"
  5	"regexp"
  6	"strconv"
  7	"strings"
  8	"time"
  9
 10	"github.com/blang/semver"
 11	"github.com/pkg/errors"
 12)
 13
 14var _ Config = &gitConfig{}
 15
 16type gitConfig struct {
 17	repo         *GitRepo
 18	localityFlag string
 19}
 20
 21func newGitConfig(repo *GitRepo, global bool) *gitConfig {
 22	localityFlag := "--local"
 23	if global {
 24		localityFlag = "--global"
 25	}
 26	return &gitConfig{
 27		repo:         repo,
 28		localityFlag: localityFlag,
 29	}
 30}
 31
 32// StoreConfig store a single key/value pair in the config of the repo
 33func (gc *gitConfig) StoreString(key string, value string) error {
 34	_, err := gc.repo.runGitCommand("config", gc.localityFlag, "--replace-all", key, value)
 35	return err
 36}
 37
 38func (gc *gitConfig) StoreBool(key string, value bool) error {
 39	return gc.StoreString(key, strconv.FormatBool(value))
 40}
 41
 42func (gc *gitConfig) StoreTimestamp(key string, value time.Time) error {
 43	return gc.StoreString(key, strconv.Itoa(int(value.Unix())))
 44}
 45
 46// ReadConfigs read all key/value pair matching the key prefix
 47func (gc *gitConfig) ReadAll(keyPrefix string) (map[string]string, error) {
 48	stdout, err := gc.repo.runGitCommand("config", gc.localityFlag, "--get-regexp", keyPrefix)
 49
 50	//   / \
 51	//  / ! \
 52	// -------
 53	//
 54	// There can be a legitimate error here, but I see no portable way to
 55	// distinguish them from the git error that say "no matching value exist"
 56	if err != nil {
 57		return nil, nil
 58	}
 59
 60	lines := strings.Split(stdout, "\n")
 61
 62	result := make(map[string]string, len(lines))
 63
 64	for _, line := range lines {
 65		if strings.TrimSpace(line) == "" {
 66			continue
 67		}
 68
 69		parts := strings.Fields(line)
 70		if len(parts) != 2 {
 71			return nil, fmt.Errorf("bad git config: %s", line)
 72		}
 73
 74		result[parts[0]] = parts[1]
 75	}
 76
 77	return result, nil
 78}
 79
 80func (gc *gitConfig) ReadString(key string) (string, error) {
 81	stdout, err := gc.repo.runGitCommand("config", gc.localityFlag, "--get-all", key)
 82
 83	//   / \
 84	//  / ! \
 85	// -------
 86	//
 87	// There can be a legitimate error here, but I see no portable way to
 88	// distinguish them from the git error that say "no matching value exist"
 89	if err != nil {
 90		return "", ErrNoConfigEntry
 91	}
 92
 93	lines := strings.Split(stdout, "\n")
 94
 95	if len(lines) == 0 {
 96		return "", ErrNoConfigEntry
 97	}
 98	if len(lines) > 1 {
 99		return "", ErrMultipleConfigEntry
100	}
101
102	return lines[0], nil
103}
104
105func (gc *gitConfig) ReadBool(key string) (bool, error) {
106	val, err := gc.ReadString(key)
107	if err != nil {
108		return false, err
109	}
110
111	return strconv.ParseBool(val)
112}
113
114func (gc *gitConfig) ReadTimestamp(key string) (*time.Time, error) {
115	value, err := gc.ReadString(key)
116	if err != nil {
117		return nil, err
118	}
119	return parseTimestamp(value)
120}
121
122func (gc *gitConfig) rmSection(keyPrefix string) error {
123	_, err := gc.repo.runGitCommand("config", gc.localityFlag, "--remove-section", keyPrefix)
124	return err
125}
126
127func (gc *gitConfig) unsetAll(keyPrefix string) error {
128	_, err := gc.repo.runGitCommand("config", gc.localityFlag, "--unset-all", keyPrefix)
129	return err
130}
131
132// return keyPrefix section
133// example: sectionFromKey(a.b.c.d) return a.b.c
134func sectionFromKey(keyPrefix string) string {
135	s := strings.Split(keyPrefix, ".")
136	if len(s) == 1 {
137		return keyPrefix
138	}
139
140	return strings.Join(s[:len(s)-1], ".")
141}
142
143// rmConfigs with git version lesser than 2.18
144func (gc *gitConfig) rmConfigsGitVersionLT218(keyPrefix string) error {
145	// try to remove key/value pair by key
146	err := gc.unsetAll(keyPrefix)
147	if err != nil {
148		return gc.rmSection(keyPrefix)
149	}
150
151	m, err := gc.ReadAll(sectionFromKey(keyPrefix))
152	if err != nil {
153		return err
154	}
155
156	// if section doesn't have any left key/value remove the section
157	if len(m) == 0 {
158		return gc.rmSection(sectionFromKey(keyPrefix))
159	}
160
161	return nil
162}
163
164// RmConfigs remove all key/value pair matching the key prefix
165func (gc *gitConfig) RemoveAll(keyPrefix string) error {
166	// starting from git 2.18.0 sections are automatically deleted when the last existing
167	// key/value is removed. Before 2.18.0 we should remove the section
168	// see https://github.com/git/git/blob/master/Documentation/RelNotes/2.18.0.txt#L379
169	lt218, err := gc.gitVersionLT218()
170	if err != nil {
171		return errors.Wrap(err, "getting git version")
172	}
173
174	if lt218 {
175		return gc.rmConfigsGitVersionLT218(keyPrefix)
176	}
177
178	err = gc.unsetAll(keyPrefix)
179	if err != nil {
180		return gc.rmSection(keyPrefix)
181	}
182
183	return nil
184}
185
186func (gc *gitConfig) gitVersion() (*semver.Version, error) {
187	versionOut, err := gc.repo.runGitCommand("version")
188	if err != nil {
189		return nil, err
190	}
191	return parseGitVersion(versionOut)
192}
193
194func parseGitVersion(versionOut string) (*semver.Version, error) {
195	// extract the version and truncate potential bad parts
196	// ex: 2.23.0.rc1 instead of 2.23.0-rc1
197	r := regexp.MustCompile(`(\d+\.){1,2}\d+`)
198
199	extracted := r.FindString(versionOut)
200	if extracted == "" {
201		return nil, fmt.Errorf("unreadable git version %s", versionOut)
202	}
203
204	version, err := semver.Make(extracted)
205	if err != nil {
206		return nil, err
207	}
208
209	return &version, nil
210}
211
212func (gc *gitConfig) gitVersionLT218() (bool, error) {
213	version, err := gc.gitVersion()
214	if err != nil {
215		return false, err
216	}
217
218	version218string := "2.18.0"
219	gitVersion218, err := semver.Make(version218string)
220	if err != nil {
221		return false, err
222	}
223
224	return version.LT(gitVersion218), nil
225}