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 cli gitCli
18 localityFlag string
19}
20
21func newGitConfig(cli gitCli, global bool) *gitConfig {
22 localityFlag := "--local"
23 if global {
24 localityFlag = "--global"
25 }
26 return &gitConfig{
27 cli: cli,
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.cli.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.cli.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.cli.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.cli.runGitCommand("config", gc.localityFlag, "--remove-section", keyPrefix)
120 return err
121}
122
123func (gc *gitConfig) unsetAll(keyPrefix string) error {
124 _, err := gc.cli.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.cli.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}