-
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.
Add
auth setup-git
for setting up gh as a git credential helper (cl…
…i#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ć <[email protected]>
- Loading branch information
1 parent
a056fbf
commit 94a640b
Showing
5 changed files
with
226 additions
and
2 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,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 | ||
} |
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,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()) | ||
}) | ||
} | ||
} |
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