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}