Add auth setup-git for setting up gh as a git credential helper (#4246)
Adds a new command `gh auth setup-git [<hostname>]` that sets up git to use the GitHub 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 --replace-all '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. Co-authored-by: Mislav Marohnić <mislav@github.com>
This commit is contained in:
parent
a056fbf0cb
commit
94a640bd2a
5 changed files with 226 additions and 2 deletions
|
|
@ -5,6 +5,7 @@ import (
|
|||
authLoginCmd "github.com/cli/cli/v2/pkg/cmd/auth/login"
|
||||
authLogoutCmd "github.com/cli/cli/v2/pkg/cmd/auth/logout"
|
||||
authRefreshCmd "github.com/cli/cli/v2/pkg/cmd/auth/refresh"
|
||||
authSetupGitCmd "github.com/cli/cli/v2/pkg/cmd/auth/setupgit"
|
||||
authStatusCmd "github.com/cli/cli/v2/pkg/cmd/auth/status"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/spf13/cobra"
|
||||
|
|
@ -24,6 +25,7 @@ func NewCmdAuth(f *cmdutil.Factory) *cobra.Command {
|
|||
cmd.AddCommand(authStatusCmd.NewCmdStatus(f, nil))
|
||||
cmd.AddCommand(authRefreshCmd.NewCmdRefresh(f, nil))
|
||||
cmd.AddCommand(gitCredentialCmd.NewCmdCredential(f, nil))
|
||||
cmd.AddCommand(authSetupGitCmd.NewCmdSetupGit(f, nil))
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
|
|
|||
100
pkg/cmd/auth/setupgit/setupgit.go
Normal file
100
pkg/cmd/auth/setupgit/setupgit.go
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
package setupgit
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/cli/cli/v2/internal/config"
|
||||
"github.com/cli/cli/v2/pkg/cmd/auth/shared"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type gitConfigurator interface {
|
||||
Setup(hostname, username, authToken string) error
|
||||
}
|
||||
|
||||
type SetupGitOptions struct {
|
||||
IO *iostreams.IOStreams
|
||||
Config func() (config.Config, error)
|
||||
Hostname string
|
||||
gitConfigure gitConfigurator
|
||||
}
|
||||
|
||||
func NewCmdSetupGit(f *cmdutil.Factory, runF func(*SetupGitOptions) error) *cobra.Command {
|
||||
opts := &SetupGitOptions{
|
||||
IO: f.IOStreams,
|
||||
Config: f.Config,
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Short: "Configure git to use GitHub CLI as a credential helper",
|
||||
Use: "setup-git",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
opts.gitConfigure = &shared.GitCredentialFlow{
|
||||
Executable: f.Executable(),
|
||||
}
|
||||
|
||||
if runF != nil {
|
||||
return runF(opts)
|
||||
}
|
||||
return setupGitRun(opts)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVarP(&opts.Hostname, "hostname", "h", "", "The hostname to configure git for")
|
||||
|
||||
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 !has(opts.Hostname, hostnames) {
|
||||
return fmt.Errorf("You are not logged into the GitHub host %q\n", opts.Hostname)
|
||||
}
|
||||
hostnamesToSetup = []string{opts.Hostname}
|
||||
}
|
||||
|
||||
for _, hostname := range hostnamesToSetup {
|
||||
if err := opts.gitConfigure.Setup(hostname, "", ""); err != nil {
|
||||
return fmt.Errorf("failed to set up git credential helper: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func has(needle string, haystack []string) bool {
|
||||
for _, s := range haystack {
|
||||
if strings.EqualFold(s, needle) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
122
pkg/cmd/auth/setupgit/setupgit_test.go
Normal file
122
pkg/cmd/auth/setupgit/setupgit_test.go
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
package setupgit
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/cli/cli/v2/internal/config"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"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_setupGitRun(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
opts *SetupGitOptions
|
||||
expectedErr string
|
||||
expectedErrOut string
|
||||
}{
|
||||
{
|
||||
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: "You are not logged into any GitHub hosts. Run gh auth login to authenticate.\n",
|
||||
},
|
||||
{
|
||||
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: "You are not logged into the GitHub host \"foo\"\n",
|
||||
expectedErrOut: "",
|
||||
},
|
||||
{
|
||||
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: "failed to set up git credential helper: broken",
|
||||
expectedErrOut: "",
|
||||
},
|
||||
{
|
||||
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)
|
||||
}
|
||||
|
||||
assert.Equal(t, tt.expectedErrOut, stderr.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -64,7 +64,7 @@ func (flow *GitCredentialFlow) Setup(hostname, username, authToken string) error
|
|||
func (flow *GitCredentialFlow) gitCredentialSetup(hostname, username, password string) error {
|
||||
if flow.helper == "" {
|
||||
// first use a blank value to indicate to git we want to sever the chain of credential helpers
|
||||
preConfigureCmd, err := git.GitCommand("config", "--global", gitCredentialHelperKey(hostname), "")
|
||||
preConfigureCmd, err := git.GitCommand("config", "--global", "--replace-all", gitCredentialHelperKey(hostname), "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ func TestGitCredentialSetup_configureExisting(t *testing.T) {
|
|||
func TestGitCredentialSetup_setOurs(t *testing.T) {
|
||||
cs, restoreRun := run.Stub()
|
||||
defer restoreRun(t)
|
||||
cs.Register(`git config --global credential\.`, 0, "", func(args []string) {
|
||||
cs.Register(`git config --global --replace-all credential\.`, 0, "", func(args []string) {
|
||||
if key := args[len(args)-2]; key != "credential.https://example.com.helper" {
|
||||
t.Errorf("git config key was %q", key)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue