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  }