X Tutup
Skip to content

Commit 94a640b

Browse files
desprestonmislav
andauthored
Add auth setup-git for setting up gh as a git credential helper (cli#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>
1 parent a056fbf commit 94a640b

File tree

5 files changed

+226
-2
lines changed

5 files changed

+226
-2
lines changed

pkg/cmd/auth/auth.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
authLoginCmd "github.com/cli/cli/v2/pkg/cmd/auth/login"
66
authLogoutCmd "github.com/cli/cli/v2/pkg/cmd/auth/logout"
77
authRefreshCmd "github.com/cli/cli/v2/pkg/cmd/auth/refresh"
8+
authSetupGitCmd "github.com/cli/cli/v2/pkg/cmd/auth/setupgit"
89
authStatusCmd "github.com/cli/cli/v2/pkg/cmd/auth/status"
910
"github.com/cli/cli/v2/pkg/cmdutil"
1011
"github.com/spf13/cobra"
@@ -24,6 +25,7 @@ func NewCmdAuth(f *cmdutil.Factory) *cobra.Command {
2425
cmd.AddCommand(authStatusCmd.NewCmdStatus(f, nil))
2526
cmd.AddCommand(authRefreshCmd.NewCmdRefresh(f, nil))
2627
cmd.AddCommand(gitCredentialCmd.NewCmdCredential(f, nil))
28+
cmd.AddCommand(authSetupGitCmd.NewCmdSetupGit(f, nil))
2729

2830
return cmd
2931
}

pkg/cmd/auth/setupgit/setupgit.go

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package setupgit
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
7+
"github.com/cli/cli/v2/internal/config"
8+
"github.com/cli/cli/v2/pkg/cmd/auth/shared"
9+
"github.com/cli/cli/v2/pkg/cmdutil"
10+
"github.com/cli/cli/v2/pkg/iostreams"
11+
"github.com/spf13/cobra"
12+
)
13+
14+
type gitConfigurator interface {
15+
Setup(hostname, username, authToken string) error
16+
}
17+
18+
type SetupGitOptions struct {
19+
IO *iostreams.IOStreams
20+
Config func() (config.Config, error)
21+
Hostname string
22+
gitConfigure gitConfigurator
23+
}
24+
25+
func NewCmdSetupGit(f *cmdutil.Factory, runF func(*SetupGitOptions) error) *cobra.Command {
26+
opts := &SetupGitOptions{
27+
IO: f.IOStreams,
28+
Config: f.Config,
29+
}
30+
31+
cmd := &cobra.Command{
32+
Short: "Configure git to use GitHub CLI as a credential helper",
33+
Use: "setup-git",
34+
RunE: func(cmd *cobra.Command, args []string) error {
35+
opts.gitConfigure = &shared.GitCredentialFlow{
36+
Executable: f.Executable(),
37+
}
38+
39+
if runF != nil {
40+
return runF(opts)
41+
}
42+
return setupGitRun(opts)
43+
},
44+
}
45+
46+
cmd.Flags().StringVarP(&opts.Hostname, "hostname", "h", "", "The hostname to configure git for")
47+
48+
return cmd
49+
}
50+
51+
func setupGitRun(opts *SetupGitOptions) error {
52+
cfg, err := opts.Config()
53+
if err != nil {
54+
return err
55+
}
56+
57+
hostnames, err := cfg.Hosts()
58+
if err != nil {
59+
return err
60+
}
61+
62+
stderr := opts.IO.ErrOut
63+
cs := opts.IO.ColorScheme()
64+
65+
if len(hostnames) == 0 {
66+
fmt.Fprintf(
67+
stderr,
68+
"You are not logged into any GitHub hosts. Run %s to authenticate.\n",
69+
cs.Bold("gh auth login"),
70+
)
71+
72+
return cmdutil.SilentError
73+
}
74+
75+
hostnamesToSetup := hostnames
76+
77+
if opts.Hostname != "" {
78+
if !has(opts.Hostname, hostnames) {
79+
return fmt.Errorf("You are not logged into the GitHub host %q\n", opts.Hostname)
80+
}
81+
hostnamesToSetup = []string{opts.Hostname}
82+
}
83+
84+
for _, hostname := range hostnamesToSetup {
85+
if err := opts.gitConfigure.Setup(hostname, "", ""); err != nil {
86+
return fmt.Errorf("failed to set up git credential helper: %w", err)
87+
}
88+
}
89+
90+
return nil
91+
}
92+
93+
func has(needle string, haystack []string) bool {
94+
for _, s := range haystack {
95+
if strings.EqualFold(s, needle) {
96+
return true
97+
}
98+
}
99+
return false
100+
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
package setupgit
2+
3+
import (
4+
"fmt"
5+
"testing"
6+
7+
"github.com/cli/cli/v2/internal/config"
8+
"github.com/cli/cli/v2/pkg/iostreams"
9+
"github.com/stretchr/testify/assert"
10+
"github.com/stretchr/testify/require"
11+
)
12+
13+
type mockGitConfigurer struct {
14+
setupErr error
15+
}
16+
17+
func (gf *mockGitConfigurer) Setup(hostname, username, authToken string) error {
18+
return gf.setupErr
19+
}
20+
21+
func Test_setupGitRun(t *testing.T) {
22+
tests := []struct {
23+
name string
24+
opts *SetupGitOptions
25+
expectedErr string
26+
expectedErrOut string
27+
}{
28+
{
29+
name: "opts.Config returns an error",
30+
opts: &SetupGitOptions{
31+
Config: func() (config.Config, error) {
32+
return nil, fmt.Errorf("oops")
33+
},
34+
},
35+
expectedErr: "oops",
36+
},
37+
{
38+
name: "no authenticated hostnames",
39+
opts: &SetupGitOptions{},
40+
expectedErr: "SilentError",
41+
expectedErrOut: "You are not logged into any GitHub hosts. Run gh auth login to authenticate.\n",
42+
},
43+
{
44+
name: "not authenticated with the hostname given as flag",
45+
opts: &SetupGitOptions{
46+
Hostname: "foo",
47+
Config: func() (config.Config, error) {
48+
cfg := config.NewBlankConfig()
49+
require.NoError(t, cfg.Set("bar", "", ""))
50+
return cfg, nil
51+
},
52+
},
53+
expectedErr: "You are not logged into the GitHub host \"foo\"\n",
54+
expectedErrOut: "",
55+
},
56+
{
57+
name: "error setting up git for hostname",
58+
opts: &SetupGitOptions{
59+
gitConfigure: &mockGitConfigurer{
60+
setupErr: fmt.Errorf("broken"),
61+
},
62+
Config: func() (config.Config, error) {
63+
cfg := config.NewBlankConfig()
64+
require.NoError(t, cfg.Set("bar", "", ""))
65+
return cfg, nil
66+
},
67+
},
68+
expectedErr: "failed to set up git credential helper: broken",
69+
expectedErrOut: "",
70+
},
71+
{
72+
name: "no hostname option given. Setup git for each hostname in config",
73+
opts: &SetupGitOptions{
74+
gitConfigure: &mockGitConfigurer{},
75+
Config: func() (config.Config, error) {
76+
cfg := config.NewBlankConfig()
77+
require.NoError(t, cfg.Set("bar", "", ""))
78+
return cfg, nil
79+
},
80+
},
81+
},
82+
{
83+
name: "setup git for the hostname given via options",
84+
opts: &SetupGitOptions{
85+
Hostname: "yes",
86+
gitConfigure: &mockGitConfigurer{},
87+
Config: func() (config.Config, error) {
88+
cfg := config.NewBlankConfig()
89+
require.NoError(t, cfg.Set("bar", "", ""))
90+
require.NoError(t, cfg.Set("yes", "", ""))
91+
return cfg, nil
92+
},
93+
},
94+
},
95+
}
96+
97+
for _, tt := range tests {
98+
t.Run(tt.name, func(t *testing.T) {
99+
if tt.opts.Config == nil {
100+
tt.opts.Config = func() (config.Config, error) {
101+
return config.NewBlankConfig(), nil
102+
}
103+
}
104+
105+
io, _, _, stderr := iostreams.Test()
106+
107+
io.SetStdinTTY(true)
108+
io.SetStderrTTY(true)
109+
io.SetStdoutTTY(true)
110+
tt.opts.IO = io
111+
112+
err := setupGitRun(tt.opts)
113+
if tt.expectedErr != "" {
114+
assert.EqualError(t, err, tt.expectedErr)
115+
} else {
116+
assert.NoError(t, err)
117+
}
118+
119+
assert.Equal(t, tt.expectedErrOut, stderr.String())
120+
})
121+
}
122+
}

pkg/cmd/auth/shared/git_credential.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ func (flow *GitCredentialFlow) Setup(hostname, username, authToken string) error
6464
func (flow *GitCredentialFlow) gitCredentialSetup(hostname, username, password string) error {
6565
if flow.helper == "" {
6666
// first use a blank value to indicate to git we want to sever the chain of credential helpers
67-
preConfigureCmd, err := git.GitCommand("config", "--global", gitCredentialHelperKey(hostname), "")
67+
preConfigureCmd, err := git.GitCommand("config", "--global", "--replace-all", gitCredentialHelperKey(hostname), "")
6868
if err != nil {
6969
return err
7070
}

pkg/cmd/auth/shared/git_credential_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ func TestGitCredentialSetup_configureExisting(t *testing.T) {
2525
func TestGitCredentialSetup_setOurs(t *testing.T) {
2626
cs, restoreRun := run.Stub()
2727
defer restoreRun(t)
28-
cs.Register(`git config --global credential\.`, 0, "", func(args []string) {
28+
cs.Register(`git config --global --replace-all credential\.`, 0, "", func(args []string) {
2929
if key := args[len(args)-2]; key != "credential.https://example.com.helper" {
3030
t.Errorf("git config key was %q", key)
3131
}

0 commit comments

Comments
 (0)
X Tutup