github.com/cli/cli@v1.14.1-0.20210902173923-1af6a669e342/pkg/cmd/auth/shared/git_credential.go (about)

     1  package shared
     2  
     3  import (
     4  	"bytes"
     5  	"errors"
     6  	"fmt"
     7  	"path/filepath"
     8  	"strings"
     9  
    10  	"github.com/AlecAivazis/survey/v2"
    11  	"github.com/MakeNowJust/heredoc"
    12  	"github.com/cli/cli/git"
    13  	"github.com/cli/cli/internal/run"
    14  	"github.com/cli/cli/pkg/prompt"
    15  	"github.com/google/shlex"
    16  )
    17  
    18  type GitCredentialFlow struct {
    19  	Executable string
    20  
    21  	shouldSetup bool
    22  	helper      string
    23  	scopes      []string
    24  }
    25  
    26  func (flow *GitCredentialFlow) Prompt(hostname string) error {
    27  	var gitErr error
    28  	flow.helper, gitErr = gitCredentialHelper(hostname)
    29  	if isOurCredentialHelper(flow.helper) {
    30  		flow.scopes = append(flow.scopes, "workflow")
    31  		return nil
    32  	}
    33  
    34  	err := prompt.SurveyAskOne(&survey.Confirm{
    35  		Message: "Authenticate Git with your GitHub credentials?",
    36  		Default: true,
    37  	}, &flow.shouldSetup)
    38  	if err != nil {
    39  		return fmt.Errorf("could not prompt: %w", err)
    40  	}
    41  	if flow.shouldSetup {
    42  		if isGitMissing(gitErr) {
    43  			return gitErr
    44  		}
    45  		flow.scopes = append(flow.scopes, "workflow")
    46  	}
    47  
    48  	return nil
    49  }
    50  
    51  func (flow *GitCredentialFlow) Scopes() []string {
    52  	return flow.scopes
    53  }
    54  
    55  func (flow *GitCredentialFlow) ShouldSetup() bool {
    56  	return flow.shouldSetup
    57  }
    58  
    59  func (flow *GitCredentialFlow) Setup(hostname, username, authToken string) error {
    60  	return flow.gitCredentialSetup(hostname, username, authToken)
    61  }
    62  
    63  func (flow *GitCredentialFlow) gitCredentialSetup(hostname, username, password string) error {
    64  	if flow.helper == "" {
    65  		// first use a blank value to indicate to git we want to sever the chain of credential helpers
    66  		preConfigureCmd, err := git.GitCommand("config", "--global", gitCredentialHelperKey(hostname), "")
    67  		if err != nil {
    68  			return err
    69  		}
    70  		if err = run.PrepareCmd(preConfigureCmd).Run(); err != nil {
    71  			return err
    72  		}
    73  
    74  		// use GitHub CLI as a credential helper (for this host only)
    75  		configureCmd, err := git.GitCommand(
    76  			"config", "--global", "--add",
    77  			gitCredentialHelperKey(hostname),
    78  			fmt.Sprintf("!%s auth git-credential", shellQuote(flow.Executable)),
    79  		)
    80  		if err != nil {
    81  			return err
    82  		}
    83  		return run.PrepareCmd(configureCmd).Run()
    84  	}
    85  
    86  	// clear previous cached credentials
    87  	rejectCmd, err := git.GitCommand("credential", "reject")
    88  	if err != nil {
    89  		return err
    90  	}
    91  
    92  	rejectCmd.Stdin = bytes.NewBufferString(heredoc.Docf(`
    93  		protocol=https
    94  		host=%s
    95  	`, hostname))
    96  
    97  	err = run.PrepareCmd(rejectCmd).Run()
    98  	if err != nil {
    99  		return err
   100  	}
   101  
   102  	approveCmd, err := git.GitCommand("credential", "approve")
   103  	if err != nil {
   104  		return err
   105  	}
   106  
   107  	approveCmd.Stdin = bytes.NewBufferString(heredoc.Docf(`
   108  		protocol=https
   109  		host=%s
   110  		username=%s
   111  		password=%s
   112  	`, hostname, username, password))
   113  
   114  	err = run.PrepareCmd(approveCmd).Run()
   115  	if err != nil {
   116  		return err
   117  	}
   118  
   119  	return nil
   120  }
   121  
   122  func gitCredentialHelperKey(hostname string) string {
   123  	return fmt.Sprintf("credential.https://%s.helper", hostname)
   124  }
   125  
   126  func gitCredentialHelper(hostname string) (helper string, err error) {
   127  	helper, err = git.Config(gitCredentialHelperKey(hostname))
   128  	if helper != "" {
   129  		return
   130  	}
   131  	helper, err = git.Config("credential.helper")
   132  	return
   133  }
   134  
   135  func isOurCredentialHelper(cmd string) bool {
   136  	if !strings.HasPrefix(cmd, "!") {
   137  		return false
   138  	}
   139  
   140  	args, err := shlex.Split(cmd[1:])
   141  	if err != nil || len(args) == 0 {
   142  		return false
   143  	}
   144  
   145  	return strings.TrimSuffix(filepath.Base(args[0]), ".exe") == "gh"
   146  }
   147  
   148  func isGitMissing(err error) bool {
   149  	if err == nil {
   150  		return false
   151  	}
   152  	var errNotInstalled *git.NotInstalled
   153  	return errors.As(err, &errNotInstalled)
   154  }
   155  
   156  func shellQuote(s string) string {
   157  	if strings.ContainsAny(s, " $") {
   158  		return "'" + s + "'"
   159  	}
   160  	return s
   161  }