forked from cli/cli
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(3796): add cmd for setting up gh-cli as git cred helper
Adds a new command `gh auth setup-git [<hostname>]` that sets up git to use the GH CLI as a credential helper. The gist is that it runs these two git commands for each hostname the user is authenticated with. ``` git config --global 'credential.https://github.com.helper' '' git config --global --add 'credential.https://github.com.helper' '!gh auth git-credential' ``` If a hostname flag is given, it'll setup GH CLI as a credential helper for only that hostname. If the user is not authenticated with any git hostnames, or the user is not authenticated with the hostname given as a flag, it'll print an error and return a SilentError. Closes cli#3796
- Loading branch information
1 parent
e6ff77c
commit 2c41882
Showing
5 changed files
with
306 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
package setupgit | ||
|
||
import ( | ||
"fmt" | ||
|
||
"github.com/cli/cli/internal/config" | ||
"github.com/cli/cli/pkg/cmd/auth/shared" | ||
"github.com/cli/cli/pkg/cmdutil" | ||
"github.com/cli/cli/pkg/iostreams" | ||
"github.com/cli/cli/utils" | ||
"github.com/spf13/cobra" | ||
) | ||
|
||
type gitConfigurator interface { | ||
Setup(hostname, username, authToken string) error | ||
} | ||
|
||
type SetupGitOptions struct { | ||
IO *iostreams.IOStreams | ||
// Hostname is the host to setup as git credential helper for. This is set via command flag. | ||
Hostname string | ||
Config func() (config.Config, error) | ||
gitConfigure gitConfigurator | ||
} | ||
|
||
func NewCmdSetupGit(f *cmdutil.Factory, runF func(*SetupGitOptions) error) *cobra.Command { | ||
opts := &SetupGitOptions{ | ||
IO: f.IOStreams, | ||
Config: f.Config, | ||
gitConfigure: &shared.GitCredentialFlow{}, | ||
} | ||
|
||
cmd := &cobra.Command{ | ||
Short: "Setup GH CLI as a git credential helper", | ||
Long: "Setup GH CLI as a git credential helper for all authenticated hostnames.", | ||
Use: "setup-git [<hostname>]", | ||
RunE: func(cmd *cobra.Command, args []string) error { | ||
if runF != nil { | ||
return runF(opts) | ||
} | ||
return setupGitRun(opts) | ||
}, | ||
} | ||
|
||
cmd.Flags().StringVarP( | ||
&opts.Hostname, | ||
"hostname", | ||
"h", | ||
"", | ||
"Setup git credential helper for a specific hostname", | ||
) | ||
|
||
return cmd | ||
} | ||
|
||
func setupGitRun(opts *SetupGitOptions) error { | ||
cfg, err := opts.Config() | ||
if err != nil { | ||
return err | ||
} | ||
|
||
hostnames, err := cfg.Hosts() | ||
if err != nil { | ||
return err | ||
} | ||
|
||
stderr := opts.IO.ErrOut | ||
cs := opts.IO.ColorScheme() | ||
|
||
if len(hostnames) == 0 { | ||
fmt.Fprintf( | ||
stderr, | ||
"You are not logged into any GitHub hosts. Run %s to authenticate.\n", | ||
cs.Bold("gh auth login"), | ||
) | ||
|
||
return cmdutil.SilentError | ||
} | ||
|
||
hostnamesToSetup := hostnames | ||
|
||
if opts.Hostname != "" { | ||
if !utils.Has(opts.Hostname, hostnames) { | ||
fmt.Fprintf( | ||
stderr, | ||
"You are not logged into any Github host with the hostname %s\n", | ||
opts.Hostname, | ||
) | ||
return cmdutil.SilentError | ||
} | ||
hostnamesToSetup = []string{opts.Hostname} | ||
} | ||
|
||
if err := setupGitForHostnames(hostnamesToSetup, opts.gitConfigure); err != nil { | ||
fmt.Fprintln(stderr, err.Error()) | ||
return cmdutil.SilentError | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func setupGitForHostnames(hostnames []string, gitCfg gitConfigurator) error { | ||
for _, hostname := range hostnames { | ||
if err := gitCfg.Setup(hostname, "", ""); err != nil { | ||
return fmt.Errorf("failed to setup git credential helper: %w", err) | ||
} | ||
} | ||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,178 @@ | ||
package setupgit | ||
|
||
import ( | ||
"bytes" | ||
"fmt" | ||
"regexp" | ||
"testing" | ||
|
||
"github.com/cli/cli/internal/config" | ||
"github.com/cli/cli/pkg/cmdutil" | ||
"github.com/cli/cli/pkg/iostreams" | ||
"github.com/google/shlex" | ||
"github.com/stretchr/testify/assert" | ||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
type mockGitConfigurer struct { | ||
setupErr error | ||
} | ||
|
||
func (gf *mockGitConfigurer) Setup(hostname, username, authToken string) error { | ||
return gf.setupErr | ||
} | ||
|
||
func Test_NewCmdSetupGit(t *testing.T) { | ||
tests := []struct { | ||
name string | ||
cli string | ||
wants SetupGitOptions | ||
wantsErr bool | ||
}{ | ||
{ | ||
name: "no arguments", | ||
cli: "", | ||
wants: SetupGitOptions{Hostname: ""}, | ||
}, | ||
{ | ||
name: "hostname argument", | ||
cli: "--hostname whatever", | ||
wants: SetupGitOptions{Hostname: "whatever"}, | ||
}, | ||
} | ||
|
||
for _, tt := range tests { | ||
t.Run(tt.name, func(t *testing.T) { | ||
f := &cmdutil.Factory{} | ||
|
||
argv, err := shlex.Split(tt.cli) | ||
assert.NoError(t, err) | ||
|
||
var gotOpts *SetupGitOptions | ||
cmd := NewCmdSetupGit(f, func(opts *SetupGitOptions) error { | ||
gotOpts = opts | ||
return nil | ||
}) | ||
|
||
// TODO cobra hack-around | ||
cmd.Flags().BoolP("help", "x", false, "") | ||
|
||
cmd.SetArgs(argv) | ||
cmd.SetIn(&bytes.Buffer{}) | ||
cmd.SetOut(&bytes.Buffer{}) | ||
cmd.SetErr(&bytes.Buffer{}) | ||
|
||
_, err = cmd.ExecuteC() | ||
assert.NoError(t, err) | ||
|
||
assert.Equal(t, tt.wants.Hostname, gotOpts.Hostname) | ||
}) | ||
} | ||
} | ||
|
||
func Test_setupGitRun(t *testing.T) { | ||
tests := []struct { | ||
name string | ||
opts *SetupGitOptions | ||
expectedErr string | ||
expectedErrOut *regexp.Regexp | ||
}{ | ||
{ | ||
name: "opts.Config returns an error", | ||
opts: &SetupGitOptions{ | ||
Config: func() (config.Config, error) { | ||
return nil, fmt.Errorf("oops") | ||
}, | ||
}, | ||
expectedErr: "oops", | ||
}, | ||
{ | ||
name: "no authenticated hostnames", | ||
opts: &SetupGitOptions{}, | ||
expectedErr: "SilentError", | ||
expectedErrOut: regexp.MustCompile("You are not logged into any GitHub hosts."), | ||
}, | ||
{ | ||
name: "not authenticated with the hostname given as flag", | ||
opts: &SetupGitOptions{ | ||
Hostname: "foo", | ||
Config: func() (config.Config, error) { | ||
cfg := config.NewBlankConfig() | ||
require.NoError(t, cfg.Set("bar", "", "")) | ||
return cfg, nil | ||
}, | ||
}, | ||
expectedErr: "SilentError", | ||
expectedErrOut: regexp.MustCompile("You are not logged into any Github host with the hostname foo"), | ||
}, | ||
{ | ||
name: "error setting up git for hostname", | ||
opts: &SetupGitOptions{ | ||
gitConfigure: &mockGitConfigurer{ | ||
setupErr: fmt.Errorf("broken"), | ||
}, | ||
Config: func() (config.Config, error) { | ||
cfg := config.NewBlankConfig() | ||
require.NoError(t, cfg.Set("bar", "", "")) | ||
return cfg, nil | ||
}, | ||
}, | ||
expectedErr: "SilentError", | ||
expectedErrOut: regexp.MustCompile("failed to setup git credential helper"), | ||
}, | ||
{ | ||
name: "no hostname option given. Setup git for each hostname in config", | ||
opts: &SetupGitOptions{ | ||
gitConfigure: &mockGitConfigurer{}, | ||
Config: func() (config.Config, error) { | ||
cfg := config.NewBlankConfig() | ||
require.NoError(t, cfg.Set("bar", "", "")) | ||
return cfg, nil | ||
}, | ||
}, | ||
}, | ||
{ | ||
name: "setup git for the hostname given via options", | ||
opts: &SetupGitOptions{ | ||
Hostname: "yes", | ||
gitConfigure: &mockGitConfigurer{}, | ||
Config: func() (config.Config, error) { | ||
cfg := config.NewBlankConfig() | ||
require.NoError(t, cfg.Set("bar", "", "")) | ||
require.NoError(t, cfg.Set("yes", "", "")) | ||
return cfg, nil | ||
}, | ||
}, | ||
}, | ||
} | ||
|
||
for _, tt := range tests { | ||
t.Run(tt.name, func(t *testing.T) { | ||
if tt.opts.Config == nil { | ||
tt.opts.Config = func() (config.Config, error) { | ||
return config.NewBlankConfig(), nil | ||
} | ||
} | ||
|
||
io, _, _, stderr := iostreams.Test() | ||
|
||
io.SetStdinTTY(true) | ||
io.SetStderrTTY(true) | ||
io.SetStdoutTTY(true) | ||
tt.opts.IO = io | ||
|
||
err := setupGitRun(tt.opts) | ||
if tt.expectedErr != "" { | ||
assert.EqualError(t, err, tt.expectedErr) | ||
} else { | ||
assert.NoError(t, err) | ||
} | ||
|
||
if tt.expectedErrOut == nil { | ||
assert.Equal(t, "", stderr.String()) | ||
} else { | ||
assert.True(t, tt.expectedErrOut.MatchString(stderr.String())) | ||
} | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters