If git credential helper is non-defined, set gh as credential helper

This commit is contained in:
Mislav Marohnić 2020-11-23 20:19:18 +01:00
parent e36c9029d3
commit d56d92c908
4 changed files with 196 additions and 23 deletions

View file

@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"sort"
"strings"
"github.com/cli/cli/internal/ghinstance"
"gopkg.in/yaml.v3"
@ -378,7 +379,7 @@ func (c *fileConfig) configForHost(hostname string) (*HostConfig, error) {
}
for _, hc := range hosts {
if hc.Host == hostname {
if strings.EqualFold(hc.Host, hostname) {
return hc, nil
}
}

View file

@ -1,6 +1,7 @@
package auth
import (
gitCredentialCmd "github.com/cli/cli/pkg/cmd/auth/gitcredential"
authLoginCmd "github.com/cli/cli/pkg/cmd/auth/login"
authLogoutCmd "github.com/cli/cli/pkg/cmd/auth/logout"
authRefreshCmd "github.com/cli/cli/pkg/cmd/auth/refresh"
@ -22,6 +23,7 @@ func NewCmdAuth(f *cmdutil.Factory) *cobra.Command {
cmd.AddCommand(authLogoutCmd.NewCmdLogout(f, nil))
cmd.AddCommand(authStatusCmd.NewCmdStatus(f, nil))
cmd.AddCommand(authRefreshCmd.NewCmdRefresh(f, nil))
cmd.AddCommand(gitCredentialCmd.NewCmdCredential(f, nil))
return cmd
}

View file

@ -0,0 +1,112 @@
package login
import (
"bufio"
"fmt"
"net/url"
"strings"
"github.com/cli/cli/internal/config"
"github.com/cli/cli/pkg/cmdutil"
"github.com/cli/cli/pkg/iostreams"
"github.com/spf13/cobra"
)
type CredentialOptions struct {
IO *iostreams.IOStreams
Config func() (config.Config, error)
Operation string
}
func NewCmdCredential(f *cmdutil.Factory, runF func(*CredentialOptions) error) *cobra.Command {
opts := &CredentialOptions{
IO: f.IOStreams,
Config: f.Config,
}
cmd := &cobra.Command{
Use: "git-credential",
Args: cobra.ExactArgs(1),
Short: "Implements git credential helper protocol",
Hidden: true,
RunE: func(cmd *cobra.Command, args []string) error {
opts.Operation = args[0]
if runF != nil {
return runF(opts)
}
return helperRun(opts)
},
}
return cmd
}
func helperRun(opts *CredentialOptions) error {
if opts.Operation == "store" {
// We pretend to implement the "store" operation, but do nothing since we already have a cached token.
return cmdutil.SilentError
}
if opts.Operation != "get" {
return fmt.Errorf("gh auth git-credential: %q operation not supported", opts.Operation)
}
wants := map[string]string{}
s := bufio.NewScanner(opts.IO.In)
for s.Scan() {
line := s.Text()
if line == "" {
break
}
parts := strings.SplitN(line, "=", 2)
if len(parts) < 2 {
continue
}
wants[parts[0]] = parts[1]
}
if err := s.Err(); err != nil {
return err
}
if uv := wants["url"]; uv != "" {
u, err := url.Parse(uv)
if err != nil {
return err
}
wants["protocol"] = u.Scheme
wants["host"] = u.Host
wants["path"] = u.Path
wants["username"] = u.User.Username()
wants["password"], _ = u.User.Password()
}
if wants["protocol"] != "https" {
return cmdutil.SilentError
}
cfg, err := opts.Config()
if err != nil {
return err
}
gotUser, _ := cfg.Get(wants["host"], "user")
gotToken, _ := cfg.Get(wants["host"], "oauth_token")
if gotUser == "" || gotToken == "" {
return cmdutil.SilentError
}
if wants["username"] != "" && !strings.EqualFold(wants["username"], gotUser) {
return cmdutil.SilentError
}
fmt.Fprint(opts.IO.Out, "protocol=https\n")
fmt.Fprintf(opts.IO.Out, "host=%s\n", wants["host"])
fmt.Fprintf(opts.IO.Out, "username=%s\n", gotUser)
fmt.Fprintf(opts.IO.Out, "password=%s\n", gotToken)
return nil
}

View file

@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"io/ioutil"
"path/filepath"
"strings"
"github.com/AlecAivazis/survey/v2"
@ -19,6 +20,7 @@ import (
"github.com/cli/cli/pkg/cmdutil"
"github.com/cli/cli/pkg/iostreams"
"github.com/cli/cli/pkg/prompt"
"github.com/google/shlex"
"github.com/spf13/cobra"
)
@ -314,32 +316,62 @@ func loginRun(opts *LoginOptions) error {
}
if opts.Interactive && gitProtocol == "https" {
var primeCredentials bool
err = prompt.SurveyAskOne(&survey.Confirm{
Message: "Set up git for passwordless push/pull operations?",
Default: true,
}, &primeCredentials)
if err != nil {
return fmt.Errorf("could not prompt: %w", err)
}
if primeCredentials {
gitCredential, err := git.GitCommand("credential", "approve")
helper, _ := gitCredentialHelper(hostname)
if !isOurCredentialHelper(helper) {
var primeCredentials bool
err = prompt.SurveyAskOne(&survey.Confirm{
Message: "Set up git for passwordless push/pull operations?",
Default: true,
}, &primeCredentials)
if err != nil {
return err
return fmt.Errorf("could not prompt: %w", err)
}
credentialStdin := &bytes.Buffer{}
gitCredential.Stdin = credentialStdin
password, _ := cfg.Get(hostname, "oauth_token")
fmt.Fprint(credentialStdin, "protocol=https\n")
fmt.Fprintf(credentialStdin, "host=%s\n", hostname)
fmt.Fprintf(credentialStdin, "username=%s\n", username)
fmt.Fprintf(credentialStdin, "password=%s\n", password)
fmt.Fprint(credentialStdin, "\n")
if primeCredentials {
if helper == "" {
configureCmd, err := git.GitCommand("config", "--global", gitCredentialHelperKey(hostname), "!gh auth git-credential")
if err != nil {
return err
}
err = run.PrepareCmd(gitCredential).Run()
if err != nil {
return err
err = run.PrepareCmd(configureCmd).Run()
if err != nil {
return err
}
} else {
rejectCmd, err := git.GitCommand("credential", "reject")
if err != nil {
return err
}
rejectCmd.Stdin = bytes.NewBufferString(fmt.Sprintf(heredoc.Doc(`
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
}
password, _ := cfg.Get(hostname, "oauth_token")
approveCmd.Stdin = bytes.NewBufferString(fmt.Sprintf(heredoc.Doc(`
protocol=https
host=%s
username=%s
password=%s
`), hostname, username, password))
err = run.PrepareCmd(approveCmd).Run()
if err != nil {
return err
}
}
}
}
}
@ -358,3 +390,29 @@ func getAccessTokenTip(hostname string) string {
Tip: you can generate a Personal Access Token here https://%s/settings/tokens
The minimum required scopes are 'repo' and 'read:org'.`, ghHostname)
}
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"
}