mirror of
https://github.com/jesseduffield/lazygit.git
synced 2025-07-28 16:02:01 +03:00
WIP
This commit is contained in:
@ -5,18 +5,27 @@ import (
|
||||
)
|
||||
|
||||
// A command object is a general way to represent a command to be run on the
|
||||
// command line. If you want to log the command you'll use .ToString() and
|
||||
// if you want to run it you'll use .GetCmd()
|
||||
// command line.
|
||||
type ICmdObj interface {
|
||||
GetCmd() *exec.Cmd
|
||||
ToString() string
|
||||
AddEnvVars(...string) ICmdObj
|
||||
GetEnvVars() []string
|
||||
|
||||
Run() error
|
||||
RunWithOutput() (string, error)
|
||||
RunLineOutputCmd(onLine func(line string) (bool, error)) error
|
||||
|
||||
// logs command
|
||||
Log() ICmdObj
|
||||
}
|
||||
|
||||
type CmdObj struct {
|
||||
cmdStr string
|
||||
cmd *exec.Cmd
|
||||
|
||||
runner ICmdObjRunner
|
||||
logCommand func(ICmdObj)
|
||||
}
|
||||
|
||||
func (self *CmdObj) GetCmd() *exec.Cmd {
|
||||
@ -36,3 +45,21 @@ func (self *CmdObj) AddEnvVars(vars ...string) ICmdObj {
|
||||
func (self *CmdObj) GetEnvVars() []string {
|
||||
return self.cmd.Env
|
||||
}
|
||||
|
||||
func (self *CmdObj) Log() ICmdObj {
|
||||
self.logCommand(self)
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
func (self *CmdObj) Run() error {
|
||||
return self.runner.Run(self)
|
||||
}
|
||||
|
||||
func (self *CmdObj) RunWithOutput() (string, error) {
|
||||
return self.runner.RunWithOutput(self)
|
||||
}
|
||||
|
||||
func (self *CmdObj) RunLineOutputCmd(onLine func(line string) (bool, error)) error {
|
||||
return self.runner.RunLineOutputCmd(self, onLine)
|
||||
}
|
||||
|
72
pkg/commands/oscommands/cmd_obj_builder.go
Normal file
72
pkg/commands/oscommands/cmd_obj_builder.go
Normal file
@ -0,0 +1,72 @@
|
||||
package oscommands
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/mgutz/str"
|
||||
)
|
||||
|
||||
type ICmdObjBuilder interface {
|
||||
// New returns a new command object based on the string provided
|
||||
New(cmdStr string) ICmdObj
|
||||
// NewShell takes a string like `git commit` and returns an executable shell command for it e.g. `sh -c 'git commit'`
|
||||
NewShell(commandStr string) ICmdObj
|
||||
// NewFromArgs takes a slice of strings like []string{"git", "commit"} and returns a new command object. This can be useful when you don't want to worry about whitespace and quoting and stuff.
|
||||
NewFromArgs(args []string) ICmdObj
|
||||
// Quote wraps a string in quotes with any necessary escaping applied. The reason for bundling this up with the other methods in this interface is that we basically always need to make use of this when creating new command objects.
|
||||
Quote(str string) string
|
||||
}
|
||||
|
||||
// poor man's version of explicitly saying that struct X implements interface Y
|
||||
var _ ICmdObjBuilder = &CmdObjBuilder{}
|
||||
|
||||
type CmdObjBuilder struct {
|
||||
runner ICmdObjRunner
|
||||
logCmdObj func(ICmdObj)
|
||||
// TODO: see if you can just remove this entirely and use secureexec.Command,
|
||||
// now that we're mocking out the runner itself.
|
||||
command func(string, ...string) *exec.Cmd
|
||||
platform *Platform
|
||||
}
|
||||
|
||||
func (self *CmdObjBuilder) New(cmdStr string) ICmdObj {
|
||||
args := str.ToArgv(cmdStr)
|
||||
cmd := self.command(args[0], args[1:]...)
|
||||
cmd.Env = os.Environ()
|
||||
|
||||
return &CmdObj{
|
||||
cmdStr: cmdStr,
|
||||
cmd: cmd,
|
||||
runner: self.runner,
|
||||
logCommand: self.logCmdObj,
|
||||
}
|
||||
}
|
||||
|
||||
func (self *CmdObjBuilder) NewFromArgs(args []string) ICmdObj {
|
||||
cmd := self.command(args[0], args[1:]...)
|
||||
cmd.Env = os.Environ()
|
||||
|
||||
return &CmdObj{
|
||||
cmdStr: strings.Join(args, " "),
|
||||
cmd: cmd,
|
||||
runner: self.runner,
|
||||
logCommand: self.logCmdObj,
|
||||
}
|
||||
}
|
||||
|
||||
func (self *CmdObjBuilder) NewShell(commandStr string) ICmdObj {
|
||||
return self.NewFromArgs([]string{self.platform.Shell, self.platform.ShellArg, commandStr})
|
||||
}
|
||||
|
||||
func (self *CmdObjBuilder) CloneWithNewRunner(decorate func(ICmdObjRunner) ICmdObjRunner) *CmdObjBuilder {
|
||||
decoratedRunner := decorate(self.runner)
|
||||
|
||||
return &CmdObjBuilder{
|
||||
runner: decoratedRunner,
|
||||
logCmdObj: self.logCmdObj,
|
||||
command: self.command,
|
||||
platform: self.platform,
|
||||
}
|
||||
}
|
79
pkg/commands/oscommands/cmd_obj_runner.go
Normal file
79
pkg/commands/oscommands/cmd_obj_runner.go
Normal file
@ -0,0 +1,79 @@
|
||||
package oscommands
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type ICmdObjRunner interface {
|
||||
Run(cmdObj ICmdObj) error
|
||||
RunWithOutput(cmdObj ICmdObj) (string, error)
|
||||
RunLineOutputCmd(cmdObj ICmdObj, onLine func(line string) (bool, error)) error
|
||||
}
|
||||
|
||||
type RunExpectation func(ICmdObj) (string, error)
|
||||
|
||||
type RealRunner struct {
|
||||
log *logrus.Entry
|
||||
logCmdObj func(ICmdObj)
|
||||
}
|
||||
|
||||
func (self *RealRunner) Run(cmdObj ICmdObj) error {
|
||||
_, err := self.RunWithOutput(cmdObj)
|
||||
return err
|
||||
}
|
||||
|
||||
func (self *RealRunner) RunWithOutput(cmdObj ICmdObj) (string, error) {
|
||||
self.logCmdObj(cmdObj)
|
||||
output, err := sanitisedCommandOutput(cmdObj.GetCmd().CombinedOutput())
|
||||
if err != nil {
|
||||
self.log.WithField("command", cmdObj.ToString()).Error(output)
|
||||
}
|
||||
return output, err
|
||||
}
|
||||
|
||||
func (self *RealRunner) RunLineOutputCmd(cmdObj ICmdObj, onLine func(line string) (bool, error)) error {
|
||||
cmd := cmdObj.GetCmd()
|
||||
stdoutPipe, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
scanner := bufio.NewScanner(stdoutPipe)
|
||||
scanner.Split(bufio.ScanLines)
|
||||
if err := cmd.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
stop, err := onLine(line)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if stop {
|
||||
_ = cmd.Process.Kill()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
_ = cmd.Wait()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func sanitisedCommandOutput(output []byte, err error) (string, error) {
|
||||
outputString := string(output)
|
||||
if err != nil {
|
||||
// errors like 'exit status 1' are not very useful so we'll create an error
|
||||
// from the combined output
|
||||
if outputString == "" {
|
||||
return "", utils.WrapError(err)
|
||||
}
|
||||
return outputString, errors.New(outputString)
|
||||
}
|
||||
return outputString, nil
|
||||
}
|
@ -1,7 +1,6 @@
|
||||
package oscommands
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
@ -16,7 +15,6 @@ import (
|
||||
"github.com/jesseduffield/lazygit/pkg/common"
|
||||
"github.com/jesseduffield/lazygit/pkg/secureexec"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/mgutz/str"
|
||||
)
|
||||
|
||||
// Platform stores the os state
|
||||
@ -55,7 +53,7 @@ type OSCommand struct {
|
||||
|
||||
removeFile func(string) error
|
||||
|
||||
IRunner
|
||||
Cmd *CmdObjBuilder
|
||||
}
|
||||
|
||||
// TODO: make these fields private
|
||||
@ -90,15 +88,20 @@ func NewCmdLogEntry(cmdStr string, span string, commandLine bool) CmdLogEntry {
|
||||
|
||||
// NewOSCommand os command runner
|
||||
func NewOSCommand(common *common.Common) *OSCommand {
|
||||
command := secureexec.Command
|
||||
platform := getPlatform()
|
||||
|
||||
c := &OSCommand{
|
||||
Common: common,
|
||||
Platform: getPlatform(),
|
||||
Command: secureexec.Command,
|
||||
Platform: platform,
|
||||
Command: command,
|
||||
Getenv: os.Getenv,
|
||||
removeFile: os.RemoveAll,
|
||||
}
|
||||
|
||||
c.IRunner = &RealRunner{c: c}
|
||||
runner := &RealRunner{log: common.Log, logCmdObj: c.LogCmdObj}
|
||||
c.Cmd = &CmdObjBuilder{runner: runner, command: command, logCmdObj: c.LogCmdObj, platform: platform}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
@ -162,8 +165,7 @@ func (c *OSCommand) OpenFile(filename string) error {
|
||||
"filename": c.Quote(filename),
|
||||
}
|
||||
command := utils.ResolvePlaceholderString(commandTemplate, templateValues)
|
||||
err := c.Run(c.NewShellCmdObjFromString(command))
|
||||
return err
|
||||
return c.Cmd.NewShell(command).Run()
|
||||
}
|
||||
|
||||
// OpenLink opens a file with the given
|
||||
@ -175,14 +177,17 @@ func (c *OSCommand) OpenLink(link string) error {
|
||||
}
|
||||
|
||||
command := utils.ResolvePlaceholderString(commandTemplate, templateValues)
|
||||
err := c.Run(c.NewShellCmdObjFromString(command))
|
||||
return err
|
||||
return c.Cmd.NewShell(command).Run()
|
||||
}
|
||||
|
||||
// Quote wraps a message in platform-specific quotation marks
|
||||
func (c *OSCommand) Quote(message string) string {
|
||||
return c.Cmd.Quote(message)
|
||||
}
|
||||
|
||||
func (self *CmdObjBuilder) Quote(message string) string {
|
||||
var quote string
|
||||
if c.Platform.OS == "windows" {
|
||||
if self.platform.OS == "windows" {
|
||||
quote = `\"`
|
||||
message = strings.NewReplacer(
|
||||
`"`, `"'"'"`,
|
||||
@ -289,7 +294,7 @@ func (c *OSCommand) PipeCommands(commandStrings ...string) error {
|
||||
logCmdStr += " | "
|
||||
}
|
||||
logCmdStr += str
|
||||
cmds[i] = c.NewCmdObj(str).GetCmd()
|
||||
cmds[i] = c.Cmd.New(str).GetCmd()
|
||||
}
|
||||
c.LogCommand(logCmdStr, true)
|
||||
|
||||
@ -365,127 +370,6 @@ func (c *OSCommand) RemoveFile(path string) error {
|
||||
return c.removeFile(path)
|
||||
}
|
||||
|
||||
// builders
|
||||
|
||||
func (c *OSCommand) NewCmdObj(cmdStr string) ICmdObj {
|
||||
args := str.ToArgv(cmdStr)
|
||||
cmd := c.Command(args[0], args[1:]...)
|
||||
cmd.Env = os.Environ()
|
||||
|
||||
return &CmdObj{
|
||||
cmdStr: cmdStr,
|
||||
cmd: cmd,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *OSCommand) NewCmdObjFromArgs(args []string) ICmdObj {
|
||||
cmd := c.Command(args[0], args[1:]...)
|
||||
cmd.Env = os.Environ()
|
||||
|
||||
return &CmdObj{
|
||||
cmdStr: strings.Join(args, " "),
|
||||
cmd: cmd,
|
||||
}
|
||||
}
|
||||
|
||||
// NewShellCmdObjFromString takes a string like `git commit` and returns an executable shell command for it
|
||||
func (c *OSCommand) NewShellCmdObjFromString(commandStr string) ICmdObj {
|
||||
quotedCommand := ""
|
||||
// Windows does not seem to like quotes around the command
|
||||
if c.Platform.OS == "windows" {
|
||||
quotedCommand = strings.NewReplacer(
|
||||
"^", "^^",
|
||||
"&", "^&",
|
||||
"|", "^|",
|
||||
"<", "^<",
|
||||
">", "^>",
|
||||
"%", "^%",
|
||||
).Replace(commandStr)
|
||||
} else {
|
||||
quotedCommand = c.Quote(commandStr)
|
||||
}
|
||||
|
||||
shellCommand := fmt.Sprintf("%s %s %s", c.Platform.Shell, c.Platform.ShellArg, quotedCommand)
|
||||
return c.NewCmdObj(shellCommand)
|
||||
}
|
||||
|
||||
// TODO: pick one of NewShellCmdObjFromString2 and ShellCommandFromString to use. I'm not sure
|
||||
// which one actually is better, but I suspect it's NewShellCmdObjFromString2
|
||||
func (c *OSCommand) NewShellCmdObjFromString2(command string) ICmdObj {
|
||||
return c.NewCmdObjFromArgs([]string{c.Platform.Shell, c.Platform.ShellArg, command})
|
||||
}
|
||||
|
||||
// runners
|
||||
|
||||
type IRunner interface {
|
||||
Run(cmdObj ICmdObj) error
|
||||
RunWithOutput(cmdObj ICmdObj) (string, error)
|
||||
RunLineOutputCmd(cmdObj ICmdObj, onLine func(line string) (bool, error)) error
|
||||
}
|
||||
|
||||
type RunExpectation func(ICmdObj) (string, error)
|
||||
|
||||
type RealRunner struct {
|
||||
c *OSCommand
|
||||
}
|
||||
|
||||
func (self *RealRunner) Run(cmdObj ICmdObj) error {
|
||||
_, err := self.RunWithOutput(cmdObj)
|
||||
return err
|
||||
}
|
||||
|
||||
func (self *RealRunner) RunWithOutput(cmdObj ICmdObj) (string, error) {
|
||||
self.c.LogCmdObj(cmdObj)
|
||||
output, err := sanitisedCommandOutput(cmdObj.GetCmd().CombinedOutput())
|
||||
if err != nil {
|
||||
self.c.Log.WithField("command", cmdObj.ToString()).Error(output)
|
||||
}
|
||||
return output, err
|
||||
}
|
||||
|
||||
func (self *RealRunner) RunLineOutputCmd(cmdObj ICmdObj, onLine func(line string) (bool, error)) error {
|
||||
cmd := cmdObj.GetCmd()
|
||||
stdoutPipe, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
scanner := bufio.NewScanner(stdoutPipe)
|
||||
scanner.Split(bufio.ScanLines)
|
||||
if err := cmd.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
stop, err := onLine(line)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if stop {
|
||||
_ = cmd.Process.Kill()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
_ = cmd.Wait()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func sanitisedCommandOutput(output []byte, err error) (string, error) {
|
||||
outputString := string(output)
|
||||
if err != nil {
|
||||
// errors like 'exit status 1' are not very useful so we'll create an error
|
||||
// from the combined output
|
||||
if outputString == "" {
|
||||
return "", utils.WrapError(err)
|
||||
}
|
||||
return outputString, errors.New(outputString)
|
||||
}
|
||||
return outputString, nil
|
||||
}
|
||||
|
||||
func GetTempDir() string {
|
||||
return filepath.Join(os.TempDir(), "lazygit")
|
||||
}
|
||||
|
@ -33,7 +33,7 @@ func TestOSCommandRunWithOutput(t *testing.T) {
|
||||
|
||||
for _, s := range scenarios {
|
||||
c := NewDummyOSCommand()
|
||||
s.test(NewDummyOSCommand().RunWithOutput(c.NewCmdObj(s.command)))
|
||||
s.test(c.Cmd.New(s.command).RunWithOutput())
|
||||
}
|
||||
}
|
||||
|
||||
@ -55,7 +55,7 @@ func TestOSCommandRun(t *testing.T) {
|
||||
|
||||
for _, s := range scenarios {
|
||||
c := NewDummyOSCommand()
|
||||
s.test(c.Run(c.NewCmdObj(s.command)))
|
||||
s.test(c.Cmd.New(s.command)).Run()
|
||||
}
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user