1
2
3
4 package config
5
6 import (
7 "errors"
8 "io"
9 "os"
10 "path/filepath"
11 "runtime"
12 "sync"
13
14 "github.com/cli/go-gh/v2/internal/yamlmap"
15 )
16
17 const (
18 appData = "AppData"
19 ghConfigDir = "GH_CONFIG_DIR"
20 localAppData = "LocalAppData"
21 xdgConfigHome = "XDG_CONFIG_HOME"
22 xdgDataHome = "XDG_DATA_HOME"
23 xdgStateHome = "XDG_STATE_HOME"
24 xdgCacheHome = "XDG_CACHE_HOME"
25 )
26
27 var (
28 cfg *Config
29 once sync.Once
30 loadErr error
31 )
32
33
34
35
36
37 type Config struct {
38 entries *yamlmap.Map
39 mu sync.RWMutex
40 }
41
42
43
44
45
46
47 func (c *Config) Get(keys []string) (string, error) {
48 c.mu.RLock()
49 defer c.mu.RUnlock()
50 m := c.entries
51 for _, key := range keys {
52 var err error
53 m, err = m.FindEntry(key)
54 if err != nil {
55 return "", &KeyNotFoundError{key}
56 }
57 }
58 return m.Value, nil
59 }
60
61
62
63
64
65 func (c *Config) Keys(keys []string) ([]string, error) {
66 c.mu.RLock()
67 defer c.mu.RUnlock()
68 m := c.entries
69 for _, key := range keys {
70 var err error
71 m, err = m.FindEntry(key)
72 if err != nil {
73 return nil, &KeyNotFoundError{key}
74 }
75 }
76 return m.Keys(), nil
77 }
78
79
80
81
82
83
84 func (c *Config) Remove(keys []string) error {
85 c.mu.Lock()
86 defer c.mu.Unlock()
87 m := c.entries
88 for i := 0; i < len(keys)-1; i++ {
89 var err error
90 key := keys[i]
91 m, err = m.FindEntry(key)
92 if err != nil {
93 return &KeyNotFoundError{key}
94 }
95 }
96 err := m.RemoveEntry(keys[len(keys)-1])
97 if err != nil {
98 return &KeyNotFoundError{keys[len(keys)-1]}
99 }
100 return nil
101 }
102
103
104
105
106
107
108
109
110
111
112 func (c *Config) Set(keys []string, value string) {
113 c.mu.Lock()
114 defer c.mu.Unlock()
115 m := c.entries
116 for i := 0; i < len(keys)-1; i++ {
117 key := keys[i]
118 entry, err := m.FindEntry(key)
119 if err != nil {
120 entry = yamlmap.MapValue()
121 m.AddEntry(key, entry)
122 }
123 m = entry
124 }
125 val := yamlmap.StringValue(value)
126 if value == "" {
127 val = yamlmap.NullValue()
128 }
129 m.SetEntry(keys[len(keys)-1], val)
130 }
131
132 func (c *Config) deepCopy() *Config {
133 return ReadFromString(c.entries.String())
134 }
135
136
137
138
139
140
141 var Read = func(fallback *Config) (*Config, error) {
142 once.Do(func() {
143 cfg, loadErr = load(generalConfigFile(), hostsConfigFile(), fallback)
144 })
145 return cfg, loadErr
146 }
147
148
149 func ReadFromString(str string) *Config {
150 m, _ := mapFromString(str)
151 if m == nil {
152 m = yamlmap.MapValue()
153 }
154 return &Config{entries: m}
155 }
156
157
158
159
160 func Write(c *Config) error {
161 c.mu.Lock()
162 defer c.mu.Unlock()
163 hosts, err := c.entries.FindEntry("hosts")
164 if err == nil && hosts.IsModified() {
165 err := writeFile(hostsConfigFile(), []byte(hosts.String()))
166 if err != nil {
167 return err
168 }
169 hosts.SetUnmodified()
170 }
171
172 if c.entries.IsModified() {
173
174
175 hostsMap, hostsErr := c.entries.FindEntry("hosts")
176 if hostsErr == nil {
177 _ = c.entries.RemoveEntry("hosts")
178 }
179 err := writeFile(generalConfigFile(), []byte(c.entries.String()))
180 if err != nil {
181 return err
182 }
183 c.entries.SetUnmodified()
184 if hostsErr == nil {
185 c.entries.AddEntry("hosts", hostsMap)
186 }
187 }
188
189 return nil
190 }
191
192 func load(generalFilePath, hostsFilePath string, fallback *Config) (*Config, error) {
193 generalMap, err := mapFromFile(generalFilePath)
194 if err != nil && !os.IsNotExist(err) {
195 if errors.Is(err, yamlmap.ErrInvalidYaml) ||
196 errors.Is(err, yamlmap.ErrInvalidFormat) {
197 return nil, &InvalidConfigFileError{Path: generalFilePath, Err: err}
198 }
199 return nil, err
200 }
201
202 if generalMap == nil {
203 generalMap = yamlmap.MapValue()
204 }
205
206 hostsMap, err := mapFromFile(hostsFilePath)
207 if err != nil && !os.IsNotExist(err) {
208 if errors.Is(err, yamlmap.ErrInvalidYaml) ||
209 errors.Is(err, yamlmap.ErrInvalidFormat) {
210 return nil, &InvalidConfigFileError{Path: hostsFilePath, Err: err}
211 }
212 return nil, err
213 }
214
215 if hostsMap != nil && !hostsMap.Empty() {
216 generalMap.AddEntry("hosts", hostsMap)
217 generalMap.SetUnmodified()
218 }
219
220 if generalMap.Empty() && fallback != nil {
221 return fallback.deepCopy(), nil
222 }
223
224 return &Config{entries: generalMap}, nil
225 }
226
227 func generalConfigFile() string {
228 return filepath.Join(ConfigDir(), "config.yml")
229 }
230
231 func hostsConfigFile() string {
232 return filepath.Join(ConfigDir(), "hosts.yml")
233 }
234
235 func mapFromFile(filename string) (*yamlmap.Map, error) {
236 data, err := readFile(filename)
237 if err != nil {
238 return nil, err
239 }
240 return yamlmap.Unmarshal(data)
241 }
242
243 func mapFromString(str string) (*yamlmap.Map, error) {
244 return yamlmap.Unmarshal([]byte(str))
245 }
246
247
248 func ConfigDir() string {
249 var path string
250 if a := os.Getenv(ghConfigDir); a != "" {
251 path = a
252 } else if b := os.Getenv(xdgConfigHome); b != "" {
253 path = filepath.Join(b, "gh")
254 } else if c := os.Getenv(appData); runtime.GOOS == "windows" && c != "" {
255 path = filepath.Join(c, "GitHub CLI")
256 } else {
257 d, _ := os.UserHomeDir()
258 path = filepath.Join(d, ".config", "gh")
259 }
260 return path
261 }
262
263
264 func StateDir() string {
265 var path string
266 if a := os.Getenv(xdgStateHome); a != "" {
267 path = filepath.Join(a, "gh")
268 } else if b := os.Getenv(localAppData); runtime.GOOS == "windows" && b != "" {
269 path = filepath.Join(b, "GitHub CLI")
270 } else {
271 c, _ := os.UserHomeDir()
272 path = filepath.Join(c, ".local", "state", "gh")
273 }
274 return path
275 }
276
277
278 func DataDir() string {
279 var path string
280 if a := os.Getenv(xdgDataHome); a != "" {
281 path = filepath.Join(a, "gh")
282 } else if b := os.Getenv(localAppData); runtime.GOOS == "windows" && b != "" {
283 path = filepath.Join(b, "GitHub CLI")
284 } else {
285 c, _ := os.UserHomeDir()
286 path = filepath.Join(c, ".local", "share", "gh")
287 }
288 return path
289 }
290
291
292 func CacheDir() string {
293 if a := os.Getenv(xdgCacheHome); a != "" {
294 return filepath.Join(a, "gh")
295 } else if b := os.Getenv(localAppData); runtime.GOOS == "windows" && b != "" {
296 return filepath.Join(b, "GitHub CLI")
297 } else if c, err := os.UserHomeDir(); err == nil {
298 return filepath.Join(c, ".cache", "gh")
299 } else {
300
301
302
303
304 return filepath.Join(os.TempDir(), "gh-cli-cache")
305 }
306 }
307
308 func readFile(filename string) ([]byte, error) {
309 f, err := os.Open(filename)
310 if err != nil {
311 return nil, err
312 }
313 defer f.Close()
314 data, err := io.ReadAll(f)
315 if err != nil {
316 return nil, err
317 }
318 return data, nil
319 }
320
321 func writeFile(filename string, data []byte) (writeErr error) {
322 if writeErr = os.MkdirAll(filepath.Dir(filename), 0771); writeErr != nil {
323 return
324 }
325 var file *os.File
326 if file, writeErr = os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600); writeErr != nil {
327 return
328 }
329 defer func() {
330 if err := file.Close(); writeErr == nil && err != nil {
331 writeErr = err
332 }
333 }()
334 _, writeErr = file.Write(data)
335 return
336 }
337
View as plain text