github.com/secman-team/gh-api@v1.8.2/core/config/config_file.go (about) 1 package config 2 3 import ( 4 "errors" 5 "fmt" 6 "io" 7 "io/ioutil" 8 "os" 9 "path" 10 "path/filepath" 11 "syscall" 12 13 "github.com/mitchellh/go-homedir" 14 "gopkg.in/yaml.v3" 15 ) 16 17 const ( 18 GH_CONFIG_DIR = "GH_CONFIG_DIR" 19 ) 20 21 func ConfigDir() string { 22 if v := os.Getenv(GH_CONFIG_DIR); v != "" { 23 return v 24 } 25 26 homeDir, _ := homeDirAutoMigrate() 27 return homeDir 28 } 29 30 func ConfigFile() string { 31 return path.Join(ConfigDir(), "config.yml") 32 } 33 34 func HostsConfigFile() string { 35 return path.Join(ConfigDir(), "hosts.yml") 36 } 37 38 func ParseDefaultConfig() (Config, error) { 39 return parseConfig(ConfigFile()) 40 } 41 42 func HomeDirPath(subdir string) (string, error) { 43 homeDir, err := os.UserHomeDir() 44 if err != nil { 45 // TODO: remove go-homedir fallback in GitHub CLI v2 46 if legacyDir, err := homedir.Dir(); err == nil { 47 return filepath.Join(legacyDir, subdir), nil 48 } 49 return "", err 50 } 51 52 newPath := filepath.Join(homeDir, subdir) 53 if s, err := os.Stat(newPath); err == nil && s.IsDir() { 54 return newPath, nil 55 } 56 57 // TODO: remove go-homedir fallback in GitHub CLI v2 58 if legacyDir, err := homedir.Dir(); err == nil { 59 legacyPath := filepath.Join(legacyDir, subdir) 60 if s, err := os.Stat(legacyPath); err == nil && s.IsDir() { 61 return legacyPath, nil 62 } 63 } 64 65 return newPath, nil 66 } 67 68 // Looks up the `~/.config/gh` directory with backwards-compatibility with go-homedir and auto-migration 69 // when an old homedir location was found. 70 func homeDirAutoMigrate() (string, error) { 71 homeDir, err := os.UserHomeDir() 72 if err != nil { 73 // TODO: remove go-homedir fallback in GitHub CLI v2 74 if legacyDir, err := homedir.Dir(); err == nil { 75 return filepath.Join(legacyDir, ".config", "gh"), nil 76 } 77 return "", err 78 } 79 80 newPath := filepath.Join(homeDir, ".config", "gh") 81 _, newPathErr := os.Stat(newPath) 82 if newPathErr == nil || !os.IsNotExist(err) { 83 return newPath, newPathErr 84 } 85 86 // TODO: remove go-homedir fallback in GitHub CLI v2 87 if legacyDir, err := homedir.Dir(); err == nil { 88 legacyPath := filepath.Join(legacyDir, ".config", "gh") 89 if s, err := os.Stat(legacyPath); err == nil && s.IsDir() { 90 _ = os.MkdirAll(filepath.Dir(newPath), 0755) 91 return newPath, os.Rename(legacyPath, newPath) 92 } 93 } 94 95 return newPath, nil 96 } 97 98 var ReadConfigFile = func(filename string) ([]byte, error) { 99 f, err := os.Open(filename) 100 if err != nil { 101 return nil, pathError(err) 102 } 103 defer f.Close() 104 105 data, err := ioutil.ReadAll(f) 106 if err != nil { 107 return nil, err 108 } 109 110 return data, nil 111 } 112 113 var WriteConfigFile = func(filename string, data []byte) error { 114 err := os.MkdirAll(path.Dir(filename), 0771) 115 if err != nil { 116 return pathError(err) 117 } 118 119 cfgFile, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) // cargo coded from setup 120 if err != nil { 121 return err 122 } 123 defer cfgFile.Close() 124 125 n, err := cfgFile.Write(data) 126 if err == nil && n < len(data) { 127 err = io.ErrShortWrite 128 } 129 130 return err 131 } 132 133 var BackupConfigFile = func(filename string) error { 134 return os.Rename(filename, filename+".bak") 135 } 136 137 func parseConfigFile(filename string) ([]byte, *yaml.Node, error) { 138 data, err := ReadConfigFile(filename) 139 if err != nil { 140 return nil, nil, err 141 } 142 143 root, err := parseConfigData(data) 144 if err != nil { 145 return nil, nil, err 146 } 147 return data, root, err 148 } 149 150 func parseConfigData(data []byte) (*yaml.Node, error) { 151 var root yaml.Node 152 err := yaml.Unmarshal(data, &root) 153 if err != nil { 154 return nil, err 155 } 156 157 if len(root.Content) == 0 { 158 return &yaml.Node{ 159 Kind: yaml.DocumentNode, 160 Content: []*yaml.Node{{Kind: yaml.MappingNode}}, 161 }, nil 162 } 163 if root.Content[0].Kind != yaml.MappingNode { 164 return &root, fmt.Errorf("expected a top level map") 165 } 166 return &root, nil 167 } 168 169 func isLegacy(root *yaml.Node) bool { 170 for _, v := range root.Content[0].Content { 171 if v.Value == "github.com" { 172 return true 173 } 174 } 175 176 return false 177 } 178 179 func migrateConfig(filename string) error { 180 b, err := ReadConfigFile(filename) 181 if err != nil { 182 return err 183 } 184 185 var hosts map[string][]yaml.Node 186 err = yaml.Unmarshal(b, &hosts) 187 if err != nil { 188 return fmt.Errorf("error decoding legacy format: %w", err) 189 } 190 191 cfg := NewBlankConfig() 192 for hostname, entries := range hosts { 193 if len(entries) < 1 { 194 continue 195 } 196 mapContent := entries[0].Content 197 for i := 0; i < len(mapContent)-1; i += 2 { 198 if err := cfg.Set(hostname, mapContent[i].Value, mapContent[i+1].Value); err != nil { 199 return err 200 } 201 } 202 } 203 204 err = BackupConfigFile(filename) 205 if err != nil { 206 return fmt.Errorf("failed to back up existing config: %w", err) 207 } 208 209 return cfg.Write() 210 } 211 212 func parseConfig(filename string) (Config, error) { 213 _, root, err := parseConfigFile(filename) 214 if err != nil { 215 if os.IsNotExist(err) { 216 root = NewBlankRoot() 217 } else { 218 return nil, err 219 } 220 } 221 222 if isLegacy(root) { 223 err = migrateConfig(filename) 224 if err != nil { 225 return nil, fmt.Errorf("error migrating legacy config: %w", err) 226 } 227 228 _, root, err = parseConfigFile(filename) 229 if err != nil { 230 return nil, fmt.Errorf("failed to reparse migrated config: %w", err) 231 } 232 } else { 233 if _, hostsRoot, err := parseConfigFile(HostsConfigFile()); err == nil { 234 if len(hostsRoot.Content[0].Content) > 0 { 235 newContent := []*yaml.Node{ 236 {Value: "hosts"}, 237 hostsRoot.Content[0], 238 } 239 restContent := root.Content[0].Content 240 root.Content[0].Content = append(newContent, restContent...) 241 } 242 } else if !errors.Is(err, os.ErrNotExist) { 243 return nil, err 244 } 245 } 246 247 return NewConfig(root), nil 248 } 249 250 func pathError(err error) error { 251 var pathError *os.PathError 252 if errors.As(err, &pathError) && errors.Is(pathError.Err, syscall.ENOTDIR) { 253 if p := findRegularFile(pathError.Path); p != "" { 254 return fmt.Errorf("remove or rename regular file `%s` (must be a directory)", p) 255 } 256 257 } 258 return err 259 } 260 261 func findRegularFile(p string) string { 262 for { 263 if s, err := os.Stat(p); err == nil && s.Mode().IsRegular() { 264 return p 265 } 266 newPath := path.Dir(p) 267 if newPath == p || newPath == "/" || newPath == "." { 268 break 269 } 270 p = newPath 271 } 272 return "" 273 }