Add support for XDG_CONFIG_HOME and AppData on Windows

This commit is contained in:
Sam Coe 2021-05-19 10:26:38 -04:00
parent 55b183f3c9
commit 0d49bfba42
No known key found for this signature in database
GPG key ID: 8E322C20F811D086
4 changed files with 209 additions and 44 deletions

View file

@ -6,6 +6,7 @@ import (
"io/ioutil"
"os"
"path/filepath"
"runtime"
"syscall"
"github.com/mitchellh/go-homedir"
@ -13,16 +14,65 @@ import (
)
const (
GH_CONFIG_DIR = "GH_CONFIG_DIR"
GH_CONFIG_DIR = "GH_CONFIG_DIR"
XDG_CONFIG_HOME = "XDG_CONFIG_HOME"
APP_DATA = "AppData"
)
// Config path precedence
// 1. GH_CONFIG_DIR
// 2. XDG_CONFIG_HOME
// 3. AppData (windows only)
// 4. HOME
func ConfigDir() string {
if v := os.Getenv(GH_CONFIG_DIR); v != "" {
return v
var path string
if a := os.Getenv(GH_CONFIG_DIR); a != "" {
path = a
} else if b := os.Getenv(XDG_CONFIG_HOME); b != "" {
path = filepath.Join(b, "gh")
} else if c := os.Getenv(APP_DATA); runtime.GOOS == "windows" && c != "" {
path = filepath.Join(c, "GitHub CLI")
} else {
d, _ := os.UserHomeDir()
path = filepath.Join(d, ".config", "gh")
}
homeDir, _ := homeDirAutoMigrate()
return homeDir
// If the path does not exist try migrating config from default paths
if _, err := os.Stat(path); errors.Is(err, os.ErrNotExist) {
autoMigrateConfigDir(path)
}
return path
}
// Check default paths (os.UserHomeDir, and homedir.Dir) for existing configs
// If configs exist then move them to newPath
// TODO: Remove support for homedir.Dir location in v2
func autoMigrateConfigDir(newPath string) {
path, err := os.UserHomeDir()
if oldPath := filepath.Join(path, ".config", "gh"); err == nil && dirExists(oldPath) {
migrateConfigDir(oldPath, newPath)
return
}
path, err = homedir.Dir()
if oldPath := filepath.Join(path, ".config", "gh"); err == nil && dirExists(oldPath) {
migrateConfigDir(oldPath, newPath)
}
}
func dirExists(path string) bool {
f, err := os.Stat(path)
return err == nil && f.IsDir()
}
var migrateConfigDir = func(oldPath, newPath string) {
if oldPath == newPath {
return
}
_ = os.MkdirAll(filepath.Dir(newPath), 0755)
_ = os.Rename(oldPath, newPath)
}
func ConfigFile() string {
@ -63,36 +113,6 @@ func HomeDirPath(subdir string) (string, error) {
return newPath, nil
}
// Looks up the `~/.config/gh` directory with backwards-compatibility with go-homedir and auto-migration
// when an old homedir location was found.
func homeDirAutoMigrate() (string, error) {
homeDir, err := os.UserHomeDir()
if err != nil {
// TODO: remove go-homedir fallback in GitHub CLI v2
if legacyDir, err := homedir.Dir(); err == nil {
return filepath.Join(legacyDir, ".config", "gh"), nil
}
return "", err
}
newPath := filepath.Join(homeDir, ".config", "gh")
_, newPathErr := os.Stat(newPath)
if newPathErr == nil || !os.IsNotExist(err) {
return newPath, newPathErr
}
// TODO: remove go-homedir fallback in GitHub CLI v2
if legacyDir, err := homedir.Dir(); err == nil {
legacyPath := filepath.Join(legacyDir, ".config", "gh")
if s, err := os.Stat(legacyPath); err == nil && s.IsDir() {
_ = os.MkdirAll(filepath.Dir(newPath), 0755)
return newPath, os.Rename(legacyPath, newPath)
}
}
return newPath, nil
}
var ReadConfigFile = func(filename string) ([]byte, error) {
f, err := os.Open(filename)
if err != nil {

View file

@ -6,6 +6,7 @@ import (
"io/ioutil"
"os"
"path/filepath"
"runtime"
"testing"
"github.com/stretchr/testify/assert"
@ -152,20 +153,87 @@ func Test_parseConfigFile(t *testing.T) {
func Test_ConfigDir(t *testing.T) {
tests := []struct {
envVar string
want string
name string
onlyWindows bool
env map[string]string
output string
}{
{"/tmp/gh", ".tmp.gh"},
{"", ".config.gh"},
{
name: "no envVars",
env: map[string]string{
"GH_CONFIG_DIR": "",
"XDG_CONFIG_HOME": "",
"AppData": "",
"USERPROFILE": "",
"HOME": "",
},
output: ".config/gh",
},
{
name: "GH_CONFIG_DIR specified",
env: map[string]string{
"GH_CONFIG_DIR": "/tmp/gh_config_dir",
},
output: "/tmp/gh_config_dir",
},
{
name: "XDG_CONFIG_HOME specified",
env: map[string]string{
"XDG_CONFIG_HOME": "/tmp",
},
output: "/tmp/gh",
},
{
name: "GH_CONFIG_DIR and XDG_CONFIG_HOME specified",
env: map[string]string{
"GH_CONFIG_DIR": "/tmp/gh_config_dir",
"XDG_CONFIG_HOME": "/tmp",
},
output: "/tmp/gh_config_dir",
},
{
name: "AppData specified",
onlyWindows: true,
env: map[string]string{
"AppData": "/tmp/",
},
output: "/tmp/GitHub CLI",
},
{
name: "GH_CONFIG_DIR and AppData specified",
onlyWindows: true,
env: map[string]string{
"GH_CONFIG_DIR": "/tmp/gh_config_dir",
"AppData": "/tmp",
},
output: "/tmp/gh_config_dir",
},
{
name: "XDG_CONFIG_HOME and AppData specified",
onlyWindows: true,
env: map[string]string{
"XDG_CONFIG_HOME": "/tmp",
"AppData": "/tmp",
},
output: "/tmp/gh",
},
}
for _, tt := range tests {
t.Run(fmt.Sprintf("envVar: %q", tt.envVar), func(t *testing.T) {
if tt.envVar != "" {
os.Setenv(GH_CONFIG_DIR, tt.envVar)
defer os.Unsetenv(GH_CONFIG_DIR)
if tt.onlyWindows && runtime.GOOS != "windows" {
continue
}
t.Run(tt.name, func(t *testing.T) {
if tt.env != nil {
for k, v := range tt.env {
old := os.Getenv(k)
os.Setenv(k, filepath.FromSlash(v))
defer os.Setenv(k, old)
}
}
assert.Regexp(t, tt.want, ConfigDir())
defer stubMigrateConfigDir()()
assert.Equal(t, filepath.FromSlash(tt.output), ConfigDir())
})
}
}
@ -194,3 +262,69 @@ func Test_configFile_Write_toDisk(t *testing.T) {
t.Errorf("unexpected hosts.yml: %q", string(configBytes))
}
}
func Test_autoMigrateConfigDir_noMigration(t *testing.T) {
migrateDir := t.TempDir()
homeEnvVar := "HOME"
if runtime.GOOS == "windows" {
homeEnvVar = "USERPROFILE"
}
old := os.Getenv(homeEnvVar)
os.Setenv(homeEnvVar, "/nonexistent-dir")
defer os.Setenv(homeEnvVar, old)
autoMigrateConfigDir(migrateDir)
files, err := ioutil.ReadDir(migrateDir)
assert.NoError(t, err)
assert.Equal(t, 0, len(files))
}
func Test_autoMigrateConfigDir_noMigration_samePath(t *testing.T) {
migrateDir := t.TempDir()
homeEnvVar := "HOME"
if runtime.GOOS == "windows" {
homeEnvVar = "USERPROFILE"
}
old := os.Getenv(homeEnvVar)
os.Setenv(homeEnvVar, migrateDir)
defer os.Setenv(homeEnvVar, old)
autoMigrateConfigDir(migrateDir)
files, err := ioutil.ReadDir(migrateDir)
assert.NoError(t, err)
assert.Equal(t, 0, len(files))
}
func Test_autoMigrateConfigDir_migration(t *testing.T) {
defaultDir := t.TempDir()
dd := filepath.Join(defaultDir, ".config", "gh")
migrateDir := t.TempDir()
md := filepath.Join(migrateDir, ".config", "gh")
homeEnvVar := "HOME"
if runtime.GOOS == "windows" {
homeEnvVar = "USERPROFILE"
}
old := os.Getenv(homeEnvVar)
os.Setenv(homeEnvVar, defaultDir)
defer os.Setenv(homeEnvVar, old)
err := os.MkdirAll(dd, 0777)
assert.NoError(t, err)
f, err := ioutil.TempFile(dd, "")
assert.NoError(t, err)
f.Close()
autoMigrateConfigDir(md)
_, err = ioutil.ReadDir(dd)
assert.True(t, os.IsNotExist(err))
files, err := ioutil.ReadDir(md)
assert.NoError(t, err)
assert.Equal(t, 1, len(files))
}

View file

@ -13,11 +13,13 @@ func TestInheritEnv(t *testing.T) {
orig_GITHUB_ENTERPRISE_TOKEN := os.Getenv("GITHUB_ENTERPRISE_TOKEN")
orig_GH_TOKEN := os.Getenv("GH_TOKEN")
orig_GH_ENTERPRISE_TOKEN := os.Getenv("GH_ENTERPRISE_TOKEN")
orig_AppData := os.Getenv("AppData")
t.Cleanup(func() {
os.Setenv("GITHUB_TOKEN", orig_GITHUB_TOKEN)
os.Setenv("GITHUB_ENTERPRISE_TOKEN", orig_GITHUB_ENTERPRISE_TOKEN)
os.Setenv("GH_TOKEN", orig_GH_TOKEN)
os.Setenv("GH_ENTERPRISE_TOKEN", orig_GH_ENTERPRISE_TOKEN)
os.Setenv("AppData", orig_AppData)
})
type wants struct {
@ -264,6 +266,7 @@ func TestInheritEnv(t *testing.T) {
os.Setenv("GITHUB_ENTERPRISE_TOKEN", tt.GITHUB_ENTERPRISE_TOKEN)
os.Setenv("GH_TOKEN", tt.GH_TOKEN)
os.Setenv("GH_ENTERPRISE_TOKEN", tt.GH_ENTERPRISE_TOKEN)
os.Setenv("AppData", "")
baseCfg := NewFromString(tt.baseConfig)
cfg := InheritEnv(baseCfg)

View file

@ -62,3 +62,11 @@ func stubConfig(main, hosts string) func() {
ReadConfigFile = orig
}
}
func stubMigrateConfigDir() func() {
orig := migrateConfigDir
migrateConfigDir = func(_, _ string) {}
return func() {
migrateConfigDir = orig
}
}