1package repository
2
3import (
4 "fmt"
5 "strconv"
6 "strings"
7 "time"
8
9 "github.com/blang/semver"
10 "github.com/pkg/errors"
11)
12
13var _ Config = &gitConfig{}
14
15type gitConfig struct {
16 version *semver.Version
17 execFn func(args ...string) (string, error)
18}
19
20func newGitConfig(repo *GitRepo, global bool) *gitConfig {
21 version, _ := repo.GitVersion()
22
23 if global {
24 return &gitConfig{
25 execFn: func(args ...string) (string, error) {
26 args = append([]string{"config", "--global"}, args...)
27 return repo.runGitCommand(args...)
28 },
29 version: version,
30 }
31 }
32
33 return &gitConfig{
34 execFn: func(args ...string) (string, error) {
35 args = append([]string{"config", "--local"}, args...)
36 return repo.runGitCommand(args...)
37 },
38 version: version,
39 }
40}
41
42// StoreConfig store a single key/value pair in the config of the repo
43func (gc *gitConfig) Store(key string, value string) error {
44 _, err := gc.execFn("--replace-all", key, value)
45
46 return err
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("--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("--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 timestamp, err := strconv.Atoi(value)
123 if err != nil {
124 return nil, err
125 }
126
127 t := time.Unix(int64(timestamp), 0)
128 return &t, nil
129}
130
131func (gc *gitConfig) rmSection(keyPrefix string) error {
132 _, err := gc.execFn("--remove-section", keyPrefix)
133 return err
134}
135
136func (gc *gitConfig) unsetAll(keyPrefix string) error {
137 _, err := gc.execFn("--unset-all", keyPrefix)
138 return err
139}
140
141// return keyPrefix section
142// example: sectionFromKey(a.b.c.d) return a.b.c
143func sectionFromKey(keyPrefix string) string {
144 s := strings.Split(keyPrefix, ".")
145 if len(s) == 1 {
146 return keyPrefix
147 }
148
149 return strings.Join(s[:len(s)-1], ".")
150}
151
152// rmConfigs with git version lesser than 2.18
153func (gc *gitConfig) rmConfigsGitVersionLT218(keyPrefix string) error {
154 // try to remove key/value pair by key
155 err := gc.unsetAll(keyPrefix)
156 if err != nil {
157 return gc.rmSection(keyPrefix)
158 }
159
160 m, err := gc.ReadAll(sectionFromKey(keyPrefix))
161 if err != nil {
162 return err
163 }
164
165 // if section doesn't have any left key/value remove the section
166 if len(m) == 0 {
167 return gc.rmSection(sectionFromKey(keyPrefix))
168 }
169
170 return nil
171}
172
173// RmConfigs remove all key/value pair matching the key prefix
174func (gc *gitConfig) RemoveAll(keyPrefix string) error {
175 // starting from git 2.18.0 sections are automatically deleted when the last existing
176 // key/value is removed. Before 2.18.0 we should remove the section
177 // see https://github.com/git/git/blob/master/Documentation/RelNotes/2.18.0.txt#L379
178 lt218, err := gc.gitVersionLT218()
179 if err != nil {
180 return errors.Wrap(err, "getting git version")
181 }
182
183 if lt218 {
184 return gc.rmConfigsGitVersionLT218(keyPrefix)
185 }
186
187 err = gc.unsetAll(keyPrefix)
188 if err != nil {
189 return gc.rmSection(keyPrefix)
190 }
191
192 return nil
193}
194
195func (gc *gitConfig) gitVersionLT218() (bool, error) {
196 gitVersion218, err := semver.Make("2.18.0")
197 if err != nil {
198 return false, err
199 }
200
201 return gc.version.LT(gitVersion218), nil
202}