Use absolute path when configuring gh as git credential
This keeps git operations working even when PATH is modified, e.g. `brew
update` will work even though Homebrew runs the command explicitly
without `/usr/local/bin` in PATH.
Additionally, this inserts a blank value for `credential.*.helper` to
instruct git to ignore previously configured credential helpers, i.e.
those that might have been set up in system configuration files. We do
this because otherwise, git will store the credential obtained from gh
in every other credential helper in the chain, which we want to avoid.
Before:
git config --global credential.https://github.com.helper '!gh auth git-credential'
After:
git config --global credential.https://github.com.helper ''
git config --global --add credential.https://github.com.helper '!/path/to/gh auth git-credential'
This commit is contained in:
parent
3444d00bee
commit
98f1f5ec0d
7 changed files with 70 additions and 9 deletions
|
|
@ -23,6 +23,8 @@ type LoginOptions struct {
|
||||||
Config func() (config.Config, error)
|
Config func() (config.Config, error)
|
||||||
HttpClient func() (*http.Client, error)
|
HttpClient func() (*http.Client, error)
|
||||||
|
|
||||||
|
MainExecutable string
|
||||||
|
|
||||||
Interactive bool
|
Interactive bool
|
||||||
|
|
||||||
Hostname string
|
Hostname string
|
||||||
|
|
@ -36,6 +38,8 @@ func NewCmdLogin(f *cmdutil.Factory, runF func(*LoginOptions) error) *cobra.Comm
|
||||||
IO: f.IOStreams,
|
IO: f.IOStreams,
|
||||||
Config: f.Config,
|
Config: f.Config,
|
||||||
HttpClient: f.HttpClient,
|
HttpClient: f.HttpClient,
|
||||||
|
|
||||||
|
MainExecutable: f.Executable,
|
||||||
}
|
}
|
||||||
|
|
||||||
var tokenStdin bool
|
var tokenStdin bool
|
||||||
|
|
@ -189,6 +193,7 @@ func loginRun(opts *LoginOptions) error {
|
||||||
Interactive: opts.Interactive,
|
Interactive: opts.Interactive,
|
||||||
Web: opts.Web,
|
Web: opts.Web,
|
||||||
Scopes: opts.Scopes,
|
Scopes: opts.Scopes,
|
||||||
|
Executable: opts.MainExecutable,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,8 @@ type RefreshOptions struct {
|
||||||
IO *iostreams.IOStreams
|
IO *iostreams.IOStreams
|
||||||
Config func() (config.Config, error)
|
Config func() (config.Config, error)
|
||||||
|
|
||||||
|
MainExecutable string
|
||||||
|
|
||||||
Hostname string
|
Hostname string
|
||||||
Scopes []string
|
Scopes []string
|
||||||
AuthFlow func(config.Config, *iostreams.IOStreams, string, []string) error
|
AuthFlow func(config.Config, *iostreams.IOStreams, string, []string) error
|
||||||
|
|
@ -34,6 +36,7 @@ func NewCmdRefresh(f *cmdutil.Factory, runF func(*RefreshOptions) error) *cobra.
|
||||||
_, err := authflow.AuthFlowWithConfig(cfg, io, hostname, "", scopes)
|
_, err := authflow.AuthFlowWithConfig(cfg, io, hostname, "", scopes)
|
||||||
return err
|
return err
|
||||||
},
|
},
|
||||||
|
MainExecutable: f.Executable,
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,8 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type GitCredentialFlow struct {
|
type GitCredentialFlow struct {
|
||||||
|
Executable string
|
||||||
|
|
||||||
shouldSetup bool
|
shouldSetup bool
|
||||||
helper string
|
helper string
|
||||||
scopes []string
|
scopes []string
|
||||||
|
|
@ -50,13 +52,26 @@ func (flow *GitCredentialFlow) ShouldSetup() bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (flow *GitCredentialFlow) Setup(hostname, username, authToken string) error {
|
func (flow *GitCredentialFlow) Setup(hostname, username, authToken string) error {
|
||||||
return GitCredentialSetup(hostname, username, authToken, flow.helper)
|
return flow.gitCredentialSetup(hostname, username, authToken)
|
||||||
}
|
}
|
||||||
|
|
||||||
func GitCredentialSetup(hostname, username, password, helper string) error {
|
func (flow *GitCredentialFlow) gitCredentialSetup(hostname, username, password string) error {
|
||||||
if helper == "" {
|
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), "")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = run.PrepareCmd(preConfigureCmd).Run(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// use GitHub CLI as a credential helper (for this host only)
|
// use GitHub CLI as a credential helper (for this host only)
|
||||||
configureCmd, err := git.GitCommand("config", "--global", gitCredentialHelperKey(hostname), "!gh auth git-credential")
|
configureCmd, err := git.GitCommand(
|
||||||
|
"config", "--global", "--add",
|
||||||
|
gitCredentialHelperKey(hostname),
|
||||||
|
fmt.Sprintf("!%s auth git-credential", shellQuote(flow.Executable)),
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -124,3 +139,10 @@ func isOurCredentialHelper(cmd string) bool {
|
||||||
|
|
||||||
return strings.TrimSuffix(filepath.Base(args[0]), ".exe") == "gh"
|
return strings.TrimSuffix(filepath.Base(args[0]), ".exe") == "gh"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func shellQuote(s string) string {
|
||||||
|
if strings.ContainsAny(s, " $") {
|
||||||
|
return "'" + s + "'"
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,12 @@ func TestGitCredentialSetup_configureExisting(t *testing.T) {
|
||||||
cs.Register(`git credential reject`, 0, "")
|
cs.Register(`git credential reject`, 0, "")
|
||||||
cs.Register(`git credential approve`, 0, "")
|
cs.Register(`git credential approve`, 0, "")
|
||||||
|
|
||||||
if err := GitCredentialSetup("example.com", "monalisa", "PASSWD", "osxkeychain"); err != nil {
|
f := GitCredentialFlow{
|
||||||
|
Executable: "gh",
|
||||||
|
helper: "osxkeychain",
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := f.gitCredentialSetup("example.com", "monalisa", "PASSWD"); err != nil {
|
||||||
t.Errorf("GitCredentialSetup() error = %v", err)
|
t.Errorf("GitCredentialSetup() error = %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -20,13 +25,29 @@ func TestGitCredentialSetup_configureExisting(t *testing.T) {
|
||||||
func TestGitCredentialSetup_setOurs(t *testing.T) {
|
func TestGitCredentialSetup_setOurs(t *testing.T) {
|
||||||
cs, restoreRun := run.Stub()
|
cs, restoreRun := run.Stub()
|
||||||
defer restoreRun(t)
|
defer restoreRun(t)
|
||||||
cs.Register(`git config --global credential\.https://example\.com\.helper`, 0, "", func(args []string) {
|
cs.Register(`git config --global credential\.`, 0, "", func(args []string) {
|
||||||
if val := args[len(args)-1]; val != "!gh auth git-credential" {
|
if key := args[len(args)-2]; key != "credential.https://example.com.helper" {
|
||||||
|
t.Errorf("git config key was %q", key)
|
||||||
|
}
|
||||||
|
if val := args[len(args)-1]; val != "" {
|
||||||
|
t.Errorf("global credential helper configured to %q", val)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
cs.Register(`git config --global --add 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)
|
||||||
|
}
|
||||||
|
if val := args[len(args)-1]; val != "!/path/to/gh auth git-credential" {
|
||||||
t.Errorf("global credential helper configured to %q", val)
|
t.Errorf("global credential helper configured to %q", val)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
if err := GitCredentialSetup("example.com", "monalisa", "PASSWD", ""); err != nil {
|
f := GitCredentialFlow{
|
||||||
|
Executable: "/path/to/gh",
|
||||||
|
helper: "",
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := f.gitCredentialSetup("example.com", "monalisa", "PASSWD"); err != nil {
|
||||||
t.Errorf("GitCredentialSetup() error = %v", err)
|
t.Errorf("GitCredentialSetup() error = %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ type LoginOptions struct {
|
||||||
Interactive bool
|
Interactive bool
|
||||||
Web bool
|
Web bool
|
||||||
Scopes []string
|
Scopes []string
|
||||||
|
Executable string
|
||||||
|
|
||||||
sshContext sshContext
|
sshContext sshContext
|
||||||
}
|
}
|
||||||
|
|
@ -56,7 +57,7 @@ func Login(opts *LoginOptions) error {
|
||||||
|
|
||||||
var additionalScopes []string
|
var additionalScopes []string
|
||||||
|
|
||||||
credentialFlow := &GitCredentialFlow{}
|
credentialFlow := &GitCredentialFlow{Executable: opts.Executable}
|
||||||
if opts.Interactive && gitProtocol == "https" {
|
if opts.Interactive && gitProtocol == "https" {
|
||||||
if err := credentialFlow.Prompt(hostname); err != nil {
|
if err := credentialFlow.Prompt(hostname); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,11 @@ func New(appVersion string) *cmdutil.Factory {
|
||||||
}
|
}
|
||||||
remotesFunc := rr.Resolver(hostOverride)
|
remotesFunc := rr.Resolver(hostOverride)
|
||||||
|
|
||||||
|
ghExecutable := "gh"
|
||||||
|
if exe, err := os.Executable(); err == nil {
|
||||||
|
ghExecutable = exe
|
||||||
|
}
|
||||||
|
|
||||||
return &cmdutil.Factory{
|
return &cmdutil.Factory{
|
||||||
IOStreams: io,
|
IOStreams: io,
|
||||||
Config: configFunc,
|
Config: configFunc,
|
||||||
|
|
@ -70,5 +75,6 @@ func New(appVersion string) *cmdutil.Factory {
|
||||||
}
|
}
|
||||||
return currentBranch, nil
|
return currentBranch, nil
|
||||||
},
|
},
|
||||||
|
Executable: ghExecutable,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,4 +16,7 @@ type Factory struct {
|
||||||
Remotes func() (context.Remotes, error)
|
Remotes func() (context.Remotes, error)
|
||||||
Config func() (config.Config, error)
|
Config func() (config.Config, error)
|
||||||
Branch func() (string, error)
|
Branch func() (string, error)
|
||||||
|
|
||||||
|
// Executable is the path to the currently invoked gh binary
|
||||||
|
Executable string
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue