Skip to content

gh config#728

Merged
vilmibm merged 14 commits intomasterfrom
gh-config
Apr 21, 2020
Merged

gh config#728
vilmibm merged 14 commits intomasterfrom
gh-config

Conversation

@vilmibm
Copy link
Copy Markdown
Contributor

@vilmibm vilmibm commented Apr 1, 2020

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:

  • Support a new config format (including migrating to it)
  • Support gh config get and gh config set
  • Support reading/writing editor and git_protocol values
  • Support host-level overrides with --host

What this PR doesn't do:

  • Actually respect editor and git_protocol settings
  • Lay out a nice default config with comments
  • Add a gh config top level command; it just adds get/set

image

TODO TODID

  • dynamically parsing config.yml
  • supporting a top level hosts key
  • using the new config throughout the code
  • actually doing error handling and preventing segfaults on the parsing code
  • updating config file tests
  • migration code to put hosts under the new key
  • make parsing code less rigid and less eager
  • adding new tests
    • make testing configs possible (stubs for read/write operations)
    • get tests
    • set tests
    • migrate tests
    • defaults tests
  • supporting both old and new formats until a write is requested
    • deduping some of that code
  • gh config get
  • gh config set
    • migration on write
  • host level overrides
  • go back to migrate-on-read
  • rely on cobra validators for arg arity
  • add usage

fixes #622

@thefotios
Copy link
Copy Markdown
Contributor

Have you looked at https://github.com/spf13/viper? It has some nice integration with cobra and it also let's you marshal/unmarshal to interfaces.

@vilmibm vilmibm changed the title [WIP] [EXTREME WIP] new config format + parsing [WIP] new config format + parsing Apr 6, 2020
@vilmibm
Copy link
Copy Markdown
Contributor Author

vilmibm commented Apr 6, 2020

@thefotios We did look at viper. It's impressive but a lot more than we currently need; more importantly, my understanding is that it would not preserve any comments in the config file. We'd like to have a very human-friendly, documented config with preserved comments.

Copy link
Copy Markdown
Contributor

@mislav mislav left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for working on this!


switch len(args) {
case 0:
cfgInteractive(cmd)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agreed, i was just sketching. i actually meant to take all the command stuff out before even asking you to look at it, sorry

return hc, nil
}
}
return nil, fmt.Errorf("could not find config entry for \"%s\"", hostname)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: you can easily display a quoted string value by using %q instead of "%s"

}
hostConfig := HostConfig{}
hostConfig.Host = v.Value
authsRoot := topLevelKeys[i+1].Content[j+1]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there some sort of guard that this array access won't panic if it's out of bounds?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nope -- i still have to add error handling / bounds checking

config.Hosts = append(config.Hosts, &hostConfig)
}
return &entries[0], nil
case "protocol":
Copy link
Copy Markdown
Contributor

@mislav mislav Apr 7, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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!

Copy link
Copy Markdown
Contributor

@mislav mislav Apr 7, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Copy Markdown
Contributor Author

@vilmibm vilmibm Apr 8, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.Node in the Config struct. We need it to properly write the config back out in case it's changed via a gh config... invocation.
  • add methods to the Config struct 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. the Hosts method would need to be involved, but the simple k/v settings can share a helper.
  • support a mock Config in tests (like we do with Context) 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.

@ampinsk
Copy link
Copy Markdown

ampinsk commented Apr 7, 2020

Hey @vilmibm when you're ready, could you share a gif of this in action? 🙇‍♀

@vilmibm
Copy link
Copy Markdown
Contributor Author

vilmibm commented Apr 7, 2020

@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 hosts: key); no new commands.

@ampinsk
Copy link
Copy Markdown

ampinsk commented Apr 7, 2020

Oo my bad then! Carry on. 🙈

At some point would like to see the commands and the file itself but no rush!

@vilmibm vilmibm changed the title [WIP] new config format + parsing gh config Apr 10, 2020
Copy link
Copy Markdown
Contributor

@mislav mislav left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good so far! Thank you for the hard work

Comment on lines +73 to +93
newConfigData := map[string]map[string]map[string]string{}
newConfigData["hosts"] = map[string]map[string]string{}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Comment on lines +21 to +20
configForHost(string) (*HostConfig, error)
parseHosts(*yaml.Node) ([]*HostConfig, error)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

@vilmibm vilmibm Apr 17, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
@vilmibm vilmibm marked this pull request as ready for review April 17, 2020 20:19
Copy link
Copy Markdown
Contributor

@mislav mislav left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

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.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

}

var configGetCmd = &cobra.Command{
Use: "get",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could expand the usage synopsis to be get <key> so the parameters are easier to understand at a glance

Examples:
gh config get git_protocol
https
gh config get --host example.com
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 🤦‍♀️

}

var configSetCmd = &cobra.Command{
Use: "set",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice test failure for error case!

}
}

func eq(t *testing.T, got interface{}, expected interface{}) {
Copy link
Copy Markdown
Contributor

@mislav mislav Apr 20, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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


err = cfg.Set(hostname, key, value)
if err != nil {
return fmt.Errorf("failed to set %q to %q: %s", key, value, err)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤦‍♀️ 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.


err = cfg.Write()
if err != nil {
return fmt.Errorf("failed to write config to disk: %s", err)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto %w

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 {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In which case would there be an error here, and should we keep tolerating it?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think it would be worth extracting, at the end, any of the config stuff from context to a new package under internal?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@vilmibm
Copy link
Copy Markdown
Contributor Author

vilmibm commented Apr 20, 2020

@mislav caught up on all the feedback and also added stricter handling around blank user/oauth_token values.

@vilmibm vilmibm requested a review from mislav April 20, 2020 19:00
@vilmibm
Copy link
Copy Markdown
Contributor Author

vilmibm commented Apr 20, 2020

just realized i screwed up default handling and need to fix it

Copy link
Copy Markdown
Contributor

@mislav mislav left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the update!

Comment on lines +19 to +20
if err != nil {
panic("this should not happen")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: I think it's perfectly fine to ignore the error here by assigning to _ instead of panicking

command/root.go Outdated
Comment on lines +120 to +121
if token == "" || err != nil {
return nil, err
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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("...")
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good catch; i was going too fast

Copy link
Copy Markdown
Contributor

@mislav mislav left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:shipit: Great work!

@vilmibm vilmibm merged commit 80d7513 into master Apr 21, 2020
@tierninho tierninho self-requested a review April 21, 2020 21:42
@tierninho
Copy link
Copy Markdown
Contributor

Nice work. I tested get and set for both the editor and git_protocol settings and they both work as expected. Also, any manual changes to the configs are respected by the commands.

Some things to consider for future iterations:

  • Allowing the user to revert to default settings.
  • Validation on the inputs as anything can be entered

Tested with d4dd4a6, OSX. (Will verify in Windows soon.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Enable configuration in gh (gh config)

5 participants