diff --git a/internal/config/config.go b/internal/config/config.go index 3a6cdf4d7..a671f1521 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -58,16 +58,22 @@ func (c *cfg) get(hostname, key string) o.Option[string] { return o.None[string]() } -func (c *cfg) GetOrDefault(hostname, key string) o.Option[string] { +func (c *cfg) GetOrDefault(hostname, key string) o.Option[gh.ConfigEntry] { if val := c.get(hostname, key); val.IsSome() { - return val + return o.Map(val, toConfigEntry(gh.ConfigUserProvided)) } if defaultVal := defaultFor(key); defaultVal.IsSome() { - return defaultVal + return o.Map(defaultVal, toConfigEntry(gh.ConfigDefaultProvided)) } - return o.None[string]() + return o.None[gh.ConfigEntry]() +} + +func toConfigEntry(source gh.ConfigSource) func(val string) gh.ConfigEntry { + return func(val string) gh.ConfigEntry { + return gh.ConfigEntry{Value: val, Source: source} + } } func (c *cfg) Set(hostname, key, value string) { @@ -95,32 +101,32 @@ func (c *cfg) Authentication() gh.AuthConfig { return &AuthConfig{cfg: c.cfg} } -func (c *cfg) Browser(hostname string) string { +func (c *cfg) Browser(hostname string) gh.ConfigEntry { // Intentionally panic as this is a programmer error return c.GetOrDefault(hostname, browserKey).Unwrap() } -func (c *cfg) Editor(hostname string) string { +func (c *cfg) Editor(hostname string) gh.ConfigEntry { // Intentionally panic as this is a programmer error return c.GetOrDefault(hostname, editorKey).Unwrap() } -func (c *cfg) GitProtocol(hostname string) string { +func (c *cfg) GitProtocol(hostname string) gh.ConfigEntry { // Intentionally panic as this is a programmer error return c.GetOrDefault(hostname, gitProtocolKey).Unwrap() } -func (c *cfg) HTTPUnixSocket(hostname string) string { +func (c *cfg) HTTPUnixSocket(hostname string) gh.ConfigEntry { // Intentionally panic as this is a programmer error return c.GetOrDefault(hostname, httpUnixSocketKey).Unwrap() } -func (c *cfg) Pager(hostname string) string { +func (c *cfg) Pager(hostname string) gh.ConfigEntry { // Intentionally panic as this is a programmer error return c.GetOrDefault(hostname, pagerKey).Unwrap() } -func (c *cfg) Prompt(hostname string) string { +func (c *cfg) Prompt(hostname string) gh.ConfigEntry { // Intentionally panic as this is a programmer error return c.GetOrDefault(hostname, promptKey).Unwrap() } @@ -523,7 +529,7 @@ var Options = []ConfigOption{ DefaultValue: "https", AllowedValues: []string{"https", "ssh"}, CurrentValue: func(c gh.Config, hostname string) string { - return c.GitProtocol(hostname) + return c.GitProtocol(hostname).Value }, }, { @@ -531,7 +537,7 @@ var Options = []ConfigOption{ Description: "the text editor program to use for authoring text", DefaultValue: "", CurrentValue: func(c gh.Config, hostname string) string { - return c.Editor(hostname) + return c.Editor(hostname).Value }, }, { @@ -540,7 +546,7 @@ var Options = []ConfigOption{ DefaultValue: "enabled", AllowedValues: []string{"enabled", "disabled"}, CurrentValue: func(c gh.Config, hostname string) string { - return c.Prompt(hostname) + return c.Prompt(hostname).Value }, }, { @@ -548,7 +554,7 @@ var Options = []ConfigOption{ Description: "the terminal pager program to send standard output to", DefaultValue: "", CurrentValue: func(c gh.Config, hostname string) string { - return c.Pager(hostname) + return c.Pager(hostname).Value }, }, { @@ -556,7 +562,7 @@ var Options = []ConfigOption{ 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) + return c.HTTPUnixSocket(hostname).Value }, }, { @@ -564,7 +570,7 @@ var Options = []ConfigOption{ Description: "the web browser to use for opening URLs", DefaultValue: "", CurrentValue: func(c gh.Config, hostname string) string { - return c.Browser(hostname) + return c.Browser(hostname).Value }, }, } diff --git a/internal/config/config_test.go b/internal/config/config_test.go index fdad21965..fef87ddc6 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -1,10 +1,12 @@ package config import ( + "fmt" "testing" "github.com/stretchr/testify/require" + "github.com/cli/cli/v2/internal/gh" ghConfig "github.com/cli/go-gh/v2/pkg/config" ) @@ -51,11 +53,12 @@ func TestGetOrDefaultApplicationDefaults(t *testing.T) { cfg := newTestConfig() // When we get a key that has no value, but has a default - optionalVal := cfg.GetOrDefault("", tt.key) + optionalEntry := cfg.GetOrDefault("", tt.key) // Then there is an entry with the default value, and source set as default - require.True(t, optionalVal.IsSome(), "expected there to be a value") - require.Equal(t, tt.expectedDefault, optionalVal.Unwrap()) + entry := optionalEntry.Expect(fmt.Sprintf("expected there to be a value for %s", tt.key)) + require.Equal(t, tt.expectedDefault, entry.Value) + require.Equal(t, gh.ConfigDefaultProvided, entry.Source) }) } } @@ -65,10 +68,10 @@ func TestGetOrDefaultNonExistentKey(t *testing.T) { cfg := newTestConfig() // When we get a key that has no value - optionalVal := cfg.GetOrDefault("", "non-existent-key") + optionalEntry := cfg.GetOrDefault("", "non-existent-key") // Then it returns a None variant - require.True(t, optionalVal.IsNone(), "expected there to be no value") + require.True(t, optionalEntry.IsNone(), "expected there to be no value") } func TestGetOrDefaultNonExistentHostSpecificKey(t *testing.T) { @@ -76,10 +79,10 @@ func TestGetOrDefaultNonExistentHostSpecificKey(t *testing.T) { cfg := newTestConfig() // When we get a key for a host that has no value - optionalVal := cfg.GetOrDefault("non-existent-host", "non-existent-key") + optionalEntry := cfg.GetOrDefault("non-existent-host", "non-existent-key") // Then it returns a None variant - require.True(t, optionalVal.IsNone(), "expected there to be no value") + require.True(t, optionalEntry.IsNone(), "expected there to be no value") } func TestGetOrDefaultExistingTopLevelKey(t *testing.T) { @@ -88,11 +91,12 @@ func TestGetOrDefaultExistingTopLevelKey(t *testing.T) { cfg.Set("", "top-level-key", "top-level-value") // When we get that key - optionalVal := cfg.GetOrDefault("non-existent-host", "top-level-key") + optionalEntry := 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()) + // Then it returns a Some variant containing the correct value and a source of user + entry := optionalEntry.Expect("expected there to be a value") + require.Equal(t, "top-level-value", entry.Value) + require.Equal(t, gh.ConfigUserProvided, entry.Source) } func TestGetOrDefaultExistingHostSpecificKey(t *testing.T) { @@ -101,11 +105,12 @@ func TestGetOrDefaultExistingHostSpecificKey(t *testing.T) { cfg.Set("github.com", "host-specific-key", "host-specific-value") // When we get that key - optionalVal := cfg.GetOrDefault("github.com", "host-specific-key") + optionalEntry := 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()) + // Then it returns a Some variant containing the correct value and a source of user + entry := optionalEntry.Expect("expected there to be a value") + require.Equal(t, "host-specific-value", entry.Value) + require.Equal(t, gh.ConfigUserProvided, entry.Source) } func TestGetOrDefaultHostnameSpecificKeyFallsBackToTopLevel(t *testing.T) { @@ -114,11 +119,13 @@ func TestGetOrDefaultHostnameSpecificKeyFallsBackToTopLevel(t *testing.T) { cfg.Set("", "key", "value") // When we get that key on a specific host - optionalVal := cfg.GetOrDefault("github.com", "key") + optionalEntry := 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()) + // Then it returns a Some variant containing the correct value by falling back + // to the top level config, with a source of user + entry := optionalEntry.Expect("expected there to be a value") + require.Equal(t, "value", entry.Value) + require.Equal(t, gh.ConfigUserProvided, entry.Source) } func TestFallbackConfig(t *testing.T) { diff --git a/internal/config/stub.go b/internal/config/stub.go index ec088ed07..c89868721 100644 --- a/internal/config/stub.go +++ b/internal/config/stub.go @@ -21,7 +21,7 @@ func NewFromString(cfgStr string) *ghmock.ConfigMock { c := ghConfig.ReadFromString(cfgStr) cfg := cfg{c} mock := &ghmock.ConfigMock{} - mock.GetOrDefaultFunc = func(host, key string) o.Option[string] { + mock.GetOrDefaultFunc = func(host, key string) o.Option[gh.ConfigEntry] { return cfg.GetOrDefault(host, key) } mock.SetFunc = func(host, key, value string) { @@ -52,22 +52,22 @@ func NewFromString(cfgStr string) *ghmock.ConfigMock { }, } } - mock.BrowserFunc = func(hostname string) string { + mock.BrowserFunc = func(hostname string) gh.ConfigEntry { return cfg.Browser(hostname) } - mock.EditorFunc = func(hostname string) string { + mock.EditorFunc = func(hostname string) gh.ConfigEntry { return cfg.Editor(hostname) } - mock.GitProtocolFunc = func(hostname string) string { + mock.GitProtocolFunc = func(hostname string) gh.ConfigEntry { return cfg.GitProtocol(hostname) } - mock.HTTPUnixSocketFunc = func(hostname string) string { + mock.HTTPUnixSocketFunc = func(hostname string) gh.ConfigEntry { return cfg.HTTPUnixSocket(hostname) } - mock.PagerFunc = func(hostname string) string { + mock.PagerFunc = func(hostname string) gh.ConfigEntry { return cfg.Pager(hostname) } - mock.PromptFunc = func(hostname string) string { + mock.PromptFunc = func(hostname string) gh.ConfigEntry { return cfg.Prompt(hostname) } mock.VersionFunc = func() o.Option[string] { diff --git a/internal/gh/gh.go b/internal/gh/gh.go index 5abde3af7..a6db43d66 100644 --- a/internal/gh/gh.go +++ b/internal/gh/gh.go @@ -14,27 +14,39 @@ import ( ghConfig "github.com/cli/go-gh/v2/pkg/config" ) +type ConfigSource string + +const ( + ConfigDefaultProvided ConfigSource = "default" + ConfigUserProvided ConfigSource = "user" +) + +type ConfigEntry struct { + Value string + Source ConfigSource +} + // A Config implements persistent storage and modification of application configuration. // //go:generate moq -rm -pkg ghmock -out mock/config.go . Config type Config interface { // GetOrDefault provides primitive access for fetching configuration values, optionally scoped by host. - GetOrDefault(hostname string, key string) o.Option[string] + GetOrDefault(hostname string, key string) o.Option[ConfigEntry] // Set provides primitive access for setting configuration values, optionally scoped by host. Set(hostname string, key string, value string) // Browser returns the configured browser, optionally scoped by host. - Browser(hostname string) string + Browser(hostname string) ConfigEntry // Editor returns the configured editor, optionally scoped by host. - Editor(hostname string) string + Editor(hostname string) ConfigEntry // GitProtocol returns the configured git protocol, optionally scoped by host. - GitProtocol(hostname string) string + GitProtocol(hostname string) ConfigEntry // HTTPUnixSocket returns the configured HTTP unix socket, optionally scoped by host. - HTTPUnixSocket(hostname string) string + HTTPUnixSocket(hostname string) ConfigEntry // Pager returns the configured Pager, optionally scoped by host. - Pager(hostname string) string + Pager(hostname string) ConfigEntry // Prompt returns the configured prompt, optionally scoped by host. - Prompt(hostname string) string + Prompt(hostname string) ConfigEntry // Aliases provides persistent storage and modification of command aliases. Aliases() AliasConfig diff --git a/internal/gh/mock/config.go b/internal/gh/mock/config.go index 18d9e0cbc..502047240 100644 --- a/internal/gh/mock/config.go +++ b/internal/gh/mock/config.go @@ -25,31 +25,31 @@ var _ gh.Config = &ConfigMock{} // AuthenticationFunc: func() gh.AuthConfig { // panic("mock out the Authentication method") // }, -// BrowserFunc: func(hostname string) string { +// BrowserFunc: func(hostname string) gh.ConfigEntry { // panic("mock out the Browser method") // }, // CacheDirFunc: func() string { // panic("mock out the CacheDir method") // }, -// EditorFunc: func(hostname string) string { +// EditorFunc: func(hostname string) gh.ConfigEntry { // panic("mock out the Editor method") // }, -// GetOrDefaultFunc: func(hostname string, key string) o.Option[string] { +// GetOrDefaultFunc: func(hostname string, key string) o.Option[gh.ConfigEntry] { // panic("mock out the GetOrDefault method") // }, -// GitProtocolFunc: func(hostname string) string { +// GitProtocolFunc: func(hostname string) gh.ConfigEntry { // panic("mock out the GitProtocol method") // }, -// HTTPUnixSocketFunc: func(hostname string) string { +// HTTPUnixSocketFunc: func(hostname string) gh.ConfigEntry { // panic("mock out the HTTPUnixSocket method") // }, // MigrateFunc: func(migration gh.Migration) error { // panic("mock out the Migrate method") // }, -// PagerFunc: func(hostname string) string { +// PagerFunc: func(hostname string) gh.ConfigEntry { // panic("mock out the Pager method") // }, -// PromptFunc: func(hostname string) string { +// PromptFunc: func(hostname string) gh.ConfigEntry { // panic("mock out the Prompt method") // }, // SetFunc: func(hostname string, key string, value string) { @@ -75,31 +75,31 @@ type ConfigMock struct { AuthenticationFunc func() gh.AuthConfig // BrowserFunc mocks the Browser method. - BrowserFunc func(hostname string) string + BrowserFunc func(hostname string) gh.ConfigEntry // CacheDirFunc mocks the CacheDir method. CacheDirFunc func() string // EditorFunc mocks the Editor method. - EditorFunc func(hostname string) string + EditorFunc func(hostname string) gh.ConfigEntry // GetOrDefaultFunc mocks the GetOrDefault method. - GetOrDefaultFunc func(hostname string, key string) o.Option[string] + GetOrDefaultFunc func(hostname string, key string) o.Option[gh.ConfigEntry] // GitProtocolFunc mocks the GitProtocol method. - GitProtocolFunc func(hostname string) string + GitProtocolFunc func(hostname string) gh.ConfigEntry // HTTPUnixSocketFunc mocks the HTTPUnixSocket method. - HTTPUnixSocketFunc func(hostname string) string + HTTPUnixSocketFunc func(hostname string) gh.ConfigEntry // MigrateFunc mocks the Migrate method. MigrateFunc func(migration gh.Migration) error // PagerFunc mocks the Pager method. - PagerFunc func(hostname string) string + PagerFunc func(hostname string) gh.ConfigEntry // PromptFunc mocks the Prompt method. - PromptFunc func(hostname string) string + PromptFunc func(hostname string) gh.ConfigEntry // SetFunc mocks the Set method. SetFunc func(hostname string, key string, value string) @@ -250,7 +250,7 @@ func (mock *ConfigMock) AuthenticationCalls() []struct { } // Browser calls BrowserFunc. -func (mock *ConfigMock) Browser(hostname string) string { +func (mock *ConfigMock) Browser(hostname string) gh.ConfigEntry { if mock.BrowserFunc == nil { panic("ConfigMock.BrowserFunc: method is nil but Config.Browser was just called") } @@ -309,7 +309,7 @@ func (mock *ConfigMock) CacheDirCalls() []struct { } // Editor calls EditorFunc. -func (mock *ConfigMock) Editor(hostname string) string { +func (mock *ConfigMock) Editor(hostname string) gh.ConfigEntry { if mock.EditorFunc == nil { panic("ConfigMock.EditorFunc: method is nil but Config.Editor was just called") } @@ -341,7 +341,7 @@ func (mock *ConfigMock) EditorCalls() []struct { } // GetOrDefault calls GetOrDefaultFunc. -func (mock *ConfigMock) GetOrDefault(hostname string, key string) o.Option[string] { +func (mock *ConfigMock) GetOrDefault(hostname string, key string) o.Option[gh.ConfigEntry] { if mock.GetOrDefaultFunc == nil { panic("ConfigMock.GetOrDefaultFunc: method is nil but Config.GetOrDefault was just called") } @@ -377,7 +377,7 @@ func (mock *ConfigMock) GetOrDefaultCalls() []struct { } // GitProtocol calls GitProtocolFunc. -func (mock *ConfigMock) GitProtocol(hostname string) string { +func (mock *ConfigMock) GitProtocol(hostname string) gh.ConfigEntry { if mock.GitProtocolFunc == nil { panic("ConfigMock.GitProtocolFunc: method is nil but Config.GitProtocol was just called") } @@ -409,7 +409,7 @@ func (mock *ConfigMock) GitProtocolCalls() []struct { } // HTTPUnixSocket calls HTTPUnixSocketFunc. -func (mock *ConfigMock) HTTPUnixSocket(hostname string) string { +func (mock *ConfigMock) HTTPUnixSocket(hostname string) gh.ConfigEntry { if mock.HTTPUnixSocketFunc == nil { panic("ConfigMock.HTTPUnixSocketFunc: method is nil but Config.HTTPUnixSocket was just called") } @@ -473,7 +473,7 @@ func (mock *ConfigMock) MigrateCalls() []struct { } // Pager calls PagerFunc. -func (mock *ConfigMock) Pager(hostname string) string { +func (mock *ConfigMock) Pager(hostname string) gh.ConfigEntry { if mock.PagerFunc == nil { panic("ConfigMock.PagerFunc: method is nil but Config.Pager was just called") } @@ -505,7 +505,7 @@ func (mock *ConfigMock) PagerCalls() []struct { } // Prompt calls PromptFunc. -func (mock *ConfigMock) Prompt(hostname string) string { +func (mock *ConfigMock) Prompt(hostname string) gh.ConfigEntry { if mock.PromptFunc == nil { panic("ConfigMock.PromptFunc: method is nil but Config.Prompt was just called") } diff --git a/pkg/cmd/auth/refresh/refresh.go b/pkg/cmd/auth/refresh/refresh.go index 4e675c532..262b8fb31 100644 --- a/pkg/cmd/auth/refresh/refresh.go +++ b/pkg/cmd/auth/refresh/refresh.go @@ -177,7 +177,7 @@ func refreshRun(opts *RefreshOptions) error { Prompter: opts.Prompter, GitClient: opts.GitClient, } - gitProtocol := cfg.GitProtocol(hostname) + gitProtocol := cfg.GitProtocol(hostname).Value if opts.Interactive && gitProtocol == "https" { if err := credentialFlow.Prompt(hostname); err != nil { return err diff --git a/pkg/cmd/auth/status/status.go b/pkg/cmd/auth/status/status.go index deb5ca6a1..153c03c04 100644 --- a/pkg/cmd/auth/status/status.go +++ b/pkg/cmd/auth/status/status.go @@ -201,7 +201,7 @@ func statusRun(opts *StatusOptions) error { } var activeUser string - gitProtocol := cfg.GitProtocol(hostname) + gitProtocol := cfg.GitProtocol(hostname).Value activeUserToken, activeUserTokenSource := authCfg.ActiveToken(hostname) if authTokenWriteable(activeUserTokenSource) { activeUser, _ = authCfg.ActiveUser(hostname) diff --git a/pkg/cmd/config/get/get.go b/pkg/cmd/config/get/get.go index 82d75111b..bec22a979 100644 --- a/pkg/cmd/config/get/get.go +++ b/pkg/cmd/config/get/get.go @@ -64,12 +64,12 @@ func getRun(opts *GetOptions) error { return nil } - optionalValue := opts.Config.GetOrDefault(opts.Hostname, opts.Key) - if optionalValue.IsNone() { + optionalEntry := opts.Config.GetOrDefault(opts.Hostname, opts.Key) + if optionalEntry.IsNone() { return nonExistentKeyError{key: opts.Key} } - val := optionalValue.Unwrap() + val := optionalEntry.Unwrap().Value if val != "" { fmt.Fprintf(opts.IO.Out, "%s\n", val) } diff --git a/pkg/cmd/config/set/set_test.go b/pkg/cmd/config/set/set_test.go index 98c5fb1f2..adfb7ba74 100644 --- a/pkg/cmd/config/set/set_test.go +++ b/pkg/cmd/config/set/set_test.go @@ -150,9 +150,10 @@ func Test_setRun(t *testing.T) { assert.Equal(t, tt.stdout, stdout.String()) assert.Equal(t, tt.stderr, stderr.String()) - optionalValue := tt.input.Config.GetOrDefault(tt.input.Hostname, tt.input.Key) - assert.True(t, optionalValue.IsSome(), "expected value to be set") - assert.Equal(t, tt.expectedValue, optionalValue.Unwrap()) + optionalEntry := tt.input.Config.GetOrDefault(tt.input.Hostname, tt.input.Key) + entry := optionalEntry.Expect("expected a value to be set") + assert.Equal(t, tt.expectedValue, entry.Value) + assert.Equal(t, gh.ConfigUserProvided, entry.Source) }) } } diff --git a/pkg/cmd/extension/manager.go b/pkg/cmd/extension/manager.go index 37d80573a..0ab429c2e 100644 --- a/pkg/cmd/extension/manager.go +++ b/pkg/cmd/extension/manager.go @@ -347,7 +347,7 @@ func writeManifest(dir, name string, data []byte) (writeErr error) { } func (m *Manager) installGit(repo ghrepo.Interface, target string) error { - protocol := m.config.GitProtocol(repo.RepoHost()) + protocol := m.config.GitProtocol(repo.RepoHost()).Value cloneURL := ghrepo.FormatRemoteURL(repo, protocol) var commitSHA string diff --git a/pkg/cmd/factory/default.go b/pkg/cmd/factory/default.go index cb38c10fe..7abf3ca5f 100644 --- a/pkg/cmd/factory/default.go +++ b/pkg/cmd/factory/default.go @@ -185,7 +185,7 @@ func ioStreams(f *cmdutil.Factory) *iostreams.IOStreams { if _, ghPromptDisabled := os.LookupEnv("GH_PROMPT_DISABLED"); ghPromptDisabled { io.SetNeverPrompt(true) - } else if prompt := cfg.Prompt(""); prompt == "disabled" { + } else if prompt := cfg.Prompt(""); prompt.Value == "disabled" { io.SetNeverPrompt(true) } @@ -195,8 +195,8 @@ func ioStreams(f *cmdutil.Factory) *iostreams.IOStreams { // 3. PAGER if ghPager, ghPagerExists := os.LookupEnv("GH_PAGER"); ghPagerExists { io.SetPager(ghPager) - } else if pager := cfg.Pager(""); pager != "" { - io.SetPager(pager) + } else if pager := cfg.Pager(""); pager.Value != "" { + io.SetPager(pager.Value) } return io diff --git a/pkg/cmd/gist/clone/clone.go b/pkg/cmd/gist/clone/clone.go index 0f50bce31..6fa6f226a 100644 --- a/pkg/cmd/gist/clone/clone.go +++ b/pkg/cmd/gist/clone/clone.go @@ -80,7 +80,7 @@ func cloneRun(opts *CloneOptions) error { return err } hostname, _ := cfg.Authentication().DefaultHost() - protocol := cfg.GitProtocol(hostname) + protocol := cfg.GitProtocol(hostname).Value gistURL = formatRemoteURL(hostname, gistURL, protocol) } diff --git a/pkg/cmd/pr/checkout/checkout.go b/pkg/cmd/pr/checkout/checkout.go index f47f579f2..f566cbe1d 100644 --- a/pkg/cmd/pr/checkout/checkout.go +++ b/pkg/cmd/pr/checkout/checkout.go @@ -84,7 +84,7 @@ func checkoutRun(opts *CheckoutOptions) error { if err != nil { return err } - protocol := cfg.GitProtocol(baseRepo.RepoHost()) + protocol := cfg.GitProtocol(baseRepo.RepoHost()).Value remotes, err := opts.Remotes() if err != nil { diff --git a/pkg/cmd/pr/create/create.go b/pkg/cmd/pr/create/create.go index 54b6358ac..7c3faecc3 100644 --- a/pkg/cmd/pr/create/create.go +++ b/pkg/cmd/pr/create/create.go @@ -870,7 +870,7 @@ func handlePush(opts CreateOptions, ctx CreateContext) error { return err } - cloneProtocol := cfg.GitProtocol(headRepo.RepoHost()) + cloneProtocol := cfg.GitProtocol(headRepo.RepoHost()).Value headRepoURL := ghrepo.FormatRemoteURL(headRepo, cloneProtocol) gitClient := ctx.GitClient origin, _ := remotes.FindByName("origin") diff --git a/pkg/cmd/repo/clone/clone.go b/pkg/cmd/repo/clone/clone.go index 28001e6fe..1466cd96a 100644 --- a/pkg/cmd/repo/clone/clone.go +++ b/pkg/cmd/repo/clone/clone.go @@ -153,7 +153,7 @@ func cloneRun(opts *CloneOptions) error { return err } - protocol = cfg.GitProtocol(repo.RepoHost()) + protocol = cfg.GitProtocol(repo.RepoHost()).Value } wantsWiki := strings.HasSuffix(repo.RepoName(), ".wiki") @@ -187,7 +187,7 @@ func cloneRun(opts *CloneOptions) error { // If the repo is a fork, add the parent as an upstream remote and set the parent as the default repo. if canonicalRepo.Parent != nil { - protocol := cfg.GitProtocol(canonicalRepo.Parent.RepoHost()) + protocol := cfg.GitProtocol(canonicalRepo.Parent.RepoHost()).Value upstreamURL := ghrepo.FormatRemoteURL(canonicalRepo.Parent, protocol) upstreamName := opts.UpstreamName diff --git a/pkg/cmd/repo/create/create.go b/pkg/cmd/repo/create/create.go index af30f4029..48a331bdd 100644 --- a/pkg/cmd/repo/create/create.go +++ b/pkg/cmd/repo/create/create.go @@ -396,7 +396,7 @@ func createFromScratch(opts *CreateOptions) error { } if opts.Clone { - protocol := cfg.GitProtocol(repo.RepoHost()) + protocol := cfg.GitProtocol(repo.RepoHost()).Value remoteURL := ghrepo.FormatRemoteURL(repo, protocol) if !opts.AddReadme && opts.LicenseTemplate == "" && opts.GitIgnoreTemplate == "" && opts.Template == "" { @@ -494,7 +494,7 @@ func createFromTemplate(opts *CreateOptions) error { } if opts.Clone { - protocol := cfg.GitProtocol(repo.RepoHost()) + protocol := cfg.GitProtocol(repo.RepoHost()).Value remoteURL := ghrepo.FormatRemoteURL(repo, protocol) if err := cloneWithRetry(opts, remoteURL, templateRepoMainBranch); err != nil { @@ -617,7 +617,7 @@ func createFromLocal(opts *CreateOptions) error { fmt.Fprintln(stdout, repo.URL) } - protocol := cfg.GitProtocol(repo.RepoHost()) + protocol := cfg.GitProtocol(repo.RepoHost()).Value remoteURL := ghrepo.FormatRemoteURL(repo, protocol) if opts.Interactive { diff --git a/pkg/cmd/repo/fork/fork.go b/pkg/cmd/repo/fork/fork.go index a5e9066b2..a49f5d567 100644 --- a/pkg/cmd/repo/fork/fork.go +++ b/pkg/cmd/repo/fork/fork.go @@ -243,7 +243,9 @@ func forkRun(opts *ForkOptions) error { if err != nil { return err } - protocol := cfg.GitProtocol(repoToFork.RepoHost()) + protocolConfig := cfg.GitProtocol(repoToFork.RepoHost()) + protocolIsConfiguredByUser := protocolConfig.Source == gh.ConfigUserProvided + protocol := protocolConfig.Value gitClient := opts.GitClient ctx := context.Background() @@ -254,7 +256,7 @@ func forkRun(opts *ForkOptions) error { return err } - if protocol == "" { // user has no set preference + if !protocolIsConfiguredByUser { if remote, err := remotes.FindByRepo(repoToFork.RepoOwner(), repoToFork.RepoName()); err == nil { scheme := "" if remote.FetchURL != nil { diff --git a/pkg/cmd/repo/fork/fork_test.go b/pkg/cmd/repo/fork/fork_test.go index b1b62e11a..0f94496f0 100644 --- a/pkg/cmd/repo/fork/fork_test.go +++ b/pkg/cmd/repo/fork/fork_test.go @@ -234,6 +234,9 @@ func TestRepoFork(t *testing.T) { Repo: ghrepo.New("OWNER", "REPO"), }, }, + cfgStubs: func(_ *testing.T, c gh.Config) { + c.Set("", "git_protocol", "https") + }, httpStubs: forkPost, execStubs: func(cs *run.CommandStubber) { cs.Register(`git remote add fork https://github\.com/someone/REPO\.git`, 0, "") @@ -255,9 +258,6 @@ func TestRepoFork(t *testing.T) { Repo: ghrepo.New("OWNER", "REPO"), }, }, - cfgStubs: func(_ *testing.T, c gh.Config) { - c.Set("", "git_protocol", "") - }, httpStubs: forkPost, execStubs: func(cs *run.CommandStubber) { cs.Register(`git remote add fork git@github\.com:someone/REPO\.git`, 0, "") diff --git a/pkg/cmd/repo/rename/rename.go b/pkg/cmd/repo/rename/rename.go index 7a011b4e3..3676f93fe 100644 --- a/pkg/cmd/repo/rename/rename.go +++ b/pkg/cmd/repo/rename/rename.go @@ -153,7 +153,7 @@ func updateRemote(repo ghrepo.Interface, renamed ghrepo.Interface, opts *RenameO return nil, err } - protocol := cfg.GitProtocol(repo.RepoHost()) + protocol := cfg.GitProtocol(repo.RepoHost()).Value remotes, err := opts.Remotes() if err != nil { diff --git a/pkg/cmdutil/legacy.go b/pkg/cmdutil/legacy.go index 0cc9674e1..04e72aedb 100644 --- a/pkg/cmdutil/legacy.go +++ b/pkg/cmdutil/legacy.go @@ -16,7 +16,7 @@ func DetermineEditor(cf func() (gh.Config, error)) (string, error) { if err != nil { return "", fmt.Errorf("could not read config: %w", err) } - editorCommand = cfg.Editor("") + editorCommand = cfg.Editor("").Value } return editorCommand, nil diff --git a/pkg/option/option.go b/pkg/option/option.go index 30fdc1412..081ccb632 100644 --- a/pkg/option/option.go +++ b/pkg/option/option.go @@ -117,3 +117,11 @@ func (o Option[T]) Expect(message string) T { panic(message) } + +func Map[T, U any](o Option[T], f func(T) U) 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 index 5e5068124..0f28dd5d4 100644 --- a/pkg/option/option_test.go +++ b/pkg/option/option_test.go @@ -99,6 +99,19 @@ func ExampleOption_Expect() { // Output: 4 } +func ExampleMap() { + fmt.Println(o.Map(o.Some(2), double)) + fmt.Println(o.Map(o.None[int](), double)) + + // Output: + // Some(4) + // None +} + +func double(i int) int { + return i * 2 +} + 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 @@ -179,3 +192,8 @@ func TestNoneExpect(t *testing.T) { o.None[int]().Expect("oops") t.Error("did not panic") } + +func TestMap(t *testing.T) { + require.Equal(t, o.Map(o.Some(2), double), o.Some(4)) + require.True(t, o.Map(o.None[int](), double).IsNone()) +}