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