summaryrefslogtreecommitdiffstats
path: root/pkg/commands/oscommands
diff options
context:
space:
mode:
authorJesse Duffield <jessedduffield@gmail.com>2021-10-24 10:43:48 +1100
committerJesse Duffield <jessedduffield@gmail.com>2021-10-30 18:26:06 +1100
commitf704707d291387b2c1d7432c7700fb5398432f18 (patch)
tree4d9a54fbfb789af24f49e215f70b7bfe664f1616 /pkg/commands/oscommands
parent01d82749b17cd7a048d69c8386044d79548f7893 (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.go153
-rw-r--r--pkg/commands/oscommands/exec_live_default.go113
-rw-r--r--pkg/commands/oscommands/exec_live_win.go59
-rw-r--r--pkg/commands/oscommands/os.go33
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...)