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'
148 lines
3.2 KiB
Go
148 lines
3.2 KiB
Go
package shared
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/AlecAivazis/survey/v2"
|
|
"github.com/MakeNowJust/heredoc"
|
|
"github.com/cli/cli/git"
|
|
"github.com/cli/cli/internal/run"
|
|
"github.com/cli/cli/pkg/prompt"
|
|
"github.com/google/shlex"
|
|
)
|
|
|
|
type GitCredentialFlow struct {
|
|
Executable string
|
|
|
|
shouldSetup bool
|
|
helper string
|
|
scopes []string
|
|
}
|
|
|
|
func (flow *GitCredentialFlow) Prompt(hostname string) error {
|
|
flow.helper, _ = gitCredentialHelper(hostname)
|
|
if isOurCredentialHelper(flow.helper) {
|
|
flow.scopes = append(flow.scopes, "workflow")
|
|
return nil
|
|
}
|
|
|
|
err := prompt.SurveyAskOne(&survey.Confirm{
|
|
Message: "Authenticate Git with your GitHub credentials?",
|
|
Default: true,
|
|
}, &flow.shouldSetup)
|
|
if err != nil {
|
|
return fmt.Errorf("could not prompt: %w", err)
|
|
}
|
|
if flow.shouldSetup {
|
|
flow.scopes = append(flow.scopes, "workflow")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (flow *GitCredentialFlow) Scopes() []string {
|
|
return flow.scopes
|
|
}
|
|
|
|
func (flow *GitCredentialFlow) ShouldSetup() bool {
|
|
return flow.shouldSetup
|
|
}
|
|
|
|
func (flow *GitCredentialFlow) Setup(hostname, username, authToken string) error {
|
|
return flow.gitCredentialSetup(hostname, username, authToken)
|
|
}
|
|
|
|
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), "")
|
|
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)
|
|
configureCmd, err := git.GitCommand(
|
|
"config", "--global", "--add",
|
|
gitCredentialHelperKey(hostname),
|
|
fmt.Sprintf("!%s auth git-credential", shellQuote(flow.Executable)),
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return run.PrepareCmd(configureCmd).Run()
|
|
}
|
|
|
|
// clear previous cached credentials
|
|
rejectCmd, err := git.GitCommand("credential", "reject")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
rejectCmd.Stdin = bytes.NewBufferString(heredoc.Docf(`
|
|
protocol=https
|
|
host=%s
|
|
`, hostname))
|
|
|
|
err = run.PrepareCmd(rejectCmd).Run()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
approveCmd, err := git.GitCommand("credential", "approve")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
approveCmd.Stdin = bytes.NewBufferString(heredoc.Docf(`
|
|
protocol=https
|
|
host=%s
|
|
username=%s
|
|
password=%s
|
|
`, hostname, username, password))
|
|
|
|
err = run.PrepareCmd(approveCmd).Run()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func gitCredentialHelperKey(hostname string) string {
|
|
return fmt.Sprintf("credential.https://%s.helper", hostname)
|
|
}
|
|
|
|
func gitCredentialHelper(hostname string) (helper string, err error) {
|
|
helper, err = git.Config(gitCredentialHelperKey(hostname))
|
|
if helper != "" {
|
|
return
|
|
}
|
|
helper, err = git.Config("credential.helper")
|
|
return
|
|
}
|
|
|
|
func isOurCredentialHelper(cmd string) bool {
|
|
if !strings.HasPrefix(cmd, "!") {
|
|
return false
|
|
}
|
|
|
|
args, err := shlex.Split(cmd[1:])
|
|
if err != nil || len(args) == 0 {
|
|
return false
|
|
}
|
|
|
|
return strings.TrimSuffix(filepath.Base(args[0]), ".exe") == "gh"
|
|
}
|
|
|
|
func shellQuote(s string) string {
|
|
if strings.ContainsAny(s, " $") {
|
|
return "'" + s + "'"
|
|
}
|
|
return s
|
|
}
|