X Tutup
Skip to content

Commit 0d49bfb

Browse files
committed
Add support for XDG_CONFIG_HOME and AppData on Windows
1 parent 55b183f commit 0d49bfb

File tree

4 files changed

+209
-44
lines changed

4 files changed

+209
-44
lines changed

internal/config/config_file.go

Lines changed: 55 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,73 @@ import (
66
"io/ioutil"
77
"os"
88
"path/filepath"
9+
"runtime"
910
"syscall"
1011

1112
"github.com/mitchellh/go-homedir"
1213
"gopkg.in/yaml.v3"
1314
)
1415

1516
const (
16-
GH_CONFIG_DIR = "GH_CONFIG_DIR"
17+
GH_CONFIG_DIR = "GH_CONFIG_DIR"
18+
XDG_CONFIG_HOME = "XDG_CONFIG_HOME"
19+
APP_DATA = "AppData"
1720
)
1821

22+
// Config path precedence
23+
// 1. GH_CONFIG_DIR
24+
// 2. XDG_CONFIG_HOME
25+
// 3. AppData (windows only)
26+
// 4. HOME
1927
func ConfigDir() string {
20-
if v := os.Getenv(GH_CONFIG_DIR); v != "" {
21-
return v
28+
var path string
29+
if a := os.Getenv(GH_CONFIG_DIR); a != "" {
30+
path = a
31+
} else if b := os.Getenv(XDG_CONFIG_HOME); b != "" {
32+
path = filepath.Join(b, "gh")
33+
} else if c := os.Getenv(APP_DATA); runtime.GOOS == "windows" && c != "" {
34+
path = filepath.Join(c, "GitHub CLI")
35+
} else {
36+
d, _ := os.UserHomeDir()
37+
path = filepath.Join(d, ".config", "gh")
38+
}
39+
40+
// If the path does not exist try migrating config from default paths
41+
if _, err := os.Stat(path); errors.Is(err, os.ErrNotExist) {
42+
autoMigrateConfigDir(path)
2243
}
2344

24-
homeDir, _ := homeDirAutoMigrate()
25-
return homeDir
45+
return path
46+
}
47+
48+
// Check default paths (os.UserHomeDir, and homedir.Dir) for existing configs
49+
// If configs exist then move them to newPath
50+
// TODO: Remove support for homedir.Dir location in v2
51+
func autoMigrateConfigDir(newPath string) {
52+
path, err := os.UserHomeDir()
53+
if oldPath := filepath.Join(path, ".config", "gh"); err == nil && dirExists(oldPath) {
54+
migrateConfigDir(oldPath, newPath)
55+
return
56+
}
57+
58+
path, err = homedir.Dir()
59+
if oldPath := filepath.Join(path, ".config", "gh"); err == nil && dirExists(oldPath) {
60+
migrateConfigDir(oldPath, newPath)
61+
}
62+
}
63+
64+
func dirExists(path string) bool {
65+
f, err := os.Stat(path)
66+
return err == nil && f.IsDir()
67+
}
68+
69+
var migrateConfigDir = func(oldPath, newPath string) {
70+
if oldPath == newPath {
71+
return
72+
}
73+
74+
_ = os.MkdirAll(filepath.Dir(newPath), 0755)
75+
_ = os.Rename(oldPath, newPath)
2676
}
2777

2878
func ConfigFile() string {
@@ -63,36 +113,6 @@ func HomeDirPath(subdir string) (string, error) {
63113
return newPath, nil
64114
}
65115

66-
// Looks up the `~/.config/gh` directory with backwards-compatibility with go-homedir and auto-migration
67-
// when an old homedir location was found.
68-
func homeDirAutoMigrate() (string, error) {
69-
homeDir, err := os.UserHomeDir()
70-
if err != nil {
71-
// TODO: remove go-homedir fallback in GitHub CLI v2
72-
if legacyDir, err := homedir.Dir(); err == nil {
73-
return filepath.Join(legacyDir, ".config", "gh"), nil
74-
}
75-
return "", err
76-
}
77-
78-
newPath := filepath.Join(homeDir, ".config", "gh")
79-
_, newPathErr := os.Stat(newPath)
80-
if newPathErr == nil || !os.IsNotExist(err) {
81-
return newPath, newPathErr
82-
}
83-
84-
// TODO: remove go-homedir fallback in GitHub CLI v2
85-
if legacyDir, err := homedir.Dir(); err == nil {
86-
legacyPath := filepath.Join(legacyDir, ".config", "gh")
87-
if s, err := os.Stat(legacyPath); err == nil && s.IsDir() {
88-
_ = os.MkdirAll(filepath.Dir(newPath), 0755)
89-
return newPath, os.Rename(legacyPath, newPath)
90-
}
91-
}
92-
93-
return newPath, nil
94-
}
95-
96116
var ReadConfigFile = func(filename string) ([]byte, error) {
97117
f, err := os.Open(filename)
98118
if err != nil {

internal/config/config_file_test.go

Lines changed: 143 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"io/ioutil"
77
"os"
88
"path/filepath"
9+
"runtime"
910
"testing"
1011

1112
"github.com/stretchr/testify/assert"
@@ -152,20 +153,87 @@ func Test_parseConfigFile(t *testing.T) {
152153

153154
func Test_ConfigDir(t *testing.T) {
154155
tests := []struct {
155-
envVar string
156-
want string
156+
name string
157+
onlyWindows bool
158+
env map[string]string
159+
output string
157160
}{
158-
{"/tmp/gh", ".tmp.gh"},
159-
{"", ".config.gh"},
161+
{
162+
name: "no envVars",
163+
env: map[string]string{
164+
"GH_CONFIG_DIR": "",
165+
"XDG_CONFIG_HOME": "",
166+
"AppData": "",
167+
"USERPROFILE": "",
168+
"HOME": "",
169+
},
170+
output: ".config/gh",
171+
},
172+
{
173+
name: "GH_CONFIG_DIR specified",
174+
env: map[string]string{
175+
"GH_CONFIG_DIR": "/tmp/gh_config_dir",
176+
},
177+
output: "/tmp/gh_config_dir",
178+
},
179+
{
180+
name: "XDG_CONFIG_HOME specified",
181+
env: map[string]string{
182+
"XDG_CONFIG_HOME": "/tmp",
183+
},
184+
output: "/tmp/gh",
185+
},
186+
{
187+
name: "GH_CONFIG_DIR and XDG_CONFIG_HOME specified",
188+
env: map[string]string{
189+
"GH_CONFIG_DIR": "/tmp/gh_config_dir",
190+
"XDG_CONFIG_HOME": "/tmp",
191+
},
192+
output: "/tmp/gh_config_dir",
193+
},
194+
{
195+
name: "AppData specified",
196+
onlyWindows: true,
197+
env: map[string]string{
198+
"AppData": "/tmp/",
199+
},
200+
output: "/tmp/GitHub CLI",
201+
},
202+
{
203+
name: "GH_CONFIG_DIR and AppData specified",
204+
onlyWindows: true,
205+
env: map[string]string{
206+
"GH_CONFIG_DIR": "/tmp/gh_config_dir",
207+
"AppData": "/tmp",
208+
},
209+
output: "/tmp/gh_config_dir",
210+
},
211+
{
212+
name: "XDG_CONFIG_HOME and AppData specified",
213+
onlyWindows: true,
214+
env: map[string]string{
215+
"XDG_CONFIG_HOME": "/tmp",
216+
"AppData": "/tmp",
217+
},
218+
output: "/tmp/gh",
219+
},
160220
}
161221

162222
for _, tt := range tests {
163-
t.Run(fmt.Sprintf("envVar: %q", tt.envVar), func(t *testing.T) {
164-
if tt.envVar != "" {
165-
os.Setenv(GH_CONFIG_DIR, tt.envVar)
166-
defer os.Unsetenv(GH_CONFIG_DIR)
223+
if tt.onlyWindows && runtime.GOOS != "windows" {
224+
continue
225+
}
226+
t.Run(tt.name, func(t *testing.T) {
227+
if tt.env != nil {
228+
for k, v := range tt.env {
229+
old := os.Getenv(k)
230+
os.Setenv(k, filepath.FromSlash(v))
231+
defer os.Setenv(k, old)
232+
}
167233
}
168-
assert.Regexp(t, tt.want, ConfigDir())
234+
235+
defer stubMigrateConfigDir()()
236+
assert.Equal(t, filepath.FromSlash(tt.output), ConfigDir())
169237
})
170238
}
171239
}
@@ -194,3 +262,69 @@ func Test_configFile_Write_toDisk(t *testing.T) {
194262
t.Errorf("unexpected hosts.yml: %q", string(configBytes))
195263
}
196264
}
265+
266+
func Test_autoMigrateConfigDir_noMigration(t *testing.T) {
267+
migrateDir := t.TempDir()
268+
269+
homeEnvVar := "HOME"
270+
if runtime.GOOS == "windows" {
271+
homeEnvVar = "USERPROFILE"
272+
}
273+
old := os.Getenv(homeEnvVar)
274+
os.Setenv(homeEnvVar, "/nonexistent-dir")
275+
defer os.Setenv(homeEnvVar, old)
276+
277+
autoMigrateConfigDir(migrateDir)
278+
279+
files, err := ioutil.ReadDir(migrateDir)
280+
assert.NoError(t, err)
281+
assert.Equal(t, 0, len(files))
282+
}
283+
284+
func Test_autoMigrateConfigDir_noMigration_samePath(t *testing.T) {
285+
migrateDir := t.TempDir()
286+
287+
homeEnvVar := "HOME"
288+
if runtime.GOOS == "windows" {
289+
homeEnvVar = "USERPROFILE"
290+
}
291+
old := os.Getenv(homeEnvVar)
292+
os.Setenv(homeEnvVar, migrateDir)
293+
defer os.Setenv(homeEnvVar, old)
294+
295+
autoMigrateConfigDir(migrateDir)
296+
297+
files, err := ioutil.ReadDir(migrateDir)
298+
assert.NoError(t, err)
299+
assert.Equal(t, 0, len(files))
300+
}
301+
302+
func Test_autoMigrateConfigDir_migration(t *testing.T) {
303+
defaultDir := t.TempDir()
304+
dd := filepath.Join(defaultDir, ".config", "gh")
305+
migrateDir := t.TempDir()
306+
md := filepath.Join(migrateDir, ".config", "gh")
307+
308+
homeEnvVar := "HOME"
309+
if runtime.GOOS == "windows" {
310+
homeEnvVar = "USERPROFILE"
311+
}
312+
old := os.Getenv(homeEnvVar)
313+
os.Setenv(homeEnvVar, defaultDir)
314+
defer os.Setenv(homeEnvVar, old)
315+
316+
err := os.MkdirAll(dd, 0777)
317+
assert.NoError(t, err)
318+
f, err := ioutil.TempFile(dd, "")
319+
assert.NoError(t, err)
320+
f.Close()
321+
322+
autoMigrateConfigDir(md)
323+
324+
_, err = ioutil.ReadDir(dd)
325+
assert.True(t, os.IsNotExist(err))
326+
327+
files, err := ioutil.ReadDir(md)
328+
assert.NoError(t, err)
329+
assert.Equal(t, 1, len(files))
330+
}

internal/config/from_env_test.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,13 @@ func TestInheritEnv(t *testing.T) {
1313
orig_GITHUB_ENTERPRISE_TOKEN := os.Getenv("GITHUB_ENTERPRISE_TOKEN")
1414
orig_GH_TOKEN := os.Getenv("GH_TOKEN")
1515
orig_GH_ENTERPRISE_TOKEN := os.Getenv("GH_ENTERPRISE_TOKEN")
16+
orig_AppData := os.Getenv("AppData")
1617
t.Cleanup(func() {
1718
os.Setenv("GITHUB_TOKEN", orig_GITHUB_TOKEN)
1819
os.Setenv("GITHUB_ENTERPRISE_TOKEN", orig_GITHUB_ENTERPRISE_TOKEN)
1920
os.Setenv("GH_TOKEN", orig_GH_TOKEN)
2021
os.Setenv("GH_ENTERPRISE_TOKEN", orig_GH_ENTERPRISE_TOKEN)
22+
os.Setenv("AppData", orig_AppData)
2123
})
2224

2325
type wants struct {
@@ -264,6 +266,7 @@ func TestInheritEnv(t *testing.T) {
264266
os.Setenv("GITHUB_ENTERPRISE_TOKEN", tt.GITHUB_ENTERPRISE_TOKEN)
265267
os.Setenv("GH_TOKEN", tt.GH_TOKEN)
266268
os.Setenv("GH_ENTERPRISE_TOKEN", tt.GH_ENTERPRISE_TOKEN)
269+
os.Setenv("AppData", "")
267270

268271
baseCfg := NewFromString(tt.baseConfig)
269272
cfg := InheritEnv(baseCfg)

internal/config/testing.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,11 @@ func stubConfig(main, hosts string) func() {
6262
ReadConfigFile = orig
6363
}
6464
}
65+
66+
func stubMigrateConfigDir() func() {
67+
orig := migrateConfigDir
68+
migrateConfigDir = func(_, _ string) {}
69+
return func() {
70+
migrateConfigDir = orig
71+
}
72+
}

0 commit comments

Comments
 (0)
X Tutup