internal/config/config_file.go (165 lines of code) (raw):

package config import ( "errors" "fmt" "os" "path" "path/filepath" "syscall" "github.com/mitchellh/go-homedir" "gopkg.in/yaml.v3" ) var ( cachedConfig Config configError error ) // ConfigDir returns the config directory func ConfigDir() string { glabDir := os.Getenv("GLAB_CONFIG_DIR") if glabDir != "" { return glabDir } usrConfigHome := os.Getenv("XDG_CONFIG_HOME") if usrConfigHome == "" { usrConfigHome = os.Getenv("HOME") if usrConfigHome == "" { usrConfigHome, _ = homedir.Expand("~/.config") } else { usrConfigHome = filepath.Join(usrConfigHome, ".config") } } return filepath.Join(usrConfigHome, "glab-cli") } // ConfigFile returns the config file path func ConfigFile() string { return path.Join(ConfigDir(), "config.yml") } // Init initialises and returns the cached configuration func Init() (Config, error) { if cachedConfig != nil || configError != nil { return cachedConfig, configError } cachedConfig, configError = ParseDefaultConfig() if os.IsNotExist(configError) { if err := cachedConfig.WriteAll(); err != nil { return nil, err } configError = nil } return cachedConfig, configError } func ParseDefaultConfig() (Config, error) { return ParseConfig(ConfigFile()) } var ReadConfigFile = func(filename string) ([]byte, error) { data, err := os.ReadFile(filename) if err != nil { return nil, pathError(err) } return data, nil } var WriteConfigFile = func(filename string, data []byte) error { err := os.MkdirAll(path.Dir(filename), 0o750) if err != nil { return pathError(err) } _, err = os.ReadFile(filename) if err != nil && !os.IsNotExist(err) { return err } err = WriteFile(filename, data, 0o600) return err } func ParseConfigFile(filename string) ([]byte, *yaml.Node, error) { stat, err := os.Stat(filename) // we want to check if there actually is a file, sometimes // configs are just passed via stubs if err == nil { if !HasSecurePerms(stat.Mode().Perm()) { return nil, nil, fmt.Errorf("%s has the permissions %o, but glab requires 600.\nConsider running `chmod 600 %s`", filename, stat.Mode(), filename, ) } } data, err := ReadConfigFile(filename) if err != nil { return nil, nil, err } root, err := parseConfigData(data) if err != nil { return nil, nil, err } return data, root, err } func parseConfigData(data []byte) (*yaml.Node, error) { var root yaml.Node err := yaml.Unmarshal(data, &root) if err != nil { return nil, err } if len(root.Content) == 0 { return &yaml.Node{ Kind: yaml.DocumentNode, Content: []*yaml.Node{{Kind: yaml.MappingNode}}, }, nil } if root.Content[0].Kind != yaml.MappingNode { return &root, fmt.Errorf("expected a top level map") } return &root, nil } func ParseConfig(filename string) (Config, error) { _, root, err := ParseConfigFile(filename) var confError error if err != nil { if os.IsNotExist(err) { root = NewBlankRoot() confError = os.ErrNotExist } else { return nil, err } } // Load local config file if _, localRoot, err := ParseConfigFile(LocalConfigFile()); err == nil { if len(localRoot.Content[0].Content) > 0 { newContent := []*yaml.Node{ {Value: "local"}, localRoot.Content[0], } restContent := root.Content[0].Content root.Content[0].Content = append(newContent, restContent...) } } // Load aliases config file if _, aliasesRoot, err := ParseConfigFile(aliasesConfigFile()); err == nil { if len(aliasesRoot.Content[0].Content) > 0 { newContent := []*yaml.Node{ {Value: "aliases"}, aliasesRoot.Content[0], } restContent := root.Content[0].Content root.Content[0].Content = append(newContent, restContent...) } } else if !errors.Is(err, os.ErrNotExist) { return nil, err } return NewConfig(root), confError } func pathError(err error) error { var pathError *os.PathError if errors.As(err, &pathError) && errors.Is(pathError.Err, syscall.ENOTDIR) { if p := findRegularFile(pathError.Path); p != "" { return fmt.Errorf("remove or rename regular file `%s` (must be a directory)", p) } } return err } func findRegularFile(p string) string { for { if s, err := os.Stat(p); err == nil && s.Mode().IsRegular() { return p } newPath := path.Dir(p) if newPath == p || newPath == "/" || newPath == "." { break } p = newPath } return "" }