cli/pkg/cmd/auth/shared/gitcredentials/gitcredentials.go
2024-05-16 13:15:26 +02:00

162 lines
3.3 KiB
Go

package gitcredentials
import (
"bytes"
"context"
"fmt"
"path/filepath"
"strings"
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/git"
"github.com/cli/cli/v2/internal/ghinstance"
"github.com/google/shlex"
)
type HelperConfig struct {
SelfExecutablePath string
GitClient *git.Client
}
func (hc *HelperConfig) Configure(hostname string) error {
ctx := context.TODO()
credHelperKeys := []string{
keyFor(hostname),
}
gistHost := strings.TrimSuffix(ghinstance.GistHost(hostname), "/")
if strings.HasPrefix(gistHost, "gist.") {
credHelperKeys = append(credHelperKeys, keyFor(gistHost))
}
var configErr error
for _, credHelperKey := range credHelperKeys {
if configErr != nil {
break
}
// first use a blank value to indicate to git we want to sever the chain of credential helpers
preConfigureCmd, err := hc.GitClient.Command(ctx, "config", "--global", "--replace-all", credHelperKey, "")
if err != nil {
configErr = err
break
}
if _, err = preConfigureCmd.Output(); err != nil {
configErr = err
break
}
// second configure the actual helper for this host
configureCmd, err := hc.GitClient.Command(ctx,
"config", "--global", "--add",
credHelperKey,
fmt.Sprintf("!%s auth git-credential", shellQuote(hc.SelfExecutablePath)),
)
if err != nil {
configErr = err
} else {
_, configErr = configureCmd.Output()
}
}
return configErr
}
type Helper struct {
Cmd string
}
func (h Helper) IsConfigured() bool {
return h.Cmd != ""
}
func (h Helper) IsOurs() bool {
if !strings.HasPrefix(h.Cmd, "!") {
return false
}
args, err := shlex.Split(h.Cmd[1:])
if err != nil || len(args) == 0 {
return false
}
return strings.TrimSuffix(filepath.Base(args[0]), ".exe") == "gh"
}
func (hc *HelperConfig) ConfiguredHelper(hostname string) (Helper, error) {
ctx := context.TODO()
hostHelperCmd, err := hc.GitClient.Config(ctx, keyFor(hostname))
if hostHelperCmd != "" {
// TODO: This is a direct refactoring removing named and naked returns
// but we should probably look closer at the error handling here
return Helper{
Cmd: hostHelperCmd,
}, err
}
globalHelperCmd, err := hc.GitClient.Config(ctx, "credential.helper")
if globalHelperCmd != "" {
return Helper{
Cmd: globalHelperCmd,
}, err
}
return Helper{}, nil
}
func keyFor(hostname string) string {
host := strings.TrimSuffix(ghinstance.HostPrefix(hostname), "/")
return fmt.Sprintf("credential.%s.helper", host)
}
func shellQuote(s string) string {
if strings.ContainsAny(s, " $\\") {
return "'" + s + "'"
}
return s
}
type Updater struct {
GitClient *git.Client
}
func (u *Updater) Update(hostname, username, password string) error {
ctx := context.TODO()
// clear previous cached credentials
rejectCmd, err := u.GitClient.Command(ctx, "credential", "reject")
if err != nil {
return err
}
rejectCmd.Stdin = bytes.NewBufferString(heredoc.Docf(`
protocol=https
host=%s
`, hostname))
_, err = rejectCmd.Output()
if err != nil {
return err
}
approveCmd, err := u.GitClient.Command(ctx, "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 = approveCmd.Output()
if err != nil {
return err
}
return nil
}