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}