mirror of
https://github.com/jesseduffield/lazygit.git
synced 2025-05-11 04:15:48 +02:00
Handle pending actions properly in git commands that require credentials
I don't know if this is a hack or not: we run a git command and increment the pending action count to 1 but at some point the command requests a username or password, so we need to prompt the user to enter that. At that point we don't want to say that there is a pending action, so we decrement the action count before prompting the user and then re-increment it again afterward. Given that we panic when the counter goes below zero, it's important that it's not zero when we run the git command (should be impossible anyway). I toyed with a different approach using channels and a long-running goroutine that handles all commands that request credentials but it feels over-engineered compared to this commit's approach.
This commit is contained in:
parent
6c4e7ee972
commit
26ca41a40e
5 changed files with 111 additions and 68 deletions
|
@ -19,15 +19,6 @@ type ICmdObjRunner interface {
|
|||
RunAndProcessLines(cmdObj ICmdObj, onLine func(line string) (bool, error)) error
|
||||
}
|
||||
|
||||
type CredentialType int
|
||||
|
||||
const (
|
||||
Password CredentialType = iota
|
||||
Username
|
||||
Passphrase
|
||||
PIN
|
||||
)
|
||||
|
||||
type cmdObjRunner struct {
|
||||
log *logrus.Entry
|
||||
guiIO *guiIO
|
||||
|
@ -182,26 +173,6 @@ 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 credential strategy")
|
||||
}
|
||||
|
||||
return self.runAndDetectCredentialRequest(cmdObj, promptFn)
|
||||
}
|
||||
|
||||
func (self *cmdObjRunner) logCmdObj(cmdObj ICmdObj) {
|
||||
self.guiIO.logCommandFn(cmdObj.ToString(), true)
|
||||
}
|
||||
|
@ -233,25 +204,6 @@ func (self *cmdObjRunner) runAndStream(cmdObj ICmdObj) 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 {
|
||||
// setting the output to english so we can parse it for a username/password request
|
||||
cmdObj.AddEnvVars("LANG=en_US.UTF-8", "LC_ALL=en_US.UTF-8")
|
||||
|
||||
return self.runAndStreamAux(cmdObj, func(handler *cmdHandler, cmdWriter io.Writer) {
|
||||
tr := io.TeeReader(handler.stdoutPipe, cmdWriter)
|
||||
|
||||
go utils.Safe(func() {
|
||||
self.processOutput(tr, handler.stdinPipe, promptUserForCredential)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func (self *cmdObjRunner) runAndStreamAux(
|
||||
cmdObj ICmdObj,
|
||||
onRun func(*cmdHandler, io.Writer),
|
||||
|
@ -302,7 +254,70 @@ func (self *cmdObjRunner) runAndStreamAux(
|
|||
return nil
|
||||
}
|
||||
|
||||
func (self *cmdObjRunner) processOutput(reader io.Reader, writer io.Writer, promptUserForCredential func(CredentialType) string) {
|
||||
type CredentialType int
|
||||
|
||||
const (
|
||||
Password CredentialType = iota
|
||||
Username
|
||||
Passphrase
|
||||
PIN
|
||||
)
|
||||
|
||||
// Whenever we're asked for a password we just enter a newline, which will
|
||||
// eventually cause the command to fail.
|
||||
var failPromptFn = func(CredentialType) <-chan string {
|
||||
ch := make(chan string)
|
||||
go func() {
|
||||
ch <- "\n"
|
||||
}()
|
||||
return ch
|
||||
}
|
||||
|
||||
func (self *cmdObjRunner) runWithCredentialHandling(cmdObj ICmdObj) error {
|
||||
promptFn, err := self.getCredentialPromptFn(cmdObj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return self.runAndDetectCredentialRequest(cmdObj, promptFn)
|
||||
}
|
||||
|
||||
func (self *cmdObjRunner) getCredentialPromptFn(cmdObj ICmdObj) (func(CredentialType) <-chan string, error) {
|
||||
switch cmdObj.GetCredentialStrategy() {
|
||||
case PROMPT:
|
||||
return self.guiIO.promptForCredentialFn, nil
|
||||
case FAIL:
|
||||
return failPromptFn, nil
|
||||
default:
|
||||
// we should never land here
|
||||
return nil, errors.New("runWithCredentialHandling called but cmdObj does not have a credential strategy")
|
||||
}
|
||||
}
|
||||
|
||||
// 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) <-chan string,
|
||||
) error {
|
||||
// setting the output to english so we can parse it for a username/password request
|
||||
cmdObj.AddEnvVars("LANG=en_US.UTF-8", "LC_ALL=en_US.UTF-8")
|
||||
|
||||
return self.runAndStreamAux(cmdObj, func(handler *cmdHandler, cmdWriter io.Writer) {
|
||||
tr := io.TeeReader(handler.stdoutPipe, cmdWriter)
|
||||
|
||||
go utils.Safe(func() {
|
||||
self.processOutput(tr, handler.stdinPipe, promptUserForCredential)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func (self *cmdObjRunner) processOutput(
|
||||
reader io.Reader,
|
||||
writer io.Writer,
|
||||
promptUserForCredential func(CredentialType) <-chan string,
|
||||
) {
|
||||
checkForCredentialRequest := self.getCheckForCredentialRequestFunc()
|
||||
|
||||
scanner := bufio.NewScanner(reader)
|
||||
|
@ -311,7 +326,14 @@ func (self *cmdObjRunner) processOutput(reader io.Reader, writer io.Writer, prom
|
|||
newBytes := scanner.Bytes()
|
||||
askFor, ok := checkForCredentialRequest(newBytes)
|
||||
if ok {
|
||||
toInput := promptUserForCredential(askFor)
|
||||
responseChan := promptUserForCredential(askFor)
|
||||
// We assume that the busy count is greater than zero here because we're
|
||||
// in the middle of a command. We decrement it so that The user can be prompted
|
||||
// without lazygit thinking it's still doing its own processing. This helps
|
||||
// integration tests know how long to wait before typing in a response.
|
||||
self.guiIO.DecrementBusyCount()
|
||||
toInput := <-responseChan
|
||||
self.guiIO.IncrementBusyCount()
|
||||
// If the return data is empty we don't write anything to stdin
|
||||
if toInput != "" {
|
||||
_, _ = writer.Write([]byte(toInput))
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue