diff options
author | Jesse Duffield <jessedduffield@gmail.com> | 2021-10-24 10:43:48 +1100 |
---|---|---|
committer | Jesse Duffield <jessedduffield@gmail.com> | 2021-10-30 18:26:06 +1100 |
commit | f704707d291387b2c1d7432c7700fb5398432f18 (patch) | |
tree | 4d9a54fbfb789af24f49e215f70b7bfe664f1616 /pkg/commands/oscommands | |
parent | 01d82749b17cd7a048d69c8386044d79548f7893 (diff) |
stream output from certain git commands in command log panel
Diffstat (limited to 'pkg/commands/oscommands')
-rw-r--r-- | pkg/commands/oscommands/exec_live.go | 153 | ||||
-rw-r--r-- | pkg/commands/oscommands/exec_live_default.go | 113 | ||||
-rw-r--r-- | pkg/commands/oscommands/exec_live_win.go | 59 | ||||
-rw-r--r-- | pkg/commands/oscommands/os.go | 33 |
4 files changed, 235 insertions, 123 deletions
diff --git a/pkg/commands/oscommands/exec_live.go b/pkg/commands/oscommands/exec_live.go new file mode 100644 index 000000000..6ca70a5ec --- /dev/null +++ b/pkg/commands/oscommands/exec_live.go @@ -0,0 +1,153 @@ +package oscommands + +import ( + "bufio" + "bytes" + "io" + "os/exec" + "regexp" + "strings" + "unicode/utf8" + + "github.com/go-errors/errors" + "github.com/jesseduffield/lazygit/pkg/utils" +) + +// DetectUnamePass 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 (c *OSCommand) DetectUnamePass(cmdObj ICmdObj, writer io.Writer, promptUserForCredential func(string) string) error { + ttyText := "" + errMessage := c.RunCommandWithOutputLive(cmdObj, writer, func(word string) string { + ttyText = ttyText + " " + word + + prompts := map[string]string{ + `.+'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 errMessage +} + +// Due to a lack of pty support on windows we have RunCommandWithOutputLiveWrapper being defined +// separate for windows and other OS's +func (c *OSCommand) RunCommandWithOutputLive(cmdObj ICmdObj, writer io.Writer, handleOutput func(string) string) error { + return RunCommandWithOutputLiveWrapper(c, cmdObj, writer, handleOutput) +} + +type cmdHandler struct { + stdoutPipe io.Reader + stdinPipe io.Writer + close func() error +} + +// RunCommandWithOutputLiveAux runs a command and return every word that gets written in stdout +// Output is a function that executes by every word that gets read by bufio +// As return of output you need to give a string that will be written to stdin +// NOTE: If the return data is empty it won't write anything to stdin +func RunCommandWithOutputLiveAux( + c *OSCommand, + cmdObj ICmdObj, + writer io.Writer, + // handleOutput takes a word from stdout and returns a string to be written to stdin. + // See DetectUnamePass 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 { + c.Log.WithField("command", cmdObj.ToString()).Info("RunCommand") + c.LogCommand(cmdObj.ToString(), true) + cmd := cmdObj.AddEnvVars("LANG=en_US.UTF-8", "LC_ALL=en_US.UTF-8").GetCmd() + + var stderr bytes.Buffer + cmd.Stderr = io.MultiWriter(writer, &stderr) + + handler, err := startCmd(cmd) + if err != nil { + return err + } + + defer func() { + if closeErr := handler.close(); closeErr != nil { + c.Log.Error(closeErr) + } + }() + + tr := io.TeeReader(handler.stdoutPipe, writer) + + go utils.Safe(func() { + scanner := bufio.NewScanner(tr) + 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() + if err != nil { + return errors.New(stderr.String()) + } + + return nil +} + +// 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 +func scanWordsWithNewLines(data []byte, atEOF bool) (advance int, token []byte, err error) { + start := 0 + for width := 0; start < len(data); start += width { + var r rune + r, width = utf8.DecodeRune(data[start:]) + if !isSpace(r) { + break + } + } + for width, i := 0, start; i < len(data); i += width { + var r rune + r, width = utf8.DecodeRune(data[i:]) + if isSpace(r) { + return i + width, data[start:i], nil + } + } + if atEOF && len(data) > start { + return len(data), data[start:], nil + } + return start, nil, nil +} + +// isSpace is also copied from the bufio package and has been modified to also captures new lines +// For specific comments about this function take a look at: bufio.isSpace +func isSpace(r rune) bool { + if r <= '\u00FF' { + switch r { + case ' ', '\t', '\v', '\f': + return true + case '\u0085', '\u00A0': + return true + } + return false + } + if '\u2000' <= r && r <= '\u200a' { + return true + } + switch r { + case '\u1680', '\u2028', '\u2029', '\u202f', '\u205f', '\u3000': + return true + } + return false +} diff --git a/pkg/commands/oscommands/exec_live_default.go b/pkg/commands/oscommands/exec_live_default.go index 95d5d09fb..827db9ed3 100644 --- a/pkg/commands/oscommands/exec_live_default.go +++ b/pkg/commands/oscommands/exec_live_default.go @@ -4,95 +4,34 @@ package oscommands import ( - "bufio" - "bytes" - "strings" - "unicode/utf8" - - "github.com/go-errors/errors" - "github.com/jesseduffield/lazygit/pkg/utils" + "io" + "os/exec" "github.com/creack/pty" ) -// RunCommandWithOutputLiveWrapper runs a command and return every word that gets written in stdout -// Output is a function that executes by every word that gets read by bufio -// As return of output you need to give a string that will be written to stdin -// NOTE: If the return data is empty it won't written anything to stdin -func RunCommandWithOutputLiveWrapper(c *OSCommand, cmdObj ICmdObj, output func(string) string) error { - c.Log.WithField("command", cmdObj.ToString()).Info("RunCommand") - c.LogCommand(cmdObj.ToString(), true) - cmd := cmdObj.AddEnvVars("LANG=en_US.UTF-8", "LC_ALL=en_US.UTF-8").GetCmd() - - var stderr bytes.Buffer - cmd.Stderr = &stderr - - ptmx, err := pty.Start(cmd) - - if err != nil { - return err - } - - go utils.Safe(func() { - scanner := bufio.NewScanner(ptmx) - scanner.Split(scanWordsWithNewLines) - for scanner.Scan() { - toOutput := strings.Trim(scanner.Text(), " ") - _, _ = ptmx.WriteString(output(toOutput)) - } - }) - - err = cmd.Wait() - ptmx.Close() - if err != nil { - return errors.New(stderr.String()) - } - - return nil -} - -// 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 -func scanWordsWithNewLines(data []byte, atEOF bool) (advance int, token []byte, err error) { - start := 0 - for width := 0; start < len(data); start += width { - var r rune - r, width = utf8.DecodeRune(data[start:]) - if !isSpace(r) { - break - } - } - for width, i := 0, start; i < len(data); i += width { - var r rune - r, width = utf8.DecodeRune(data[i:]) - if isSpace(r) { - return i + width, data[start:i], nil - } - } - if atEOF && len(data) > start { - return len(data), data[start:], nil - } - return start, nil, nil -} - -// isSpace is also copied from the bufio package and has been modified to also captures new lines -// For specific comments about this function take a look at: bufio.isSpace -func isSpace(r rune) bool { - if r <= '\u00FF' { - switch r { - case ' ', '\t', '\v', '\f': - return true - case '\u0085', '\u00A0': - return true - } - return false - } - if '\u2000' <= r && r <= '\u200a' { - return true - } - switch r { - case '\u1680', '\u2028', '\u2029', '\u202f', '\u205f', '\u3000': - return true - } - return false +func RunCommandWithOutputLiveWrapper( + c *OSCommand, + cmdObj ICmdObj, + writer io.Writer, + output func(string) string, +) error { + return RunCommandWithOutputLiveAux( + c, + cmdObj, + writer, + output, + func(cmd *exec.Cmd) (*cmdHandler, error) { + ptmx, err := pty.Start(cmd) + if err != nil { + return nil, err + } + + return &cmdHandler{ + stdoutPipe: ptmx, + stdinPipe: ptmx, + close: ptmx.Close, + }, nil + }, + ) } diff --git a/pkg/commands/oscommands/exec_live_win.go b/pkg/commands/oscommands/exec_live_win.go index aa17242e3..5b61e478b 100644 --- a/pkg/commands/oscommands/exec_live_win.go +++ b/pkg/commands/oscommands/exec_live_win.go @@ -3,8 +3,61 @@ package oscommands +import ( + "bytes" + "io" + "os/exec" + "sync" +) + +type Buffer struct { + b bytes.Buffer + m sync.Mutex +} + +func (b *Buffer) Read(p []byte) (n int, err error) { + b.m.Lock() + defer b.m.Unlock() + return b.b.Read(p) +} +func (b *Buffer) Write(p []byte) (n int, err error) { + b.m.Lock() + defer b.m.Unlock() + return b.b.Write(p) +} + // RunCommandWithOutputLiveWrapper 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 -func RunCommandWithOutputLiveWrapper(c *OSCommand, cmdObj ICmdObj, output func(string) string) error { - return c.RunCommand(cmdObj.ToString()) +// 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 RunCommandWithOutputLiveWrapper( + c *OSCommand, + cmdObj ICmdObj, + writer io.Writer, + output func(string) string, +) error { + return RunCommandWithOutputLiveAux( + c, + cmdObj, + writer, + output, + func(cmd *exec.Cmd) (*cmdHandler, error) { + stdoutReader, stdoutWriter := io.Pipe() + cmd.Stdout = stdoutWriter + + buf := &Buffer{} + cmd.Stdin = buf + + if err := cmd.Start(); err != nil { + return nil, err + } + + // 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 + // we pass a no-op function for that. + return &cmdHandler{ + stdoutPipe: stdoutReader, + stdinPipe: buf, + close: func() error { return nil }, + }, nil + }, + ) } diff --git a/pkg/commands/oscommands/os.go b/pkg/commands/oscommands/os.go index f0c9525a5..cb41edff4 100644 --- a/pkg/commands/oscommands/os.go +++ b/pkg/commands/oscommands/os.go @@ -7,7 +7,6 @@ import ( "os" "os/exec" "path/filepath" - "regexp" "strings" "sync" @@ -217,38 +216,6 @@ func (c *OSCommand) ShellCommandFromString(commandStr string) *exec.Cmd { return c.ExecutableFromString(shellCommand) } -// RunCommandWithOutputLive runs RunCommandWithOutputLiveWrapper -func (c *OSCommand) RunCommandWithOutputLive(cmdObj ICmdObj, output func(string) string) error { - return RunCommandWithOutputLiveWrapper(c, cmdObj, output) -} - -// DetectUnamePass 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 (c *OSCommand) DetectUnamePass(cmdObj ICmdObj, promptUserForCredential func(string) string) error { - ttyText := "" - errMessage := c.RunCommandWithOutputLive(cmdObj, func(word string) string { - ttyText = ttyText + " " + word - - prompts := map[string]string{ - `.+'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 errMessage -} - // RunCommand runs a command and just returns the error func (c *OSCommand) RunCommand(formatString string, formatArgs ...interface{}) error { _, err := c.RunCommandWithOutput(formatString, formatArgs...) |