summaryrefslogtreecommitdiffstats
path: root/pkg
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
parent01d82749b17cd7a048d69c8386044d79548f7893 (diff)
stream output from certain git commands in command log panel
Diffstat (limited to 'pkg')
-rw-r--r--pkg/app/app.go8
-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
-rw-r--r--pkg/gui/commit_message_panel.go2
-rw-r--r--pkg/gui/extras_panel.go31
-rw-r--r--pkg/gui/gpg.go51
-rw-r--r--pkg/gui/layout.go3
-rw-r--r--pkg/i18n/english.go4
15 files changed, 346 insertions, 152 deletions
diff --git a/pkg/app/app.go b/pkg/app/app.go
index bb7485cca..f24af6e58 100644
--- a/pkg/app/app.go
+++ b/pkg/app/app.go
@@ -127,7 +127,13 @@ func NewApp(config config.AppConfigurer, filterPath string) (*App, error) {
return app, err
}
- app.GitCommand, err = commands.NewGitCommand(app.Log, app.OSCommand, app.Tr, app.Config, git_config.NewStdCachedGitConfig(app.Log))
+ app.GitCommand, err = commands.NewGitCommand(
+ app.Log,
+ app.OSCommand,
+ app.Tr,
+ app.Config,
+ git_config.NewStdCachedGitConfig(app.Log),
+ )
if err != nil {
return app, err
}
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)
}
diff --git a/pkg/gui/commit_message_panel.go b/pkg/gui/commit_message_panel.go
index 7ed9669ab..edc86eff7 100644
--- a/pkg/gui/commit_message_panel.go
+++ b/pkg/gui/commit_message_panel.go
@@ -22,9 +22,9 @@ func (gui *Gui) handleCommitConfirm() error {
cmdStr := gui.GitCommand.CommitCmdStr(message, flags)
gui.OnRunCommand(oscommands.NewCmdLogEntry(cmdStr, gui.Tr.Spans.Commit, true))
+ _ = gui.returnFromContext()
return gui.withGpgHandling(cmdStr, gui.Tr.CommittingStatus, func() error {
gui.Views.CommitMessage.ClearTextArea()
- _ = gui.returnFromContext()
return nil
})
}
diff --git a/pkg/gui/extras_panel.go b/pkg/gui/extras_panel.go
index ee106223e..7d68bb1ec 100644
--- a/pkg/gui/extras_panel.go
+++ b/pkg/gui/extras_panel.go
@@ -1,5 +1,11 @@
package gui
+import (
+ "io"
+
+ "github.com/jesseduffield/lazygit/pkg/gui/style"
+)
+
func (gui *Gui) handleCreateExtrasMenuPanel() error {
menuItems := []*menuItem{
{
@@ -48,3 +54,28 @@ func (gui *Gui) scrollDownExtra() error {
return nil
}
+
+func (gui *Gui) getCmdWriter() io.Writer {
+ return &prefixWriter{writer: gui.Views.Extras, prefix: style.FgMagenta.Sprintf("\n\n%s\n", gui.Tr.GitOutput)}
+}
+
+// Ensures that the first write is preceded by writing a prefix.
+// This allows us to say 'Git output:' before writing the actual git output.
+// We could just write directly to the view in this package before running the command but we already have code in the commands package that writes to the same view beforehand (with the command it's about to run) so things would be out of order.
+type prefixWriter struct {
+ prefix string
+ prefixWritten bool
+ writer io.Writer
+}
+
+func (self *prefixWriter) Write(p []byte) (n int, err error) {
+ if !self.prefixWritten {
+ self.prefixWritten = true
+ // assuming we can write this prefix in one go
+ _, err = self.writer.Write([]byte(self.prefix))
+ if err != nil {
+ return
+ }
+ }
+ return self.writer.Write(p)
+}
diff --git a/pkg/gui/gpg.go b/pkg/gui/gpg.go
index e172f3720..1811e4fb5 100644
--- a/pkg/gui/gpg.go
+++ b/pkg/gui/gpg.go
@@ -1,5 +1,11 @@
package gui
+import (
+ "fmt"
+
+ "github.com/jesseduffield/lazygit/pkg/gui/style"
+)
+
// Currently there is a bug where if we switch to a subprocess from within
// WithWaitingStatus we get stuck there and can't return to lazygit. We could
// fix this bug, or just stop running subprocesses from within there, given that
@@ -19,23 +25,38 @@ func (gui *Gui) withGpgHandling(cmdStr string, waitingStatus string, onSuccess f
return err
}
- if err != nil {
- return err
- }
+ return err
} else {
- return gui.WithWaitingStatus(waitingStatus, func() error {
- err := gui.OSCommand.RunCommand(cmdStr)
- if err != nil {
- return err
- } else if onSuccess != nil {
- if err := onSuccess(); err != nil {
- return err
- }
+ return gui.RunAndStream(cmdStr, waitingStatus, onSuccess)
+ }
+}
+
+func (gui *Gui) RunAndStream(cmdStr string, waitingStatus string, onSuccess func() error) error {
+ return gui.WithWaitingStatus(waitingStatus, func() error {
+ cmd := gui.OSCommand.ShellCommandFromString(cmdStr)
+ cmd.Env = append(cmd.Env, "TERM=dumb")
+ cmdWriter := gui.getCmdWriter()
+ cmd.Stdout = cmdWriter
+ cmd.Stderr = cmdWriter
+
+ if err := cmd.Run(); err != nil {
+ if _, err := cmd.Stdout.Write([]byte(fmt.Sprintf("%s\n", style.FgRed.Sprint(err.Error())))); err != nil {
+ gui.Log.Error(err)
}
+ _ = gui.refreshSidePanels(refreshOptions{mode: ASYNC})
+ return gui.surfaceError(
+ fmt.Errorf(
+ gui.Tr.GitCommandFailed, gui.Config.GetUserConfig().Keybinding.Universal.ExtrasMenu,
+ ),
+ )
+ }
- return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
- })
- }
+ if onSuccess != nil {
+ if err := onSuccess(); err != nil {
+ return err
+ }
+ }
- return nil
+ return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
+ })
}
diff --git a/pkg/gui/layout.go b/pkg/gui/layout.go
index a7f308b7b..0c43c3f34 100644
--- a/pkg/gui/layout.go
+++ b/pkg/gui/layout.go
@@ -123,12 +123,15 @@ func (gui *Gui) createAllViews() error {
gui.Views.Extras.FgColor = theme.GocuiDefaultTextColor
gui.Views.Extras.Autoscroll = true
gui.Views.Extras.Wrap = true
+
gui.printCommandLogHeader()
if _, err := gui.g.SetCurrentView(gui.defaultSideContext().GetViewName()); err != nil {
return err
}
+ gui.GitCommand.GetCmdWriter = gui.getCmdWriter
+
return nil
}
diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go
index 2f72c151c..ae5fab1b3 100644
--- a/pkg/i18n/english.go
+++ b/pkg/i18n/english.go
@@ -434,6 +434,8 @@ type TranslationSet struct {
NoConfigFileFoundErr string
LcLoadingFileSuggestions string
MustSpecifyOriginError string
+ GitOutput string
+ GitCommandFailed string
Spans Spans
}
@@ -961,6 +963,8 @@ func englishTranslationSet() TranslationSet {
NoConfigFileFoundErr: "No config file found",
LcLoadingFileSuggestions: "loading file suggestions",
MustSpecifyOriginError: "Must specify a remote if specifying a branch",
+ GitOutput: "Git output:",
+ GitCommandFailed: "Git command failed. Check command log for details (open with %s)",
Spans: Spans{
// TODO: combine this with the original keybinding descriptions (those are all in lowercase atm)
CheckoutCommit: "Checkout commit",