github.com/cli/cli@v1.14.1-0.20210902173923-1af6a669e342/internal/config/config_file.go (about) 1 package config 2 3 import ( 4 "errors" 5 "fmt" 6 "io/ioutil" 7 "os" 8 "path/filepath" 9 "runtime" 10 "syscall" 11 12 "gopkg.in/yaml.v3" 13 ) 14 15 const ( 16 GH_CONFIG_DIR = "GH_CONFIG_DIR" 17 XDG_CONFIG_HOME = "XDG_CONFIG_HOME" 18 XDG_STATE_HOME = "XDG_STATE_HOME" 19 XDG_DATA_HOME = "XDG_DATA_HOME" 20 APP_DATA = "AppData" 21 LOCAL_APP_DATA = "LocalAppData" 22 ) 23 24 // Config path precedence 25 // 1. GH_CONFIG_DIR 26 // 2. XDG_CONFIG_HOME 27 // 3. AppData (windows only) 28 // 4. HOME 29 func ConfigDir() string { 30 var path string 31 if a := os.Getenv(GH_CONFIG_DIR); a != "" { 32 path = a 33 } else if b := os.Getenv(XDG_CONFIG_HOME); b != "" { 34 path = filepath.Join(b, "gh") 35 } else if c := os.Getenv(APP_DATA); runtime.GOOS == "windows" && c != "" { 36 path = filepath.Join(c, "GitHub CLI") 37 } else { 38 d, _ := os.UserHomeDir() 39 path = filepath.Join(d, ".config", "gh") 40 } 41 42 // If the path does not exist and the GH_CONFIG_DIR flag is not set try 43 // migrating config from default paths. 44 if !dirExists(path) && os.Getenv(GH_CONFIG_DIR) == "" { 45 _ = autoMigrateConfigDir(path) 46 } 47 48 return path 49 } 50 51 // State path precedence 52 // 1. XDG_CONFIG_HOME 53 // 2. LocalAppData (windows only) 54 // 3. HOME 55 func StateDir() string { 56 var path string 57 if a := os.Getenv(XDG_STATE_HOME); a != "" { 58 path = filepath.Join(a, "gh") 59 } else if b := os.Getenv(LOCAL_APP_DATA); runtime.GOOS == "windows" && b != "" { 60 path = filepath.Join(b, "GitHub CLI") 61 } else { 62 c, _ := os.UserHomeDir() 63 path = filepath.Join(c, ".local", "state", "gh") 64 } 65 66 // If the path does not exist try migrating state from default paths 67 if !dirExists(path) { 68 _ = autoMigrateStateDir(path) 69 } 70 71 return path 72 } 73 74 // Data path precedence 75 // 1. XDG_DATA_HOME 76 // 2. LocalAppData (windows only) 77 // 3. HOME 78 func DataDir() string { 79 var path string 80 if a := os.Getenv(XDG_DATA_HOME); a != "" { 81 path = filepath.Join(a, "gh") 82 } else if b := os.Getenv(LOCAL_APP_DATA); runtime.GOOS == "windows" && b != "" { 83 path = filepath.Join(b, "GitHub CLI") 84 } else { 85 c, _ := os.UserHomeDir() 86 path = filepath.Join(c, ".local", "share", "gh") 87 } 88 89 return path 90 } 91 92 var errSamePath = errors.New("same path") 93 var errNotExist = errors.New("not exist") 94 95 // Check default path, os.UserHomeDir, for existing configs 96 // If configs exist then move them to newPath 97 func autoMigrateConfigDir(newPath string) error { 98 path, err := os.UserHomeDir() 99 if oldPath := filepath.Join(path, ".config", "gh"); err == nil && dirExists(oldPath) { 100 return migrateDir(oldPath, newPath) 101 } 102 103 return errNotExist 104 } 105 106 // Check default path, os.UserHomeDir, for existing state file (state.yml) 107 // If state file exist then move it to newPath 108 func autoMigrateStateDir(newPath string) error { 109 path, err := os.UserHomeDir() 110 if oldPath := filepath.Join(path, ".config", "gh"); err == nil && dirExists(oldPath) { 111 return migrateFile(oldPath, newPath, "state.yml") 112 } 113 114 return errNotExist 115 } 116 117 func migrateFile(oldPath, newPath, file string) error { 118 if oldPath == newPath { 119 return errSamePath 120 } 121 122 oldFile := filepath.Join(oldPath, file) 123 newFile := filepath.Join(newPath, file) 124 125 if !fileExists(oldFile) { 126 return errNotExist 127 } 128 129 _ = os.MkdirAll(filepath.Dir(newFile), 0755) 130 return os.Rename(oldFile, newFile) 131 } 132 133 func migrateDir(oldPath, newPath string) error { 134 if oldPath == newPath { 135 return errSamePath 136 } 137 138 if !dirExists(oldPath) { 139 return errNotExist 140 } 141 142 _ = os.MkdirAll(filepath.Dir(newPath), 0755) 143 return os.Rename(oldPath, newPath) 144 } 145 146 func dirExists(path string) bool { 147 f, err := os.Stat(path) 148 return err == nil && f.IsDir() 149 } 150 151 func fileExists(path string) bool { 152 f, err := os.Stat(path) 153 return err == nil && !f.IsDir() 154 } 155 156 func ConfigFile() string { 157 return filepath.Join(ConfigDir(), "config.yml") 158 } 159 160 func HostsConfigFile() string { 161 return filepath.Join(ConfigDir(), "hosts.yml") 162 } 163 164 func ParseDefaultConfig() (Config, error) { 165 return parseConfig(ConfigFile()) 166 } 167 168 func HomeDirPath(subdir string) (string, error) { 169 homeDir, err := os.UserHomeDir() 170 if err != nil { 171 return "", err 172 } 173 174 newPath := filepath.Join(homeDir, subdir) 175 return newPath, nil 176 } 177 178 var ReadConfigFile = func(filename string) ([]byte, error) { 179 f, err := os.Open(filename) 180 if err != nil { 181 return nil, pathError(err) 182 } 183 defer f.Close() 184 185 data, err := ioutil.ReadAll(f) 186 if err != nil { 187 return nil, err 188 } 189 190 return data, nil 191 } 192 193 var WriteConfigFile = func(filename string, data []byte) error { 194 err := os.MkdirAll(filepath.Dir(filename), 0771) 195 if err != nil { 196 return pathError(err) 197 } 198 199 cfgFile, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) // cargo coded from setup 200 if err != nil { 201 return err 202 } 203 defer cfgFile.Close() 204 205 _, err = cfgFile.Write(data) 206 return err 207 } 208 209 var BackupConfigFile = func(filename string) error { 210 return os.Rename(filename, filename+".bak") 211 } 212 213 func parseConfigFile(filename string) ([]byte, *yaml.Node, error) { 214 data, err := ReadConfigFile(filename) 215 if err != nil { 216 return nil, nil, err 217 } 218 219 root, err := parseConfigData(data) 220 if err != nil { 221 return nil, nil, err 222 } 223 return data, root, err 224 } 225 226 func parseConfigData(data []byte) (*yaml.Node, error) { 227 var root yaml.Node 228 err := yaml.Unmarshal(data, &root) 229 if err != nil { 230 return nil, err 231 } 232 233 if len(root.Content) == 0 { 234 return &yaml.Node{ 235 Kind: yaml.DocumentNode, 236 Content: []*yaml.Node{{Kind: yaml.MappingNode}}, 237 }, nil 238 } 239 if root.Content[0].Kind != yaml.MappingNode { 240 return &root, fmt.Errorf("expected a top level map") 241 } 242 return &root, nil 243 } 244 245 func isLegacy(root *yaml.Node) bool { 246 for _, v := range root.Content[0].Content { 247 if v.Value == "github.com" { 248 return true 249 } 250 } 251 252 return false 253 } 254 255 func migrateConfig(filename string) error { 256 b, err := ReadConfigFile(filename) 257 if err != nil { 258 return err 259 } 260 261 var hosts map[string][]yaml.Node 262 err = yaml.Unmarshal(b, &hosts) 263 if err != nil { 264 return fmt.Errorf("error decoding legacy format: %w", err) 265 } 266 267 cfg := NewBlankConfig() 268 for hostname, entries := range hosts { 269 if len(entries) < 1 { 270 continue 271 } 272 mapContent := entries[0].Content 273 for i := 0; i < len(mapContent)-1; i += 2 { 274 if err := cfg.Set(hostname, mapContent[i].Value, mapContent[i+1].Value); err != nil { 275 return err 276 } 277 } 278 } 279 280 err = BackupConfigFile(filename) 281 if err != nil { 282 return fmt.Errorf("failed to back up existing config: %w", err) 283 } 284 285 return cfg.Write() 286 } 287 288 func parseConfig(filename string) (Config, error) { 289 _, root, err := parseConfigFile(filename) 290 if err != nil { 291 if os.IsNotExist(err) { 292 root = NewBlankRoot() 293 } else { 294 return nil, err 295 } 296 } 297 298 if isLegacy(root) { 299 err = migrateConfig(filename) 300 if err != nil { 301 return nil, fmt.Errorf("error migrating legacy config: %w", err) 302 } 303 304 _, root, err = parseConfigFile(filename) 305 if err != nil { 306 return nil, fmt.Errorf("failed to reparse migrated config: %w", err) 307 } 308 } else { 309 if _, hostsRoot, err := parseConfigFile(HostsConfigFile()); err == nil { 310 if len(hostsRoot.Content[0].Content) > 0 { 311 newContent := []*yaml.Node{ 312 {Value: "hosts"}, 313 hostsRoot.Content[0], 314 } 315 restContent := root.Content[0].Content 316 root.Content[0].Content = append(newContent, restContent...) 317 } 318 } else if !errors.Is(err, os.ErrNotExist) { 319 return nil, err 320 } 321 } 322 323 return NewConfig(root), nil 324 } 325 326 func pathError(err error) error { 327 var pathError *os.PathError 328 if errors.As(err, &pathError) && errors.Is(pathError.Err, syscall.ENOTDIR) { 329 if p := findRegularFile(pathError.Path); p != "" { 330 return fmt.Errorf("remove or rename regular file `%s` (must be a directory)", p) 331 } 332 333 } 334 return err 335 } 336 337 func findRegularFile(p string) string { 338 for { 339 if s, err := os.Stat(p); err == nil && s.Mode().IsRegular() { 340 return p 341 } 342 newPath := filepath.Dir(p) 343 if newPath == p || newPath == "/" || newPath == "." { 344 break 345 } 346 p = newPath 347 } 348 return "" 349 }