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, ¬Found) { 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, ¬Found) { 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, ¬Found) { 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, ¬Found) { 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, ¬Found) { 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 }