git_config.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// StoreString 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// ReadAll 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, "--includes", "--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.SplitN(line, " ", 2)
 70		result[parts[0]] = parts[1]
 71	}
 72
 73	return result, nil
 74}
 75
 76func (gc *gitConfig) ReadString(key string) (string, error) {
 77	stdout, err := gc.repo.runGitCommand("config", gc.localityFlag, "--includes", "--get-all", key)
 78
 79	//   / \
 80	//  / ! \
 81	// -------
 82	//
 83	// There can be a legitimate error here, but I see no portable way to
 84	// distinguish them from the git error that say "no matching value exist"
 85	if err != nil {
 86		return "", ErrNoConfigEntry
 87	}
 88
 89	lines := strings.Split(stdout, "\n")
 90
 91	if len(lines) == 0 {
 92		return "", ErrNoConfigEntry
 93	}
 94	if len(lines) > 1 {
 95		return "", ErrMultipleConfigEntry
 96	}
 97
 98	return lines[0], nil
 99}
100
101func (gc *gitConfig) ReadBool(key string) (bool, error) {
102	val, err := gc.ReadString(key)
103	if err != nil {
104		return false, err
105	}
106
107	return strconv.ParseBool(val)
108}
109
110func (gc *gitConfig) ReadTimestamp(key string) (time.Time, error) {
111	value, err := gc.ReadString(key)
112	if err != nil {
113		return time.Time{}, err
114	}
115	return ParseTimestamp(value)
116}
117
118func (gc *gitConfig) rmSection(keyPrefix string) error {
119	_, err := gc.repo.runGitCommand("config", gc.localityFlag, "--remove-section", keyPrefix)
120	return err
121}
122
123func (gc *gitConfig) unsetAll(keyPrefix string) error {
124	_, err := gc.repo.runGitCommand("config", gc.localityFlag, "--unset-all", keyPrefix)
125	return err
126}
127
128// return keyPrefix section
129// example: sectionFromKey(a.b.c.d) return a.b.c
130func sectionFromKey(keyPrefix string) string {
131	s := strings.Split(keyPrefix, ".")
132	if len(s) == 1 {
133		return keyPrefix
134	}
135
136	return strings.Join(s[:len(s)-1], ".")
137}
138
139// rmConfigs with git version lesser than 2.18
140func (gc *gitConfig) rmConfigsGitVersionLT218(keyPrefix string) error {
141	// try to remove key/value pair by key
142	err := gc.unsetAll(keyPrefix)
143	if err != nil {
144		return gc.rmSection(keyPrefix)
145	}
146
147	m, err := gc.ReadAll(sectionFromKey(keyPrefix))
148	if err != nil {
149		return err
150	}
151
152	// if section doesn't have any left key/value remove the section
153	if len(m) == 0 {
154		return gc.rmSection(sectionFromKey(keyPrefix))
155	}
156
157	return nil
158}
159
160// RmConfigs remove all key/value pair matching the key prefix
161func (gc *gitConfig) RemoveAll(keyPrefix string) error {
162	// starting from git 2.18.0 sections are automatically deleted when the last existing
163	// key/value is removed. Before 2.18.0 we should remove the section
164	// see https://github.com/git/git/blob/master/Documentation/RelNotes/2.18.0.txt#L379
165	lt218, err := gc.gitVersionLT218()
166	if err != nil {
167		return errors.Wrap(err, "getting git version")
168	}
169
170	if lt218 {
171		return gc.rmConfigsGitVersionLT218(keyPrefix)
172	}
173
174	err = gc.unsetAll(keyPrefix)
175	if err != nil {
176		return gc.rmSection(keyPrefix)
177	}
178
179	return nil
180}
181
182func (gc *gitConfig) gitVersion() (*semver.Version, error) {
183	versionOut, err := gc.repo.runGitCommand("version")
184	if err != nil {
185		return nil, err
186	}
187	return parseGitVersion(versionOut)
188}
189
190func parseGitVersion(versionOut string) (*semver.Version, error) {
191	// extract the version and truncate potential bad parts
192	// ex: 2.23.0.rc1 instead of 2.23.0-rc1
193	r := regexp.MustCompile(`(\d+\.){1,2}\d+`)
194
195	extracted := r.FindString(versionOut)
196	if extracted == "" {
197		return nil, fmt.Errorf("unreadable git version %s", versionOut)
198	}
199
200	version, err := semver.Make(extracted)
201	if err != nil {
202		return nil, err
203	}
204
205	return &version, nil
206}
207
208func (gc *gitConfig) gitVersionLT218() (bool, error) {
209	version, err := gc.gitVersion()
210	if err != nil {
211		return false, err
212	}
213
214	version218string := "2.18.0"
215	gitVersion218, err := semver.Make(version218string)
216	if err != nil {
217		return false, err
218	}
219
220	return version.LT(gitVersion218), nil
221}