diff --git a/internal/config/config.go b/internal/config/config.go index cf4d761c1..0b5adc3dd 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -42,7 +42,7 @@ type cfg struct { cfg *ghConfig.Config } -func (c *cfg) Get(hostname, key string) o.Option[string] { +func (c *cfg) get(hostname, key string) o.Option[string] { if hostname != "" { val, err := c.cfg.Get([]string{hostsKey, hostname, key}) if err == nil { @@ -59,7 +59,7 @@ func (c *cfg) Get(hostname, key string) o.Option[string] { } func (c *cfg) GetOrDefault(hostname, key string) o.Option[string] { - if val := c.Get(hostname, key); val.IsSome() { + if val := c.get(hostname, key); val.IsSome() { return val } @@ -126,7 +126,7 @@ func (c *cfg) Prompt(hostname string) string { } func (c *cfg) Version() o.Option[string] { - return c.Get("", versionKey) + return c.get("", versionKey) } func (c *cfg) Migrate(m gh.Migration) error { @@ -513,6 +513,7 @@ type ConfigOption struct { Description string DefaultValue string AllowedValues []string + CurrentValue func(c gh.Config, hostname string) string } func ConfigOptions() []ConfigOption { @@ -522,32 +523,50 @@ func ConfigOptions() []ConfigOption { Description: "the protocol to use for git clone and push operations", DefaultValue: "https", AllowedValues: []string{"https", "ssh"}, + CurrentValue: func(c gh.Config, hostname string) string { + return c.GitProtocol(hostname) + }, }, { Key: editorKey, Description: "the text editor program to use for authoring text", DefaultValue: "", + CurrentValue: func(c gh.Config, hostname string) string { + return c.Editor(hostname) + }, }, { Key: promptKey, Description: "toggle interactive prompting in the terminal", DefaultValue: "enabled", AllowedValues: []string{"enabled", "disabled"}, + CurrentValue: func(c gh.Config, hostname string) string { + return c.Prompt(hostname) + }, }, { Key: pagerKey, Description: "the terminal pager program to send standard output to", DefaultValue: "", + CurrentValue: func(c gh.Config, hostname string) string { + return c.Pager(hostname) + }, }, { Key: httpUnixSocketKey, Description: "the path to a Unix socket through which to make an HTTP connection", DefaultValue: "", + CurrentValue: func(c gh.Config, hostname string) string { + return c.HTTPUnixSocket(hostname) + }, }, { Key: browserKey, Description: "the web browser to use for opening URLs", DefaultValue: "", + CurrentValue: func(c gh.Config, hostname string) string { + return c.Browser(hostname) + }, }, } } diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 3d13fb7d7..fdad21965 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -32,67 +32,6 @@ func TestNewConfigProvidesFallback(t *testing.T) { requireKeyWithValue(t, spiedCfg, []string{browserKey}, "") } -func TestGetNonExistentKey(t *testing.T) { - // Given we have no top level configuration - cfg := newTestConfig() - - // When we get a key that has no value - optionalVal := cfg.Get("", "non-existent-key") - - // Then it returns a None variant - require.True(t, optionalVal.IsNone(), "expected there to be no value") -} - -func TestGetNonExistentHostSpecificKey(t *testing.T) { - // Given have no top level configuration - cfg := newTestConfig() - - // When we get a key for a host that has no value - optionalVal := cfg.Get("non-existent-host", "non-existent-key") - - // Then it returns a None variant - require.True(t, optionalVal.IsNone(), "expected there to be no value") -} - -func TestGetExistingTopLevelKey(t *testing.T) { - // Given have a top level config entry - cfg := newTestConfig() - cfg.Set("", "top-level-key", "top-level-value") - - // When we get that key - optionalVal := cfg.Get("non-existent-host", "top-level-key") - - // Then it returns a Some variant containing the correct value - require.True(t, optionalVal.IsSome(), "expected there to be a value") - require.Equal(t, "top-level-value", optionalVal.Unwrap()) -} - -func TestGetExistingHostSpecificKey(t *testing.T) { - // Given have a host specific config entry - cfg := newTestConfig() - cfg.Set("github.com", "host-specific-key", "host-specific-value") - - // When we get that key - optionalVal := cfg.Get("github.com", "host-specific-key") - - // Then it returns a Some variant containing the correct value - require.True(t, optionalVal.IsSome(), "expected there to be a value") - require.Equal(t, "host-specific-value", optionalVal.Unwrap()) -} - -func TestGetHostnameSpecificKeyFallsBackToTopLevel(t *testing.T) { - // Given have a top level config entry - cfg := newTestConfig() - cfg.Set("", "key", "value") - - // When we get that key on a specific host - optionalVal := cfg.Get("github.com", "key") - - // Then it returns a Some variant containing the correct value by falling back to the top level config - require.True(t, optionalVal.IsSome(), "expected there to be a value") - require.Equal(t, "value", optionalVal.Unwrap()) -} - func TestGetOrDefaultApplicationDefaults(t *testing.T) { tests := []struct { key string @@ -121,29 +60,65 @@ func TestGetOrDefaultApplicationDefaults(t *testing.T) { } } -func TestGetOrDefaultExistingKey(t *testing.T) { - // Given have a top level config entry +func TestGetOrDefaultNonExistentKey(t *testing.T) { + // Given we have no top level configuration cfg := newTestConfig() - cfg.Set("", gitProtocolKey, "ssh") - // When we get that key - optionalVal := cfg.GetOrDefault("", gitProtocolKey) + // When we get a key that has no value + optionalVal := cfg.GetOrDefault("", "non-existent-key") - // Then it returns successfully with the correct value, and doesn't fall back - // to the default - require.True(t, optionalVal.IsSome(), "expected there to be a value") - require.Equal(t, "ssh", optionalVal.Unwrap()) + // Then it returns a None variant + require.True(t, optionalVal.IsNone(), "expected there to be no value") } -func TestGetOrDefaultNotFoundAndNoDefault(t *testing.T) { - // Given have no configuration +func TestGetOrDefaultNonExistentHostSpecificKey(t *testing.T) { + // Given have no top level configuration cfg := newTestConfig() - // When we get a non-existent-key that has no default - optionalEntry := cfg.GetOrDefault("", "non-existent-key") + // When we get a key for a host that has no value + optionalVal := cfg.GetOrDefault("non-existent-host", "non-existent-key") - // Then it returns with no entry - require.False(t, optionalEntry.IsSome(), "expected the config to not contain a value") + // Then it returns a None variant + require.True(t, optionalVal.IsNone(), "expected there to be no value") +} + +func TestGetOrDefaultExistingTopLevelKey(t *testing.T) { + // Given have a top level config entry + cfg := newTestConfig() + cfg.Set("", "top-level-key", "top-level-value") + + // When we get that key + optionalVal := cfg.GetOrDefault("non-existent-host", "top-level-key") + + // Then it returns a Some variant containing the correct value + require.True(t, optionalVal.IsSome(), "expected there to be a value") + require.Equal(t, "top-level-value", optionalVal.Unwrap()) +} + +func TestGetOrDefaultExistingHostSpecificKey(t *testing.T) { + // Given have a host specific config entry + cfg := newTestConfig() + cfg.Set("github.com", "host-specific-key", "host-specific-value") + + // When we get that key + optionalVal := cfg.GetOrDefault("github.com", "host-specific-key") + + // Then it returns a Some variant containing the correct value + require.True(t, optionalVal.IsSome(), "expected there to be a value") + require.Equal(t, "host-specific-value", optionalVal.Unwrap()) +} + +func TestGetOrDefaultHostnameSpecificKeyFallsBackToTopLevel(t *testing.T) { + // Given have a top level config entry + cfg := newTestConfig() + cfg.Set("", "key", "value") + + // When we get that key on a specific host + optionalVal := cfg.GetOrDefault("github.com", "key") + + // Then it returns a Some variant containing the correct value by falling back to the top level config + require.True(t, optionalVal.IsSome(), "expected there to be a value") + require.Equal(t, "value", optionalVal.Unwrap()) } func TestFallbackConfig(t *testing.T) { diff --git a/pkg/cmd/config/list/list.go b/pkg/cmd/config/list/list.go index 7e4efc06c..d7e9751e0 100644 --- a/pkg/cmd/config/list/list.go +++ b/pkg/cmd/config/list/list.go @@ -57,13 +57,8 @@ func listRun(opts *ListOptions) error { configOptions := config.ConfigOptions() - for _, key := range configOptions { - optionalValue := cfg.GetOrDefault(host, key.Key) - if optionalValue.IsNone() { - return fmt.Errorf("invalid key: %s", key.Key) - } - - fmt.Fprintf(opts.IO.Out, "%s=%s\n", key.Key, optionalValue.Unwrap()) + for _, option := range configOptions { + fmt.Fprintf(opts.IO.Out, "%s=%s\n", option.Key, option.CurrentValue(cfg, host)) } return nil diff --git a/pkg/cmd/config/list/list_test.go b/pkg/cmd/config/list/list_test.go index 32088b324..b6a9d0fca 100644 --- a/pkg/cmd/config/list/list_test.go +++ b/pkg/cmd/config/list/list_test.go @@ -10,6 +10,7 @@ import ( "github.com/cli/cli/v2/pkg/iostreams" "github.com/google/shlex" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestNewCmdConfigList(t *testing.T) { @@ -108,9 +109,8 @@ browser=brave t.Run(tt.name, func(t *testing.T) { err := listRun(tt.input) - assert.NoError(t, err) - assert.Equal(t, tt.stdout, stdout.String()) - //assert.Equal(t, tt.stderr, stderr.String()) + require.NoError(t, err) + require.Equal(t, tt.stdout, stdout.String()) }) } } diff --git a/pkg/option/option.go b/pkg/option/option.go index 6120994ba..30fdc1412 100644 --- a/pkg/option/option.go +++ b/pkg/option/option.go @@ -1,3 +1,24 @@ +// MIT License + +// Copyright (c) 2022 Tom Godkin + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. package o import "fmt" @@ -76,15 +97,6 @@ func (o Option[T]) IsSome() bool { return o.present } -// IsSome returns true if the [Option] is a [Some] variant and the value inside of it equals the provided value. -// func (o Option[T]) Is(t T) bool { -// return o.present && o.value == t -// } - -func (o Option[T]) IsSomeAnd(f func(T) bool) bool { - return o.present && f(o.value) -} - // IsNone returns true if the [Option] is a [None] variant. func (o Option[T]) IsNone() bool { return !o.present @@ -105,11 +117,3 @@ func (o Option[T]) Expect(message string) T { panic(message) } - -func Map[T any, U any](f func(T) U, o Option[T]) Option[U] { - if o.present { - return Some(f(o.value)) - } - - return None[U]() -} diff --git a/pkg/option/option_test.go b/pkg/option/option_test.go new file mode 100644 index 000000000..5e5068124 --- /dev/null +++ b/pkg/option/option_test.go @@ -0,0 +1,181 @@ +// MIT License + +// Copyright (c) 2022 Tom Godkin + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +package o_test + +import ( + "fmt" + "testing" + + o "github.com/cli/cli/v2/pkg/option" + "github.com/stretchr/testify/require" +) + +func ExampleOption_Unwrap() { + fmt.Println(o.Some(4).Unwrap()) + // Output: 4 +} + +func ExampleOption_UnwrapOr() { + fmt.Println(o.Some(4).UnwrapOr(3)) + fmt.Println(o.None[int]().UnwrapOr(3)) + // Output: + // 4 + // 3 +} + +func ExampleOption_UnwrapOrElse() { + fmt.Println(o.Some(4).UnwrapOrElse(func() int { + return 3 + })) + + fmt.Println(o.None[int]().UnwrapOrElse(func() int { + return 3 + })) + + // Output: + // 4 + // 3 +} + +func ExampleOption_UnwrapOrZero() { + fmt.Println(o.Some(4).UnwrapOrZero()) + fmt.Println(o.None[int]().UnwrapOrZero()) + + // Output + // 4 + // 0 +} + +func ExampleOption_IsSome() { + fmt.Println(o.Some(4).IsSome()) + fmt.Println(o.None[int]().IsSome()) + + // Output: + // true + // false +} + +func ExampleOption_IsNone() { + fmt.Println(o.Some(4).IsNone()) + fmt.Println(o.None[int]().IsNone()) + + // Output: + // false + // true +} + +func ExampleOption_Value() { + value, ok := o.Some(4).Value() + fmt.Println(value) + fmt.Println(ok) + + // Output: + // 4 + // true +} + +func ExampleOption_Expect() { + fmt.Println(o.Some(4).Expect("oops")) + + // Output: 4 +} + +func TestSomeStringer(t *testing.T) { + require.Equal(t, fmt.Sprintf("%s", o.Some("foo")), "Some(foo)") //nolint:gosimple + require.Equal(t, fmt.Sprintf("%s", o.Some(42)), "Some(42)") //nolint:gosimple +} + +func TestNoneStringer(t *testing.T) { + require.Equal(t, fmt.Sprintf("%s", o.None[string]()), "None") //nolint:gosimple +} + +func TestSomeUnwrap(t *testing.T) { + require.Equal(t, o.Some(42).Unwrap(), 42) +} + +func TestNoneUnwrap(t *testing.T) { + defer func() { + require.Equal(t, fmt.Sprint(recover()), "called `Option.Unwrap()` on a `None` value") + }() + + o.None[string]().Unwrap() + t.Error("did not panic") +} + +func TestSomeUnwrapOr(t *testing.T) { + require.Equal(t, o.Some(42).UnwrapOr(3), 42) +} + +func TestNoneUnwrapOr(t *testing.T) { + require.Equal(t, o.None[int]().UnwrapOr(3), 3) +} + +func TestSomeUnwrapOrElse(t *testing.T) { + require.Equal(t, o.Some(42).UnwrapOrElse(func() int { return 41 }), 42) +} + +func TestNoneUnwrapOrElse(t *testing.T) { + require.Equal(t, o.None[int]().UnwrapOrElse(func() int { return 41 }), 41) +} + +func TestSomeUnwrapOrZero(t *testing.T) { + require.Equal(t, o.Some(42).UnwrapOrZero(), 42) +} + +func TestNoneUnwrapOrZero(t *testing.T) { + require.Equal(t, o.None[int]().UnwrapOrZero(), 0) +} + +func TestIsSome(t *testing.T) { + require.True(t, o.Some(42).IsSome()) + require.False(t, o.None[int]().IsSome()) +} + +func TestIsNone(t *testing.T) { + require.False(t, o.Some(42).IsNone()) + require.True(t, o.None[int]().IsNone()) +} + +func TestSomeValue(t *testing.T) { + value, ok := o.Some(42).Value() + require.Equal(t, value, 42) + require.True(t, ok) +} + +func TestNoneValue(t *testing.T) { + value, ok := o.None[int]().Value() + require.Equal(t, value, 0) + require.False(t, ok) +} + +func TestSomeExpect(t *testing.T) { + require.Equal(t, o.Some(42).Expect("oops"), 42) +} + +func TestNoneExpect(t *testing.T) { + defer func() { + require.Equal(t, fmt.Sprint(recover()), "oops") + }() + + o.None[int]().Expect("oops") + t.Error("did not panic") +}