diff options
author | Jesse Duffield <jessedduffield@gmail.com> | 2021-12-30 10:41:15 +1100 |
---|---|---|
committer | Jesse Duffield <jessedduffield@gmail.com> | 2021-12-30 10:41:15 +1100 |
commit | 65f910ebd85283b5cce9bf67d03d3f1a9ea3813a (patch) | |
tree | 03cdcebca7e71bd46ec29dac06e0fd164c6cabe2 | |
parent | 26c07b1ab33860a1a7591a0638f9925ccf497ffa (diff) |
WIP
-rw-r--r-- | pkg/commands/git.go | 41 | ||||
-rw-r--r-- | pkg/commands/git_cmd_obj_builder.go | 47 | ||||
-rw-r--r-- | pkg/commands/git_cmd_obj_runner.go | 49 | ||||
-rw-r--r-- | pkg/commands/oscommands/cmd_obj.go | 6 | ||||
-rw-r--r-- | pkg/commands/oscommands/cmd_obj_builder.go | 72 | ||||
-rw-r--r-- | pkg/commands/oscommands/cmd_obj_runner.go | 79 | ||||
-rw-r--r-- | pkg/commands/oscommands/os.go | 150 | ||||
-rw-r--r-- | pkg/commands/submodules.go | 7 | ||||
-rw-r--r-- | pkg/gui/custom_commands.go | 2 | ||||
-rw-r--r-- | pkg/gui/files_panel.go | 4 | ||||
-rw-r--r-- | pkg/gui/git_flow.go | 4 |
11 files changed, 286 insertions, 175 deletions
diff --git a/pkg/commands/git.go b/pkg/commands/git.go index 81d46d7c5..3898a654d 100644 --- a/pkg/commands/git.go +++ b/pkg/commands/git.go @@ -6,7 +6,6 @@ import ( "os" "path/filepath" "strings" - "time" "github.com/go-errors/errors" @@ -42,6 +41,8 @@ type GitCommand struct { // 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 + + Cmd oscommands.ICmdObjBuilder } // NewGitCommand it runs git commands @@ -68,6 +69,8 @@ func NewGitCommand( return nil, err } + cmd := NewGitCmdObjBuilder(cmn.Log, osCommand.Cmd) + gitCommand := &GitCommand{ Common: cmn, OSCommand: osCommand, @@ -76,6 +79,7 @@ func NewGitCommand( PushToCurrent: pushToCurrent, GitConfig: gitConfig, GetCmdWriter: func() io.Writer { return ioutil.Discard }, + Cmd: cmd, } gitCommand.PatchManager = patch.NewPatchManager(gitCommand.Log, gitCommand.ApplyPatch, gitCommand.ShowFileDiff) @@ -219,46 +223,19 @@ func VerifyInGitRepo(osCommand *oscommands.OSCommand) error { } func (c *GitCommand) Run(cmdObj oscommands.ICmdObj) error { - _, err := c.RunWithOutput(cmdObj) - return err + return cmdObj.Run() } func (c *GitCommand) RunWithOutput(cmdObj oscommands.ICmdObj) (string, error) { - // TODO: have this retry logic in other places we run the command - waitTime := 50 * time.Millisecond - retryCount := 5 - attempt := 0 - - for { - output, err := c.OSCommand.RunWithOutput(cmdObj) - if err != nil { - // if we have an error based on the index lock, we should wait a bit and then retry - if strings.Contains(output, ".git/index.lock") { - c.Log.Error(output) - c.Log.Info("index.lock prevented command from running. Retrying command after a small wait") - attempt++ - time.Sleep(waitTime) - if attempt < retryCount { - continue - } - } - } - return output, err - } + return cmdObj.RunWithOutput() } func (c *GitCommand) RunLineOutputCmd(cmdObj oscommands.ICmdObj, onLine func(line string) (bool, error)) error { - return c.OSCommand.RunLineOutputCmd(cmdObj, onLine) + return cmdObj.RunLineOutputCmd(onLine) } func (c *GitCommand) NewCmdObj(cmdStr string) oscommands.ICmdObj { - return c.OSCommand.NewCmdObj(cmdStr).AddEnvVars("GIT_OPTIONAL_LOCKS=0") -} - -func (c *GitCommand) NewCmdObjWithLog(cmdStr string) oscommands.ICmdObj { - cmdObj := c.NewCmdObj(cmdStr) - c.OSCommand.LogCmdObj(cmdObj) - return cmdObj + return c.Cmd.New(cmdStr) } func (c *GitCommand) Quote(str string) string { diff --git a/pkg/commands/git_cmd_obj_builder.go b/pkg/commands/git_cmd_obj_builder.go new file mode 100644 index 000000000..487dd2303 --- /dev/null +++ b/pkg/commands/git_cmd_obj_builder.go @@ -0,0 +1,47 @@ +package commands + +import ( + "github.com/jesseduffield/lazygit/pkg/commands/oscommands" + "github.com/sirupsen/logrus" +) + +// all we're doing here is wrapping the default command object builder with +// some git-specific stuff: e.g. adding a git-specific env var + +type gitCmdObjBuilder struct { + innerBuilder *oscommands.CmdObjBuilder +} + +var _ oscommands.ICmdObjBuilder = &gitCmdObjBuilder{} + +func NewGitCmdObjBuilder(log *logrus.Entry, innerBuilder *oscommands.CmdObjBuilder) *gitCmdObjBuilder { + // the price of having a convenient interface where we can say .New(...).Run() is that our builder now depends on our runner, so when we want to wrap the default builder/runner in new functionality we need to jump through some hoops. We could avoid the use of a decorator function here by just exporting the runner field on the default builder but that would be misleading because we don't want anybody using that to run commands (i.e. we want there to be a single API used across the codebase) + updatedBuilder := innerBuilder.CloneWithNewRunner(func(runner oscommands.ICmdObjRunner) oscommands.ICmdObjRunner { + return &gitCmdObjRunner{ + log: log, + innerRunner: runner, + } + }) + + return &gitCmdObjBuilder{ + innerBuilder: updatedBuilder, + } +} + +var defaultEnvVar = "GIT_OPTIONAL_LOCKS=0" + +func (self *gitCmdObjBuilder) New(cmdStr string) oscommands.ICmdObj { + return self.innerBuilder.New(cmdStr).AddEnvVars(defaultEnvVar) +} + +func (self *gitCmdObjBuilder) NewFromArgs(args []string) oscommands.ICmdObj { + return self.innerBuilder.NewFromArgs(args).AddEnvVars(defaultEnvVar) +} + +func (self *gitCmdObjBuilder) NewShell(cmdStr string) oscommands.ICmdObj { + return self.innerBuilder.NewShell(cmdStr).AddEnvVars(defaultEnvVar) +} + +func (self *gitCmdObjBuilder) Quote(str string) string { + return self.innerBuilder.Quote(str) +} diff --git a/pkg/commands/git_cmd_obj_runner.go b/pkg/commands/git_cmd_obj_runner.go new file mode 100644 index 000000000..7529403cd --- /dev/null +++ b/pkg/commands/git_cmd_obj_runner.go @@ -0,0 +1,49 @@ +package commands + +import ( + "strings" + "time" + + "github.com/jesseduffield/lazygit/pkg/commands/oscommands" + "github.com/sirupsen/logrus" +) + +// here we're wrapping the default command runner in some git-specific stuff e.g. retry logic if we get an error due to the presence of .git/index.lock + +type gitCmdObjRunner struct { + log *logrus.Entry + innerRunner oscommands.ICmdObjRunner +} + +func (self *gitCmdObjRunner) Run(cmdObj oscommands.ICmdObj) error { + _, err := self.RunWithOutput(cmdObj) + return err +} + +func (self *gitCmdObjRunner) RunWithOutput(cmdObj oscommands.ICmdObj) (string, error) { + // TODO: have this retry logic in other places we run the command + waitTime := 50 * time.Millisecond + retryCount := 5 + attempt := 0 + + for { + output, err := self.innerRunner.RunWithOutput(cmdObj) + if err != nil { + // if we have an error based on the index lock, we should wait a bit and then retry + if strings.Contains(output, ".git/index.lock") { + self.log.Error(output) + self.log.Info("index.lock prevented command from running. Retrying command after a small wait") + attempt++ + time.Sleep(waitTime) + if attempt < retryCount { + continue + } + } + } + return output, err + } +} + +func (self *gitCmdObjRunner) RunLineOutputCmd(cmdObj oscommands.ICmdObj, onLine func(line string) (bool, error)) error { + return self.innerRunner.RunLineOutputCmd(cmdObj, onLine) +} diff --git a/pkg/commands/oscommands/cmd_obj.go b/pkg/commands/oscommands/cmd_obj.go index ff958e589..0abbcb6b0 100644 --- a/pkg/commands/oscommands/cmd_obj.go +++ b/pkg/commands/oscommands/cmd_obj.go @@ -17,7 +17,7 @@ type ICmdObj interface { RunLineOutputCmd(onLine func(line string) (bool, error)) error // logs command - Log() + Log() ICmdObj } type CmdObj struct { @@ -46,8 +46,10 @@ func (self *CmdObj) GetEnvVars() []string { return self.cmd.Env } -func (self *CmdObj) Log() { +func (self *CmdObj) Log() ICmdObj { self.logCommand(self) + + return self } func (self *CmdObj) Run() error { diff --git a/pkg/commands/oscommands/cmd_obj_builder.go b/pkg/commands/oscommands/cmd_obj_builder.go new file mode 100644 index 000000000..e80a30823 --- /dev/null +++ b/pkg/commands/oscommands/cmd_obj_builder.go @@ -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, + } +} diff --git a/pkg/commands/oscommands/cmd_obj_runner.go b/pkg/commands/oscommands/cmd_obj_runner.go new file mode 100644 index 000000000..5da747bf3 --- /dev/null +++ b/pkg/commands/oscommands/cmd_obj_runner.go @@ -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 +} diff --git a/pkg/commands/oscommands/os.go b/pkg/commands/oscommands/os.go index 2992719b1..c3cd3cce9 100644 --- a/pkg/commands/oscommands/os.go +++ b/pkg/commands/oscommands/os.go @@ -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,9 +53,7 @@ type OSCommand struct { removeFile func(string) error - Cmd ICmdObjBuilder - - ICmdObjRunner + Cmd *CmdObjBuilder } // TODO: make these fields private @@ -92,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.ICmdObjRunner = &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 } @@ -181,8 +182,12 @@ func (c *OSCommand) OpenLink(link string) error { // 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( `"`, `"'"'"`, @@ -365,137 +370,16 @@ func (c *OSCommand) RemoveFile(path string) error { return c.removeFile(path) } -// builders - -type ICmdObjBuilder interface { - // returns a new command object based on the string provided - New(cmdStr string) ICmdObj - NewShell(commandStr string) ICmdObj - NewFromArgs(args []string) ICmdObj - Quote(str string) string -} - 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, - runner: c.ICmdObjRunner, - logCommand: c.LogCmdObj, - } + return c.Cmd.New(cmdStr) } 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, - runner: c.ICmdObjRunner, - logCommand: c.LogCmdObj, - } + return c.Cmd.NewFromArgs(args) } -// NewShellCmdObj takes a string like `git commit` and returns an executable shell command for it func (c *OSCommand) NewShellCmdObj(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 NewShellCmdObj2 and ShellCommandFromString to use. I'm not sure -// which one actually is better, but I suspect it's NewShellCmdObj2 -func (c *OSCommand) NewShellCmdObj2(command string) ICmdObj { - return c.NewCmdObjFromArgs([]string{c.Platform.Shell, c.Platform.ShellArg, command}) -} - -// runners - -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 { - 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 + return c.Cmd.NewShell(commandStr) } func GetTempDir() string { diff --git a/pkg/commands/submodules.go b/pkg/commands/submodules.go index 04e92134a..73675f2ee 100644 --- a/pkg/commands/submodules.go +++ b/pkg/commands/submodules.go @@ -111,14 +111,15 @@ func (c *GitCommand) SubmoduleDelete(submodule *models.SubmoduleConfig) error { } func (c *GitCommand) SubmoduleAdd(name string, path string, url string) error { - return c.OSCommand.Run( - c.OSCommand.NewCmdObj( + return c.Cmd. + New( fmt.Sprintf( "git submodule add --force --name %s -- %s %s ", c.OSCommand.Quote(name), c.OSCommand.Quote(url), c.OSCommand.Quote(path), - ))) + )). + Run() } func (c *GitCommand) SubmoduleUpdateUrl(name string, path string, newUrl string) error { diff --git a/pkg/gui/custom_commands.go b/pkg/gui/custom_commands.go index 163bb9f3a..e5de5fb6a 100644 --- a/pkg/gui/custom_commands.go +++ b/pkg/gui/custom_commands.go @@ -244,7 +244,7 @@ func (gui *Gui) handleCustomCommandKeybinding(customCommand config.CustomCommand } if customCommand.Subprocess { - return gui.runSubprocessWithSuspenseAndRefresh(gui.OSCommand.NewShellCmdObj2(cmdStr)) + return gui.runSubprocessWithSuspenseAndRefresh(gui.OSCommand.Cmd.NewShell(cmdStr)) } loadingText := customCommand.LoadingText diff --git a/pkg/gui/files_panel.go b/pkg/gui/files_panel.go index 4358d5abf..b5c387c81 100644 --- a/pkg/gui/files_panel.go +++ b/pkg/gui/files_panel.go @@ -465,7 +465,7 @@ func (gui *Gui) handleCommitEditorPress() error { cmdStr := "git " + strings.Join(args, " ") return gui.runSubprocessWithSuspenseAndRefresh( - gui.GitCommand.WithSpan(gui.Tr.Spans.Commit).NewCmdObjWithLog(cmdStr), + gui.GitCommand.WithSpan(gui.Tr.Spans.Commit).NewCmdObj(cmdStr).Log(), ) } @@ -923,7 +923,7 @@ func (gui *Gui) handleCustomCommand() error { gui.OnRunCommand(oscommands.NewCmdLogEntry(command, gui.Tr.Spans.CustomCommand, true)) return gui.runSubprocessWithSuspenseAndRefresh( - gui.OSCommand.NewShellCmdObj2(command), + gui.OSCommand.NewShellCmdObj(command), ) }, }) diff --git a/pkg/gui/git_flow.go b/pkg/gui/git_flow.go index c9064df5e..14676e4b2 100644 --- a/pkg/gui/git_flow.go +++ b/pkg/gui/git_flow.go @@ -32,7 +32,7 @@ func (gui *Gui) gitFlowFinishBranch(gitFlowConfig string, branchName string) err } return gui.runSubprocessWithSuspenseAndRefresh( - gui.GitCommand.WithSpan(gui.Tr.Spans.GitFlowFinish).NewCmdObjWithLog("git flow " + branchType + " finish " + suffix), + gui.GitCommand.WithSpan(gui.Tr.Spans.GitFlowFinish).NewCmdObj("git flow " + branchType + " finish " + suffix).Log(), ) } @@ -56,7 +56,7 @@ func (gui *Gui) handleCreateGitFlowMenu() error { title: title, handleConfirm: func(name string) error { return gui.runSubprocessWithSuspenseAndRefresh( - gui.GitCommand.WithSpan(gui.Tr.Spans.GitFlowStart).NewCmdObjWithLog("git flow " + branchType + " start " + name), + gui.GitCommand.WithSpan(gui.Tr.Spans.GitFlowStart).NewCmdObj("git flow " + branchType + " start " + name).Log(), ) }, }) |