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