Conversation
|
Have you looked at https://github.com/spf13/viper? It has some nice integration with |
|
@thefotios We did look at |
mislav
left a comment
There was a problem hiding this comment.
Thanks for working on this!
command/config.go
Outdated
|
|
||
| switch len(args) { | ||
| case 0: | ||
| cfgInteractive(cmd) |
There was a problem hiding this comment.
I feel that some sort of interactive mode might be jumping the gun at this point, since we primarily want to allow people to set/change a configuration option.
There was a problem hiding this comment.
agreed, i was just sketching. i actually meant to take all the command stuff out before even asking you to look at it, sorry
context/config_file.go
Outdated
| return hc, nil | ||
| } | ||
| } | ||
| return nil, fmt.Errorf("could not find config entry for \"%s\"", hostname) |
There was a problem hiding this comment.
Nit: you can easily display a quoted string value by using %q instead of "%s"
context/config_file.go
Outdated
| } | ||
| hostConfig := HostConfig{} | ||
| hostConfig.Host = v.Value | ||
| authsRoot := topLevelKeys[i+1].Content[j+1] |
There was a problem hiding this comment.
Is there some sort of guard that this array access won't panic if it's out of bounds?
There was a problem hiding this comment.
nope -- i still have to add error handling / bounds checking
context/config_file.go
Outdated
| config.Hosts = append(config.Hosts, &hostConfig) | ||
| } | ||
| return &entries[0], nil | ||
| case "protocol": |
There was a problem hiding this comment.
I wonder if we could get away with simply storing/parsing any key-value pair and letting another mechanism query the values and decide what to do with them?
I feel as if this should be a lower-level config mechanism which doesn't necessarily understand nor care about the meaning of any configuration options, apart from parsing per-host config, and then have another configuration layer that wraps the storage layer and understands what protocol, ssh, editor and such means.
I hope that this doesn't feel like I'm suggesting over-engineering this; I do believe that a simple storage later that doesn't care about the semantics of individual keys would be the easiest to build and test!
There was a problem hiding this comment.
Could we be more specific with what "protocol" refers to in configuration options, for example by renaming the key to something like clone_protocol or git_protocol?
The reason is that for GitHub enterprise, we might need to support GHE API access over the http: protocol (some intranet installs don't have GHE accessible under SSL and that's okay I guess since it's intranet). To support that case, there might be an api_protocol or similarly named key under host-specific configuration, and therefore it might be generally a good idea to avoid vagueness when using the world "protocol" unqualified
Let me know if this makes sense; just thinking ahead here. mislav/hub#2128 (comment)
There was a problem hiding this comment.
I wonder if we could get away with simply storing/parsing any key-value pair and letting another mechanism query the values and decide what to do with them?
I was considering an approach like this:
- continue store the root
*yaml.Nodein theConfigstruct. We need it to properly write the config back out in case it's changed via agh config...invocation. - add methods to the
Configstruct that lazily parse a desired config key instead of trying to parse it all up front. Each method is responsible for traversing the root and finding the desired node and parsing it. theHostsmethod would need to be involved, but the simple k/v settings can share a helper. - support a mock
Configin tests (like we do withContext) whose various methods have predictable responses - unit test the yaml parsing on its own (like we already do)
This way as we add new supported config values we add new methods to Config.
This approach means we can progressively parse the yaml and not fail because of an unknown key or malformed key that isn't relevant to the current invocation of gh.
I'm working on spiking that now; hopefully you'll be able to look over it when you start your day.
|
Hey @vilmibm when you're ready, could you share a gif of this in action? 🙇♀ |
|
@ampinsk which part? for this PR i only plan on including reading and migrating to the new config format (nesting what we have now under a |
|
Oo my bad then! Carry on. 🙈 At some point would like to see the commands and the file itself but no rush! |
context/config_file.go
Outdated
| newConfigData := map[string]map[string]map[string]string{} | ||
| newConfigData["hosts"] = map[string]map[string]string{} |
There was a problem hiding this comment.
protip: you could alias some of these types so it looks more expressive. example:
type ConfigEntry map[string]string
type ConfigMap map[string]ConfigEntry
newConfigData := make(ConfigMap)
context/config_type.go
Outdated
| configForHost(string) (*HostConfig, error) | ||
| parseHosts(*yaml.Node) ([]*HostConfig, error) |
There was a problem hiding this comment.
It's unusual to me to see un-exported methods in an interface. But if it helps the implemenation then I see no harm! Alternatively, you could consider just having package-level functions configForHost() and parseHosts() that accept the config object as parameter, but then you'd have to have a switch-type statement and that could become messy.
There was a problem hiding this comment.
it's leftover from when LegacyConfig was a thing. That is no longer a thing, so I just removed those from the interface.
- adds config get and config set commands - supports arbitrary k/v strings set at top and host level - supports writing an updated config, preserving comments - supports mostly lazy evaluation of yaml
mislav
left a comment
There was a problem hiding this comment.
Looks and works great for me in testing!
Documentation note: I think that we should omit documenting --host until we release GHE support that would take advantage of it. More thoughts in comments.
Edge case note: I edited my config file and removed the user auth setting (leaving in the token) and ran gh issue status. Instead of erroring out with the message that user could not be read, it proceeded to query the API with the username being set to an empty string (which failed on the server).
command/config.go
Outdated
| var configCmd = &cobra.Command{ | ||
| Use: "config", | ||
| Short: "Set and get gh settings", | ||
| Long: `Get and set key/value strings. They can be optionally scoped to a particular host. |
There was a problem hiding this comment.
Since we don't document --host in this help text but in subcommands, I would suggest that we remove the “They can be optionally scoped” sentence and let people discover that option in get/set subcommand docs. Otherwise we give them a small hint of a feature but we don't give them any more information to understand how the feature is used.
command/config.go
Outdated
| } | ||
|
|
||
| var configGetCmd = &cobra.Command{ | ||
| Use: "get", |
There was a problem hiding this comment.
We could expand the usage synopsis to be get <key> so the parameters are easier to understand at a glance
command/config.go
Outdated
| Examples: | ||
| gh config get git_protocol | ||
| https | ||
| gh config get --host example.com |
There was a problem hiding this comment.
Since we plan to ship this in the next release, but don't plan GHE support in the same release, I wonder if it makes sense to hide all reference to --host in the docs. We could mark the flag as "hidden".
The rationale would be that it makes no sense to advertise a feature that currently has no use-case. Scoping by host only makes sense once we add multi-host support.
There was a problem hiding this comment.
makes total sense. that occurred to me in the last hour of getting this PR ready. I was so fixated on making it working I forgot that it wouldn't even be supported in the first release of gh config 🤦♀️
command/config.go
Outdated
| } | ||
|
|
||
| var configSetCmd = &cobra.Command{ | ||
| Use: "set", |
There was a problem hiding this comment.
We could expand the usage synopsis to be set <key> <value>
| func (c *blankContext) Config() (Config, error) { | ||
| config, err := ParseConfig("boom.txt") | ||
| if err != nil { | ||
| panic(fmt.Sprintf("failed to parse config during tests. did you remember to stub? error: %s", err)) |
There was a problem hiding this comment.
Nice test failure for error case!
context/testing.go
Outdated
| } | ||
| } | ||
|
|
||
| func eq(t *testing.T, got interface{}, expected interface{}) { |
There was a problem hiding this comment.
Instead of having to import the testing package in a file that doesn't end in _test.go, could we describe t as simply conforming to an interface with only the Helper() method? Since this is the only method of t that we are invoking?
Or better yet, we can move the definition of eq into a _test.go file
command/config.go
Outdated
|
|
||
| err = cfg.Set(hostname, key, value) | ||
| if err != nil { | ||
| return fmt.Errorf("failed to set %q to %q: %s", key, value, err) |
There was a problem hiding this comment.
Nit: to legitimately wrap an error go 1.13-style, could we turn the %s into %w? I think that the error output will be the same
There was a problem hiding this comment.
🤦♀️ I wasn't sure what the %w was until now and had avoided it out of principle. will switch to it now that I know better.
command/config.go
Outdated
|
|
||
| err = cfg.Write() | ||
| if err != nil { | ||
| return fmt.Errorf("failed to write config to disk: %s", err) |
command/root.go
Outdated
| opts = append(opts, api.AddHeader("User-Agent", fmt.Sprintf("GitHub CLI %s", Version))) | ||
| if c, err := context.ParseDefaultConfig(); err == nil { | ||
| opts = append(opts, api.AddHeader("Authorization", fmt.Sprintf("token %s", c.Token))) | ||
| if token, err := c.Get(defaultHostname, "oauth_token"); err == nil { |
There was a problem hiding this comment.
In which case would there be an error here, and should we keep tolerating it?
There was a problem hiding this comment.
it would have to be something messed up in the config and we shouldn't tolerate it; changed to fail
| const defaultGitProtocol = "https" | ||
|
|
||
| // This interface describes interacting with some persistent configuration for gh. | ||
| type Config interface { |
There was a problem hiding this comment.
Do you think it would be worth extracting, at the end, any of the config stuff from context to a new package under internal?
There was a problem hiding this comment.
I do think that would be worthwhile. Having it context felt wrong and I wanted a config package; was just getting worried about how long this was taking. I'll see if I can do it quickly today.
|
@mislav caught up on all the feedback and also added stricter handling around blank user/oauth_token values. |
|
just realized i screwed up default handling and need to fix it |
mislav
left a comment
There was a problem hiding this comment.
Thank you for the update!
command/config.go
Outdated
| if err != nil { | ||
| panic("this should not happen") |
There was a problem hiding this comment.
Nit: I think it's perfectly fine to ignore the error here by assigning to _ instead of panicking
command/root.go
Outdated
| if token == "" || err != nil { | ||
| return nil, err |
There was a problem hiding this comment.
If token was an empty string but there was no error object, this will return nil, nil. If we want to return an explicit error for that case, we should describe it to ensure there is always an error object:
if err != nil {
return nil, err
}
if token == "" {
return nil, fmt.Errorf("...")
}There was a problem hiding this comment.
good catch; i was going too fast
|
Nice work. I tested Some things to consider for future iterations:
Tested with d4dd4a6, OSX. (Will verify in Windows soon.) |
This PR implements
gh config. Originally I was going to do several smaller PRs but things ended up being too conceptually coupled for me for that to make sense.What this PR does:
gh config getandgh config seteditorandgit_protocolvalues--hostWhat this PR doesn't do:
editorandgit_protocolsettingsgh configtop level command; it just adds get/setTODOTODIDhostskeygh config getgh config setfixes #622