mirror of
https://github.com/jesseduffield/lazygit.git
synced 2025-07-31 14:24:25 +03:00
refactor code for handling credential requests
This commit is contained in:
@ -4,7 +4,6 @@ import (
|
|||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
"io"
|
"io"
|
||||||
"os/exec"
|
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
@ -21,50 +20,20 @@ const (
|
|||||||
Passphrase
|
Passphrase
|
||||||
)
|
)
|
||||||
|
|
||||||
// RunAndDetectCredentialRequest detect a username / password / passphrase question in a command
|
|
||||||
// promptUserForCredential is a function that gets executed when this function detect you need to fillin a password or passphrase
|
|
||||||
// The promptUserForCredential argument will be "username", "password" or "passphrase" and expects the user's password/passphrase or username back
|
|
||||||
func (self *cmdObjRunner) RunAndDetectCredentialRequest(cmdObj ICmdObj, promptUserForCredential func(CredentialType) string) error {
|
|
||||||
ttyText := ""
|
|
||||||
err := self.RunCommandWithOutputLive(cmdObj, func(word string) string {
|
|
||||||
ttyText = ttyText + " " + word
|
|
||||||
|
|
||||||
prompts := map[string]CredentialType{
|
|
||||||
`.+'s password:`: Password,
|
|
||||||
`Password\s*for\s*'.+':`: Password,
|
|
||||||
`Username\s*for\s*'.+':`: Username,
|
|
||||||
`Enter\s*passphrase\s*for\s*key\s*'.+':`: Passphrase,
|
|
||||||
}
|
|
||||||
|
|
||||||
for pattern, askFor := range prompts {
|
|
||||||
if match, _ := regexp.MatchString(pattern, ttyText); match {
|
|
||||||
ttyText = ""
|
|
||||||
return promptUserForCredential(askFor)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ""
|
|
||||||
})
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
type cmdHandler struct {
|
type cmdHandler struct {
|
||||||
stdoutPipe io.Reader
|
stdoutPipe io.Reader
|
||||||
stdinPipe io.Writer
|
stdinPipe io.Writer
|
||||||
close func() error
|
close func() error
|
||||||
}
|
}
|
||||||
|
|
||||||
// RunCommandWithOutputLiveAux runs a command and return every word that gets written in stdout
|
// RunAndDetectCredentialRequest detect a username / password / passphrase question in a command
|
||||||
// Output is a function that executes by every word that gets read by bufio
|
// promptUserForCredential is a function that gets executed when this function detect you need to fillin a password or passphrase
|
||||||
// As return of output you need to give a string that will be written to stdin
|
// The promptUserForCredential argument will be "username", "password" or "passphrase" and expects the user's password/passphrase or username back
|
||||||
// NOTE: If the return data is empty it won't write anything to stdin
|
func (self *cmdObjRunner) RunAndDetectCredentialRequest(
|
||||||
func (self *cmdObjRunner) RunCommandWithOutputLiveAux(
|
|
||||||
cmdObj ICmdObj,
|
cmdObj ICmdObj,
|
||||||
// handleOutput takes a word from stdout and returns a string to be written to stdin.
|
promptUserForCredential func(CredentialType) string,
|
||||||
// See RunAndDetectCredentialRequest above for how this is used to check for a username/password request
|
|
||||||
handleOutput func(string) string,
|
|
||||||
startCmd func(cmd *exec.Cmd) (*cmdHandler, error),
|
|
||||||
) error {
|
) error {
|
||||||
|
self.log.Warn("HERE")
|
||||||
cmdWriter := self.guiIO.newCmdWriterFn()
|
cmdWriter := self.guiIO.newCmdWriterFn()
|
||||||
self.log.WithField("command", cmdObj.ToString()).Info("RunCommand")
|
self.log.WithField("command", cmdObj.ToString()).Info("RunCommand")
|
||||||
if cmdObj.ShouldLog() {
|
if cmdObj.ShouldLog() {
|
||||||
@ -75,7 +44,7 @@ func (self *cmdObjRunner) RunCommandWithOutputLiveAux(
|
|||||||
var stderr bytes.Buffer
|
var stderr bytes.Buffer
|
||||||
cmd.Stderr = io.MultiWriter(cmdWriter, &stderr)
|
cmd.Stderr = io.MultiWriter(cmdWriter, &stderr)
|
||||||
|
|
||||||
handler, err := startCmd(cmd)
|
handler, err := self.getCmdHandler(cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -89,16 +58,7 @@ func (self *cmdObjRunner) RunCommandWithOutputLiveAux(
|
|||||||
tr := io.TeeReader(handler.stdoutPipe, cmdWriter)
|
tr := io.TeeReader(handler.stdoutPipe, cmdWriter)
|
||||||
|
|
||||||
go utils.Safe(func() {
|
go utils.Safe(func() {
|
||||||
scanner := bufio.NewScanner(tr)
|
self.processOutput(tr, handler.stdinPipe, promptUserForCredential)
|
||||||
scanner.Split(scanWordsWithNewLines)
|
|
||||||
for scanner.Scan() {
|
|
||||||
text := scanner.Text()
|
|
||||||
output := strings.Trim(text, " ")
|
|
||||||
toInput := handleOutput(output)
|
|
||||||
if toInput != "" {
|
|
||||||
_, _ = handler.stdinPipe.Write([]byte(toInput))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
err = cmd.Wait()
|
err = cmd.Wait()
|
||||||
@ -109,6 +69,51 @@ func (self *cmdObjRunner) RunCommandWithOutputLiveAux(
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (self *cmdObjRunner) processOutput(reader io.Reader, writer io.Writer, promptUserForCredential func(CredentialType) string) {
|
||||||
|
checkForCredentialRequest := self.getCheckForCredentialRequestFunc()
|
||||||
|
|
||||||
|
scanner := bufio.NewScanner(reader)
|
||||||
|
scanner.Split(scanWordsWithNewLines)
|
||||||
|
for scanner.Scan() {
|
||||||
|
text := scanner.Text()
|
||||||
|
self.log.Info(text)
|
||||||
|
output := strings.Trim(text, " ")
|
||||||
|
askFor, ok := checkForCredentialRequest(output)
|
||||||
|
if ok {
|
||||||
|
toInput := promptUserForCredential(askFor)
|
||||||
|
// If the return data is empty we don't write anything to stdin
|
||||||
|
if toInput != "" {
|
||||||
|
_, _ = writer.Write([]byte(toInput))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// having a function that returns a function because we need to maintain some state inbetween calls hence the closure
|
||||||
|
func (self *cmdObjRunner) getCheckForCredentialRequestFunc() func(string) (CredentialType, bool) {
|
||||||
|
ttyText := ""
|
||||||
|
// this function takes each word of output from the command and builds up a string to see if we're being asked for a password
|
||||||
|
return func(word string) (CredentialType, bool) {
|
||||||
|
ttyText = ttyText + " " + word
|
||||||
|
|
||||||
|
prompts := map[string]CredentialType{
|
||||||
|
`.+'s password:`: Password,
|
||||||
|
`Password\s*for\s*'.+':`: Password,
|
||||||
|
`Username\s*for\s*'.+':`: Username,
|
||||||
|
`Enter\s*passphrase\s*for\s*key\s*'.+':`: Passphrase,
|
||||||
|
}
|
||||||
|
|
||||||
|
for pattern, askFor := range prompts {
|
||||||
|
if match, _ := regexp.MatchString(pattern, ttyText); match {
|
||||||
|
ttyText = ""
|
||||||
|
return askFor, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// scanWordsWithNewLines is a copy of bufio.ScanWords but this also captures new lines
|
// scanWordsWithNewLines is a copy of bufio.ScanWords but this also captures new lines
|
||||||
// For specific comments about this function take a look at: bufio.ScanWords
|
// For specific comments about this function take a look at: bufio.ScanWords
|
||||||
func scanWordsWithNewLines(data []byte, atEOF bool) (advance int, token []byte, err error) {
|
func scanWordsWithNewLines(data []byte, atEOF bool) (advance int, token []byte, err error) {
|
||||||
|
@ -11,24 +11,15 @@ import (
|
|||||||
|
|
||||||
// we define this separately for windows and non-windows given that windows does
|
// we define this separately for windows and non-windows given that windows does
|
||||||
// not have great PTY support and we need a PTY to handle a credential request
|
// not have great PTY support and we need a PTY to handle a credential request
|
||||||
func (self *cmdObjRunner) RunCommandWithOutputLive(
|
func (self *cmdObjRunner) getCmdHandler(cmd *exec.Cmd) (*cmdHandler, error) {
|
||||||
cmdObj ICmdObj,
|
ptmx, err := pty.Start(cmd)
|
||||||
output func(string) string,
|
if err != nil {
|
||||||
) error {
|
return nil, err
|
||||||
return self.RunCommandWithOutputLiveAux(
|
}
|
||||||
cmdObj,
|
|
||||||
output,
|
|
||||||
func(cmd *exec.Cmd) (*cmdHandler, error) {
|
|
||||||
ptmx, err := pty.Start(cmd)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &cmdHandler{
|
return &cmdHandler{
|
||||||
stdoutPipe: ptmx,
|
stdoutPipe: ptmx,
|
||||||
stdinPipe: ptmx,
|
stdinPipe: ptmx,
|
||||||
close: ptmx.Close,
|
close: ptmx.Close,
|
||||||
}, nil
|
}, nil
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
@ -26,34 +26,24 @@ func (b *Buffer) Write(p []byte) (n int, err error) {
|
|||||||
return b.b.Write(p)
|
return b.b.Write(p)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RunCommandWithOutputLive runs a command live but because of windows compatibility this command can't be ran there
|
|
||||||
// TODO: Remove this hack and replace it with a proper way to run commands live on windows. We still have an issue where if a password is requested, the request for a password is written straight to stdout because we can't control the stdout of a subprocess of a subprocess. Keep an eye on https://github.com/creack/pty/pull/109
|
// TODO: Remove this hack and replace it with a proper way to run commands live on windows. We still have an issue where if a password is requested, the request for a password is written straight to stdout because we can't control the stdout of a subprocess of a subprocess. Keep an eye on https://github.com/creack/pty/pull/109
|
||||||
func (self *cmdObjRunner) RunCommandWithOutputLive(
|
func (self *cmdObjRunner) getCmdHandler(cmd *exec.Cmd) (*cmdHandler, error) {
|
||||||
cmdObj ICmdObj,
|
stdoutReader, stdoutWriter := io.Pipe()
|
||||||
output func(string) string,
|
cmd.Stdout = stdoutWriter
|
||||||
) error {
|
|
||||||
return self.RunCommandWithOutputLiveAux(
|
|
||||||
cmdObj,
|
|
||||||
output,
|
|
||||||
func(cmd *exec.Cmd) (*cmdHandler, error) {
|
|
||||||
stdoutReader, stdoutWriter := io.Pipe()
|
|
||||||
cmd.Stdout = stdoutWriter
|
|
||||||
|
|
||||||
buf := &Buffer{}
|
buf := &Buffer{}
|
||||||
cmd.Stdin = buf
|
cmd.Stdin = buf
|
||||||
|
|
||||||
if err := cmd.Start(); err != nil {
|
if err := cmd.Start(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// because we don't yet have windows support for a pty, we instead just
|
// because we don't yet have windows support for a pty, we instead just
|
||||||
// pass our standard stream handlers and because there's no pty to close
|
// pass our standard stream handlers and because there's no pty to close
|
||||||
// we pass a no-op function for that.
|
// we pass a no-op function for that.
|
||||||
return &cmdHandler{
|
return &cmdHandler{
|
||||||
stdoutPipe: stdoutReader,
|
stdoutPipe: stdoutReader,
|
||||||
stdinPipe: buf,
|
stdinPipe: buf,
|
||||||
close: func() error { return nil },
|
close: func() error { return nil },
|
||||||
}, nil
|
}, nil
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user