github.com/cli/cli@v1.14.1-0.20210902173923-1af6a669e342/internal/config/from_file.go (about)

     1  package config
     2  
     3  import (
     4  	"bytes"
     5  	"errors"
     6  	"fmt"
     7  	"sort"
     8  	"strings"
     9  
    10  	"github.com/cli/cli/internal/ghinstance"
    11  	"gopkg.in/yaml.v3"
    12  )
    13  
    14  // This type implements a Config interface and represents a config file on disk.
    15  type fileConfig struct {
    16  	ConfigMap
    17  	documentRoot *yaml.Node
    18  }
    19  
    20  type HostConfig struct {
    21  	ConfigMap
    22  	Host string
    23  }
    24  
    25  func (c *fileConfig) Root() *yaml.Node {
    26  	return c.ConfigMap.Root
    27  }
    28  
    29  func (c *fileConfig) Get(hostname, key string) (string, error) {
    30  	val, _, err := c.GetWithSource(hostname, key)
    31  	return val, err
    32  }
    33  
    34  func (c *fileConfig) GetWithSource(hostname, key string) (string, string, error) {
    35  	if hostname != "" {
    36  		var notFound *NotFoundError
    37  
    38  		hostCfg, err := c.configForHost(hostname)
    39  		if err != nil && !errors.As(err, &notFound) {
    40  			return "", "", err
    41  		}
    42  
    43  		var hostValue string
    44  		if hostCfg != nil {
    45  			hostValue, err = hostCfg.GetStringValue(key)
    46  			if err != nil && !errors.As(err, &notFound) {
    47  				return "", "", err
    48  			}
    49  		}
    50  
    51  		if hostValue != "" {
    52  			return hostValue, HostsConfigFile(), nil
    53  		}
    54  	}
    55  
    56  	defaultSource := ConfigFile()
    57  
    58  	value, err := c.GetStringValue(key)
    59  
    60  	var notFound *NotFoundError
    61  
    62  	if err != nil && errors.As(err, &notFound) {
    63  		return defaultFor(key), defaultSource, nil
    64  	} else if err != nil {
    65  		return "", defaultSource, err
    66  	}
    67  
    68  	if value == "" {
    69  		return defaultFor(key), defaultSource, nil
    70  	}
    71  
    72  	return value, defaultSource, nil
    73  }
    74  
    75  func (c *fileConfig) Set(hostname, key, value string) error {
    76  	if hostname == "" {
    77  		return c.SetStringValue(key, value)
    78  	} else {
    79  		hostCfg, err := c.configForHost(hostname)
    80  		var notFound *NotFoundError
    81  		if errors.As(err, &notFound) {
    82  			hostCfg = c.makeConfigForHost(hostname)
    83  		} else if err != nil {
    84  			return err
    85  		}
    86  		return hostCfg.SetStringValue(key, value)
    87  	}
    88  }
    89  
    90  func (c *fileConfig) UnsetHost(hostname string) {
    91  	if hostname == "" {
    92  		return
    93  	}
    94  
    95  	hostsEntry, err := c.FindEntry("hosts")
    96  	if err != nil {
    97  		return
    98  	}
    99  
   100  	cm := ConfigMap{hostsEntry.ValueNode}
   101  	cm.RemoveEntry(hostname)
   102  }
   103  
   104  func (c *fileConfig) configForHost(hostname string) (*HostConfig, error) {
   105  	hosts, err := c.hostEntries()
   106  	if err != nil {
   107  		return nil, err
   108  	}
   109  
   110  	for _, hc := range hosts {
   111  		if strings.EqualFold(hc.Host, hostname) {
   112  			return hc, nil
   113  		}
   114  	}
   115  	return nil, &NotFoundError{fmt.Errorf("could not find config entry for %q", hostname)}
   116  }
   117  
   118  func (c *fileConfig) CheckWriteable(hostname, key string) error {
   119  	// TODO: check filesystem permissions
   120  	return nil
   121  }
   122  
   123  func (c *fileConfig) Write() error {
   124  	mainData := yaml.Node{Kind: yaml.MappingNode}
   125  	hostsData := yaml.Node{Kind: yaml.MappingNode}
   126  
   127  	nodes := c.documentRoot.Content[0].Content
   128  	for i := 0; i < len(nodes)-1; i += 2 {
   129  		if nodes[i].Value == "hosts" {
   130  			hostsData.Content = append(hostsData.Content, nodes[i+1].Content...)
   131  		} else {
   132  			mainData.Content = append(mainData.Content, nodes[i], nodes[i+1])
   133  		}
   134  	}
   135  
   136  	mainBytes, err := yaml.Marshal(&mainData)
   137  	if err != nil {
   138  		return err
   139  	}
   140  
   141  	filename := ConfigFile()
   142  	err = WriteConfigFile(filename, yamlNormalize(mainBytes))
   143  	if err != nil {
   144  		return err
   145  	}
   146  
   147  	hostsBytes, err := yaml.Marshal(&hostsData)
   148  	if err != nil {
   149  		return err
   150  	}
   151  
   152  	return WriteConfigFile(HostsConfigFile(), yamlNormalize(hostsBytes))
   153  }
   154  
   155  func (c *fileConfig) Aliases() (*AliasConfig, error) {
   156  	// The complexity here is for dealing with either a missing or empty aliases key. It's something
   157  	// we'll likely want for other config sections at some point.
   158  	entry, err := c.FindEntry("aliases")
   159  	var nfe *NotFoundError
   160  	notFound := errors.As(err, &nfe)
   161  	if err != nil && !notFound {
   162  		return nil, err
   163  	}
   164  
   165  	toInsert := []*yaml.Node{}
   166  
   167  	keyNode := entry.KeyNode
   168  	valueNode := entry.ValueNode
   169  
   170  	if keyNode == nil {
   171  		keyNode = &yaml.Node{
   172  			Kind:  yaml.ScalarNode,
   173  			Value: "aliases",
   174  		}
   175  		toInsert = append(toInsert, keyNode)
   176  	}
   177  
   178  	if valueNode == nil || valueNode.Kind != yaml.MappingNode {
   179  		valueNode = &yaml.Node{
   180  			Kind:  yaml.MappingNode,
   181  			Value: "",
   182  		}
   183  		toInsert = append(toInsert, valueNode)
   184  	}
   185  
   186  	if len(toInsert) > 0 {
   187  		newContent := []*yaml.Node{}
   188  		if notFound {
   189  			newContent = append(c.Root().Content, keyNode, valueNode)
   190  		} else {
   191  			for i := 0; i < len(c.Root().Content); i++ {
   192  				if i == entry.Index {
   193  					newContent = append(newContent, keyNode, valueNode)
   194  					i++
   195  				} else {
   196  					newContent = append(newContent, c.Root().Content[i])
   197  				}
   198  			}
   199  		}
   200  		c.Root().Content = newContent
   201  	}
   202  
   203  	return &AliasConfig{
   204  		Parent:    c,
   205  		ConfigMap: ConfigMap{Root: valueNode},
   206  	}, nil
   207  }
   208  
   209  func (c *fileConfig) hostEntries() ([]*HostConfig, error) {
   210  	entry, err := c.FindEntry("hosts")
   211  	if err != nil {
   212  		return []*HostConfig{}, nil
   213  	}
   214  
   215  	hostConfigs, err := c.parseHosts(entry.ValueNode)
   216  	if err != nil {
   217  		return nil, fmt.Errorf("could not parse hosts config: %w", err)
   218  	}
   219  
   220  	return hostConfigs, nil
   221  }
   222  
   223  // Hosts returns a list of all known hostnames configured in hosts.yml
   224  func (c *fileConfig) Hosts() ([]string, error) {
   225  	entries, err := c.hostEntries()
   226  	if err != nil {
   227  		return nil, err
   228  	}
   229  
   230  	hostnames := []string{}
   231  	for _, entry := range entries {
   232  		hostnames = append(hostnames, entry.Host)
   233  	}
   234  
   235  	sort.SliceStable(hostnames, func(i, j int) bool { return hostnames[i] == ghinstance.Default() })
   236  
   237  	return hostnames, nil
   238  }
   239  
   240  func (c *fileConfig) DefaultHost() (string, error) {
   241  	val, _, err := c.DefaultHostWithSource()
   242  	return val, err
   243  }
   244  
   245  func (c *fileConfig) DefaultHostWithSource() (string, string, error) {
   246  	hosts, err := c.Hosts()
   247  	if err == nil && len(hosts) == 1 {
   248  		return hosts[0], HostsConfigFile(), nil
   249  	}
   250  
   251  	return ghinstance.Default(), "", nil
   252  }
   253  
   254  func (c *fileConfig) makeConfigForHost(hostname string) *HostConfig {
   255  	hostRoot := &yaml.Node{Kind: yaml.MappingNode}
   256  	hostCfg := &HostConfig{
   257  		Host:      hostname,
   258  		ConfigMap: ConfigMap{Root: hostRoot},
   259  	}
   260  
   261  	var notFound *NotFoundError
   262  	hostsEntry, err := c.FindEntry("hosts")
   263  	if errors.As(err, &notFound) {
   264  		hostsEntry.KeyNode = &yaml.Node{
   265  			Kind:  yaml.ScalarNode,
   266  			Value: "hosts",
   267  		}
   268  		hostsEntry.ValueNode = &yaml.Node{Kind: yaml.MappingNode}
   269  		root := c.Root()
   270  		root.Content = append(root.Content, hostsEntry.KeyNode, hostsEntry.ValueNode)
   271  	} else if err != nil {
   272  		panic(err)
   273  	}
   274  
   275  	hostsEntry.ValueNode.Content = append(hostsEntry.ValueNode.Content,
   276  		&yaml.Node{
   277  			Kind:  yaml.ScalarNode,
   278  			Value: hostname,
   279  		}, hostRoot)
   280  
   281  	return hostCfg
   282  }
   283  
   284  func (c *fileConfig) parseHosts(hostsEntry *yaml.Node) ([]*HostConfig, error) {
   285  	hostConfigs := []*HostConfig{}
   286  
   287  	for i := 0; i < len(hostsEntry.Content)-1; i = i + 2 {
   288  		hostname := hostsEntry.Content[i].Value
   289  		hostRoot := hostsEntry.Content[i+1]
   290  		hostConfig := HostConfig{
   291  			ConfigMap: ConfigMap{Root: hostRoot},
   292  			Host:      hostname,
   293  		}
   294  		hostConfigs = append(hostConfigs, &hostConfig)
   295  	}
   296  
   297  	if len(hostConfigs) == 0 {
   298  		return nil, errors.New("could not find any host configurations")
   299  	}
   300  
   301  	return hostConfigs, nil
   302  }
   303  
   304  func yamlNormalize(b []byte) []byte {
   305  	if bytes.Equal(b, []byte("{}\n")) {
   306  		return []byte{}
   307  	}
   308  	return b
   309  }
   310  
   311  func defaultFor(key string) string {
   312  	for _, co := range configOptions {
   313  		if co.Key == key {
   314  			return co.DefaultValue
   315  		}
   316  	}
   317  	return ""
   318  }