mirror of
https://github.com/jesseduffield/lazygit.git
synced 2025-07-30 03:23:08 +03:00
stream output from certain git commands in command log panel
This commit is contained in:
@ -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 },
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
153
pkg/commands/oscommands/exec_live.go
Normal file
153
pkg/commands/oscommands/exec_live.go
Normal file
@ -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
|
||||
}
|
@ -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()
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
return &cmdHandler{
|
||||
stdoutPipe: ptmx,
|
||||
stdinPipe: ptmx,
|
||||
close: ptmx.Close,
|
||||
}, nil
|
||||
},
|
||||
)
|
||||
}
|
||||
|
@ -3,8 +3,61 @@
|
||||
|
||||
package oscommands
|
||||
|
||||
// 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())
|
||||
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. 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
|
||||
},
|
||||
)
|
||||
}
|
||||
|
@ -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...)
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
Reference in New Issue
Block a user