summaryrefslogtreecommitdiffstats
path: root/pkg/commands
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
parent01d82749b17cd7a048d69c8386044d79548f7893 (diff)
stream output from certain git commands in command log panel
Diffstat (limited to 'pkg/commands')
-rw-r--r--pkg/commands/dummies.go14
-rw-r--r--pkg/commands/git.go7
-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
-rw-r--r--pkg/commands/remotes.go8
-rw-r--r--pkg/commands/sync.go10
-rw-r--r--pkg/commands/tags.go2
9 files changed, 264 insertions, 135 deletions
diff --git a/pkg/commands/dummies.go b/pkg/commands/dummies.go
index a8178385d..ba052fe9b 100644
--- a/pkg/commands/dummies.go
+++ b/pkg/commands/dummies.go
@@ -1,6 +1,9 @@
package commands
import (
+ "io"
+ "io/ioutil"
+
"github.com/jesseduffield/lazygit/pkg/commands/git_config"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/config"
@@ -17,10 +20,11 @@ func NewDummyGitCommand() *GitCommand {
func NewDummyGitCommandWithOSCommand(osCommand *oscommands.OSCommand) *GitCommand {
newAppConfig := config.NewDummyAppConfig()
return &GitCommand{
- Log: utils.NewDummyLog(),
- OSCommand: osCommand,
- Tr: i18n.NewTranslationSet(utils.NewDummyLog(), newAppConfig.GetUserConfig().Gui.Language),
- Config: newAppConfig,
- GitConfig: git_config.NewFakeGitConfig(map[string]string{}),
+ Log: utils.NewDummyLog(),
+ OSCommand: osCommand,
+ Tr: i18n.NewTranslationSet(utils.NewDummyLog(), newAppConfig.GetUserConfig().Gui.Language),
+ Config: newAppConfig,
+ GitConfig: git_config.NewFakeGitConfig(map[string]string{}),
+ GetCmdWriter: func() io.Writer { return ioutil.Discard },
}
}
diff --git a/pkg/commands/git.go b/pkg/commands/git.go
index a1a925de7..8750ecf51 100644
--- a/pkg/commands/git.go
+++ b/pkg/commands/git.go
@@ -1,6 +1,7 @@
package commands
import (
+ "io"
"io/ioutil"
"os"
"path/filepath"
@@ -40,6 +41,11 @@ type GitCommand struct {
// Push to current determines whether the user has configured to push to the remote branch of the same name as the current or not
PushToCurrent bool
+
+ // this is just a view that we write to when running certain commands.
+ // Coincidentally at the moment it's the same view that OnRunCommand logs to
+ // but that need not always be the case.
+ GetCmdWriter func() io.Writer
}
// NewGitCommand it runs git commands
@@ -77,6 +83,7 @@ func NewGitCommand(
DotGitDir: dotGitDir,
PushToCurrent: pushToCurrent,
GitConfig: gitConfig,
+ GetCmdWriter: func() io.Writer { return ioutil.Discard },
}
gitCommand.PatchManager = patch.NewPatchManager(log, gitCommand.ApplyPatch, gitCommand.ShowFileDiff)
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...)
diff --git a/pkg/commands/remotes.go b/pkg/commands/remotes.go
index 0839f9583..11e5dc26f 100644
--- a/pkg/commands/remotes.go
+++ b/pkg/commands/remotes.go
@@ -2,6 +2,8 @@ package commands
import (
"fmt"
+
+ "github.com/jesseduffield/lazygit/pkg/commands/oscommands"
)
func (c *GitCommand) AddRemote(name string, url string) error {
@@ -23,7 +25,11 @@ func (c *GitCommand) UpdateRemoteUrl(remoteName string, updatedUrl string) error
func (c *GitCommand) DeleteRemoteBranch(remoteName string, branchName string, promptUserForCredential func(string) string) error {
command := fmt.Sprintf("git push %s --delete %s", c.OSCommand.Quote(remoteName), c.OSCommand.Quote(branchName))
cmdObj := c.NewCmdObjFromStr(command)
- return c.OSCommand.DetectUnamePass(cmdObj, promptUserForCredential)
+ return c.DetectUnamePass(cmdObj, promptUserForCredential)
+}
+
+func (c *GitCommand) DetectUnamePass(cmdObj oscommands.ICmdObj, promptUserForCredential func(string) string) error {
+ return c.OSCommand.DetectUnamePass(cmdObj, c.GetCmdWriter(), promptUserForCredential)
}
// CheckRemoteBranchExists Returns remote branch
diff --git a/pkg/commands/sync.go b/pkg/commands/sync.go
index 3f94c5f4e..35f258f4b 100644
--- a/pkg/commands/sync.go
+++ b/pkg/commands/sync.go
@@ -38,7 +38,7 @@ func (c *GitCommand) Push(opts PushOpts) error {
}
cmdObj := c.NewCmdObjFromStr(cmdStr)
- return c.OSCommand.DetectUnamePass(cmdObj, opts.PromptUserForCredential)
+ return c.DetectUnamePass(cmdObj, opts.PromptUserForCredential)
}
type FetchOptions struct {
@@ -59,7 +59,7 @@ func (c *GitCommand) Fetch(opts FetchOptions) error {
}
cmdObj := c.NewCmdObjFromStr(cmdStr)
- return c.OSCommand.DetectUnamePass(cmdObj, func(question string) string {
+ return c.DetectUnamePass(cmdObj, func(question string) string {
if opts.PromptUserForCredential != nil {
return opts.PromptUserForCredential(question)
}
@@ -95,17 +95,17 @@ func (c *GitCommand) Pull(opts PullOptions) error {
// setting GIT_SEQUENCE_EDITOR to ':' as a way of skipping it, in case the user
// has 'pull.rebase = interactive' configured.
cmdObj := c.NewCmdObjFromStr(cmdStr).AddEnvVars("GIT_SEQUENCE_EDITOR=:")
- return c.OSCommand.DetectUnamePass(cmdObj, opts.PromptUserForCredential)
+ return c.DetectUnamePass(cmdObj, opts.PromptUserForCredential)
}
func (c *GitCommand) FastForward(branchName string, remoteName string, remoteBranchName string, promptUserForCredential func(string) string) error {
cmdStr := fmt.Sprintf("git fetch %s %s:%s", c.OSCommand.Quote(remoteName), c.OSCommand.Quote(remoteBranchName), c.OSCommand.Quote(branchName))
cmdObj := c.NewCmdObjFromStr(cmdStr)
- return c.OSCommand.DetectUnamePass(cmdObj, promptUserForCredential)
+ return c.DetectUnamePass(cmdObj, promptUserForCredential)
}
func (c *GitCommand) FetchRemote(remoteName string, promptUserForCredential func(string) string) error {
cmdStr := fmt.Sprintf("git fetch %s", c.OSCommand.Quote(remoteName))
cmdObj := c.NewCmdObjFromStr(cmdStr)
- return c.OSCommand.DetectUnamePass(cmdObj, promptUserForCredential)
+ return c.DetectUnamePass(cmdObj, promptUserForCredential)
}
diff --git a/pkg/commands/tags.go b/pkg/commands/tags.go
index af40ae603..9aa327dd3 100644
--- a/pkg/commands/tags.go
+++ b/pkg/commands/tags.go
@@ -15,5 +15,5 @@ func (c *GitCommand) DeleteTag(tagName string) error {
func (c *GitCommand) PushTag(remoteName string, tagName string, promptUserForCredential func(string) string) error {
cmdStr := fmt.Sprintf("git push %s %s", c.OSCommand.Quote(remoteName), c.OSCommand.Quote(tagName))
cmdObj := c.NewCmdObjFromStr(cmdStr)
- return c.OSCommand.DetectUnamePass(cmdObj, promptUserForCredential)
+ return c.DetectUnamePass(cmdObj, promptUserForCredential)
}