1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2024-11-30 09:16:47 +02:00

more refactoring

This commit is contained in:
Jesse Duffield 2022-01-09 12:56:29 +11:00
parent 06687c8a59
commit a936c0592f
5 changed files with 135 additions and 135 deletions

View File

@ -2,6 +2,10 @@ package oscommands
import (
"bufio"
"bytes"
"io"
"regexp"
"strings"
"github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/utils"
@ -14,6 +18,14 @@ type ICmdObjRunner interface {
RunAndProcessLines(cmdObj ICmdObj, onLine func(line string) (bool, error)) error
}
type CredentialType int
const (
Password CredentialType = iota
Username
Passphrase
)
type cmdObjRunner struct {
log *logrus.Entry
guiIO *guiIO
@ -21,20 +33,6 @@ type cmdObjRunner struct {
var _ ICmdObjRunner = &cmdObjRunner{}
func (self *cmdObjRunner) runWithCredentialHandling(cmdObj ICmdObj) error {
switch cmdObj.GetCredentialStrategy() {
case PROMPT:
return self.RunAndDetectCredentialRequest(cmdObj, self.guiIO.promptForCredentialFn)
case FAIL:
return self.RunAndDetectCredentialRequest(cmdObj, func(CredentialType) string { return "\n" })
case NONE:
// we should never land here
return errors.New("runWithCredentialHandling called but cmdObj does not have a a credential strategy")
}
return errors.New("unexpected credential strategy")
}
func (self *cmdObjRunner) Run(cmdObj ICmdObj) error {
if cmdObj.GetCredentialStrategy() == NONE {
_, err := self.RunWithOutput(cmdObj)
@ -44,10 +42,6 @@ func (self *cmdObjRunner) Run(cmdObj ICmdObj) error {
}
}
func (self *cmdObjRunner) logCmdObj(cmdObj ICmdObj) {
self.guiIO.logCommandFn(cmdObj.ToString(), true)
}
func (self *cmdObjRunner) RunWithOutput(cmdObj ICmdObj) (string, error) {
if cmdObj.GetCredentialStrategy() != NONE {
err := self.runWithCredentialHandling(cmdObj)
@ -106,6 +100,30 @@ func (self *cmdObjRunner) RunAndProcessLines(cmdObj ICmdObj, onLine func(line st
return nil
}
// Whenever we're asked for a password we just enter a newline, which will
// eventually cause the command to fail.
var failPromptFn = func(CredentialType) string { return "\n" }
func (self *cmdObjRunner) runWithCredentialHandling(cmdObj ICmdObj) error {
var promptFn func(CredentialType) string
switch cmdObj.GetCredentialStrategy() {
case PROMPT:
promptFn = self.guiIO.promptForCredentialFn
case FAIL:
promptFn = failPromptFn
case NONE:
// we should never land here
return errors.New("runWithCredentialHandling called but cmdObj does not have a a credential strategy")
}
return self.runAndDetectCredentialRequest(cmdObj, promptFn)
}
func (self *cmdObjRunner) logCmdObj(cmdObj ICmdObj) {
self.guiIO.logCommandFn(cmdObj.ToString(), true)
}
func sanitisedCommandOutput(output []byte, err error) (string, error) {
outputString := string(output)
if err != nil {
@ -118,3 +136,99 @@ func sanitisedCommandOutput(output []byte, err error) (string, error) {
}
return outputString, nil
}
type cmdHandler struct {
stdoutPipe io.Reader
stdinPipe io.Writer
close func() error
}
// 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 {
cmdWriter := self.guiIO.newCmdWriterFn()
if cmdObj.ShouldLog() {
self.logCmdObj(cmdObj)
}
self.log.WithField("command", cmdObj.ToString()).Info("RunCommand")
cmd := cmdObj.AddEnvVars("LANG=en_US.UTF-8", "LC_ALL=en_US.UTF-8").GetCmd()
var stderr bytes.Buffer
cmd.Stderr = io.MultiWriter(cmdWriter, &stderr)
handler, err := self.getCmdHandler(cmd)
if err != nil {
return err
}
defer func() {
if closeErr := handler.close(); closeErr != nil {
self.log.Error(closeErr)
}
}()
tr := io.TeeReader(handler.stdoutPipe, cmdWriter)
go utils.Safe(func() {
self.processOutput(tr, handler.stdinPipe, promptUserForCredential)
})
err = cmd.Wait()
if err != nil {
return errors.New(stderr.String())
}
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(bufio.ScanBytes)
for scanner.Scan() {
newBytes := scanner.Bytes()
askFor, ok := checkForCredentialRequest(newBytes)
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([]byte) (CredentialType, bool) {
var ttyText strings.Builder
// 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(newBytes []byte) (CredentialType, bool) {
_, err := ttyText.Write(newBytes)
if err != nil {
self.log.Error(err)
}
prompts := map[string]CredentialType{
`Password:`: Password,
`.+'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.String()); match {
ttyText.Reset()
return askFor, true
}
}
return 0, false
}
}

View File

@ -1,116 +0,0 @@
package oscommands
import (
"bufio"
"bytes"
"io"
"regexp"
"strings"
"github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/utils"
)
type CredentialType int
const (
Password CredentialType = iota
Username
Passphrase
)
type cmdHandler struct {
stdoutPipe io.Reader
stdinPipe io.Writer
close func() error
}
// 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 {
self.log.Warn("HERE")
cmdWriter := self.guiIO.newCmdWriterFn()
self.log.WithField("command", cmdObj.ToString()).Info("RunCommand")
if cmdObj.ShouldLog() {
self.logCmdObj(cmdObj)
}
cmd := cmdObj.AddEnvVars("LANG=en_US.UTF-8", "LC_ALL=en_US.UTF-8").GetCmd()
var stderr bytes.Buffer
cmd.Stderr = io.MultiWriter(cmdWriter, &stderr)
handler, err := self.getCmdHandler(cmd)
if err != nil {
return err
}
defer func() {
if closeErr := handler.close(); closeErr != nil {
self.log.Error(closeErr)
}
}()
tr := io.TeeReader(handler.stdoutPipe, cmdWriter)
go utils.Safe(func() {
self.processOutput(tr, handler.stdinPipe, promptUserForCredential)
})
err = cmd.Wait()
if err != nil {
return errors.New(stderr.String())
}
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(bufio.ScanBytes)
for scanner.Scan() {
newBytes := scanner.Bytes()
askFor, ok := checkForCredentialRequest(newBytes)
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([]byte) (CredentialType, bool) {
var ttyText strings.Builder
// 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(newBytes []byte) (CredentialType, bool) {
_, err := ttyText.Write(newBytes)
if err != nil {
self.log.Error(err)
}
prompts := map[string]CredentialType{
`Password:`: Password,
`.+'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.String()); match {
ttyText.Reset()
return askFor, true
}
}
return 0, false
}
}

View File

@ -39,11 +39,13 @@ func NewGuiIO(log *logrus.Entry, logCommandFn func(string, bool), newCmdWriterFn
}
}
// we use this function when we want to access the functionality of our OS struct but we
// don't have anywhere to log things, or request input from the user.
func NewNullGuiIO(log *logrus.Entry) *guiIO {
return &guiIO{
log: log,
logCommandFn: func(string, bool) {},
newCmdWriterFn: func() io.Writer { return ioutil.Discard },
promptForCredentialFn: func(CredentialType) string { return "" },
promptForCredentialFn: failPromptFn,
}
}