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'
208 lines
5.1 KiB
Go
208 lines
5.1 KiB
Go
package shared
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/AlecAivazis/survey/v2"
|
|
"github.com/MakeNowJust/heredoc"
|
|
"github.com/cli/cli/api"
|
|
"github.com/cli/cli/internal/authflow"
|
|
"github.com/cli/cli/internal/ghinstance"
|
|
"github.com/cli/cli/pkg/iostreams"
|
|
"github.com/cli/cli/pkg/prompt"
|
|
)
|
|
|
|
type iconfig interface {
|
|
Get(string, string) (string, error)
|
|
Set(string, string, string) error
|
|
Write() error
|
|
}
|
|
|
|
type LoginOptions struct {
|
|
IO *iostreams.IOStreams
|
|
Config iconfig
|
|
HTTPClient *http.Client
|
|
Hostname string
|
|
Interactive bool
|
|
Web bool
|
|
Scopes []string
|
|
Executable string
|
|
|
|
sshContext sshContext
|
|
}
|
|
|
|
func Login(opts *LoginOptions) error {
|
|
cfg := opts.Config
|
|
hostname := opts.Hostname
|
|
httpClient := opts.HTTPClient
|
|
cs := opts.IO.ColorScheme()
|
|
|
|
var gitProtocol string
|
|
if opts.Interactive {
|
|
var proto string
|
|
err := prompt.SurveyAskOne(&survey.Select{
|
|
Message: "What is your preferred protocol for Git operations?",
|
|
Options: []string{
|
|
"HTTPS",
|
|
"SSH",
|
|
},
|
|
}, &proto)
|
|
if err != nil {
|
|
return fmt.Errorf("could not prompt: %w", err)
|
|
}
|
|
gitProtocol = strings.ToLower(proto)
|
|
}
|
|
|
|
var additionalScopes []string
|
|
|
|
credentialFlow := &GitCredentialFlow{Executable: opts.Executable}
|
|
if opts.Interactive && gitProtocol == "https" {
|
|
if err := credentialFlow.Prompt(hostname); err != nil {
|
|
return err
|
|
}
|
|
additionalScopes = append(additionalScopes, credentialFlow.Scopes()...)
|
|
}
|
|
|
|
var keyToUpload string
|
|
if opts.Interactive && gitProtocol == "ssh" {
|
|
pubKeys, err := opts.sshContext.localPublicKeys()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(pubKeys) > 0 {
|
|
var keyChoice int
|
|
err := prompt.SurveyAskOne(&survey.Select{
|
|
Message: "Upload your SSH public key to your GitHub account?",
|
|
Options: append(pubKeys, "Skip"),
|
|
}, &keyChoice)
|
|
if err != nil {
|
|
return fmt.Errorf("could not prompt: %w", err)
|
|
}
|
|
if keyChoice < len(pubKeys) {
|
|
keyToUpload = pubKeys[keyChoice]
|
|
}
|
|
} else {
|
|
var err error
|
|
keyToUpload, err = opts.sshContext.generateSSHKey()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
if keyToUpload != "" {
|
|
additionalScopes = append(additionalScopes, "admin:public_key")
|
|
}
|
|
|
|
var authMode int
|
|
if opts.Web {
|
|
authMode = 0
|
|
} else {
|
|
err := prompt.SurveyAskOne(&survey.Select{
|
|
Message: "How would you like to authenticate GitHub CLI?",
|
|
Options: []string{
|
|
"Login with a web browser",
|
|
"Paste an authentication token",
|
|
},
|
|
}, &authMode)
|
|
if err != nil {
|
|
return fmt.Errorf("could not prompt: %w", err)
|
|
}
|
|
}
|
|
|
|
var authToken string
|
|
userValidated := false
|
|
|
|
if authMode == 0 {
|
|
var err error
|
|
authToken, err = authflow.AuthFlowWithConfig(cfg, opts.IO, hostname, "", append(opts.Scopes, additionalScopes...))
|
|
if err != nil {
|
|
return fmt.Errorf("failed to authenticate via web browser: %w", err)
|
|
}
|
|
userValidated = true
|
|
} else {
|
|
minimumScopes := append([]string{"repo", "read:org"}, additionalScopes...)
|
|
fmt.Fprint(opts.IO.ErrOut, heredoc.Docf(`
|
|
Tip: you can generate a Personal Access Token here https://%s/settings/tokens
|
|
The minimum required scopes are %s.
|
|
`, hostname, scopesSentence(minimumScopes, ghinstance.IsEnterprise(hostname))))
|
|
|
|
err := prompt.SurveyAskOne(&survey.Password{
|
|
Message: "Paste your authentication token:",
|
|
}, &authToken, survey.WithValidator(survey.Required))
|
|
if err != nil {
|
|
return fmt.Errorf("could not prompt: %w", err)
|
|
}
|
|
|
|
if err := HasMinimumScopes(httpClient, hostname, authToken); err != nil {
|
|
return fmt.Errorf("error validating token: %w", err)
|
|
}
|
|
|
|
if err := cfg.Set(hostname, "oauth_token", authToken); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
var username string
|
|
if userValidated {
|
|
username, _ = cfg.Get(hostname, "user")
|
|
} else {
|
|
apiClient := api.NewClientFromHTTP(httpClient)
|
|
var err error
|
|
username, err = api.CurrentLoginName(apiClient, hostname)
|
|
if err != nil {
|
|
return fmt.Errorf("error using api: %w", err)
|
|
}
|
|
|
|
err = cfg.Set(hostname, "user", username)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if gitProtocol != "" {
|
|
fmt.Fprintf(opts.IO.ErrOut, "- gh config set -h %s git_protocol %s\n", hostname, gitProtocol)
|
|
err := cfg.Set(hostname, "git_protocol", gitProtocol)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fmt.Fprintf(opts.IO.ErrOut, "%s Configured git protocol\n", cs.SuccessIcon())
|
|
}
|
|
|
|
err := cfg.Write()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if credentialFlow.ShouldSetup() {
|
|
err := credentialFlow.Setup(hostname, username, authToken)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if keyToUpload != "" {
|
|
err := sshKeyUpload(httpClient, hostname, keyToUpload)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fmt.Fprintf(opts.IO.ErrOut, "%s Uploaded the SSH key to your GitHub account: %s\n", cs.SuccessIcon(), cs.Bold(keyToUpload))
|
|
}
|
|
|
|
fmt.Fprintf(opts.IO.ErrOut, "%s Logged in as %s\n", cs.SuccessIcon(), cs.Bold(username))
|
|
return nil
|
|
}
|
|
|
|
func scopesSentence(scopes []string, isEnterprise bool) string {
|
|
quoted := make([]string, len(scopes))
|
|
for i, s := range scopes {
|
|
quoted[i] = fmt.Sprintf("'%s'", s)
|
|
if s == "workflow" && isEnterprise {
|
|
// remove when GHE 2.x reaches EOL
|
|
quoted[i] += " (GHE 3.0+)"
|
|
}
|
|
}
|
|
return strings.Join(quoted, ", ")
|
|
}
|