From 43a4fa970dfceb7868959ff4da48d5507fa2f234 Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Wed, 29 Dec 2021 14:33:38 +1100 Subject: [PATCH] WIP --- pkg/app/app.go | 4 +- pkg/commands/branches.go | 45 +++---- pkg/commands/branches_test.go | 2 +- pkg/commands/commits.go | 33 +++-- pkg/commands/files.go | 49 ++++--- pkg/commands/git.go | 47 +------ pkg/commands/git_cmd_obj_builder.go | 47 +++++++ pkg/commands/git_cmd_obj_runner.go | 49 +++++++ pkg/commands/loading_branches.go | 90 ++++++------- pkg/commands/loading_commit_files.go | 2 +- pkg/commands/loading_commits.go | 61 ++++----- pkg/commands/loading_files.go | 2 +- pkg/commands/loading_reflog_commits.go | 4 +- pkg/commands/loading_remotes.go | 2 +- pkg/commands/loading_stash.go | 4 +- pkg/commands/loading_tags.go | 2 +- pkg/commands/oscommands/cmd_obj.go | 31 ++++- pkg/commands/oscommands/cmd_obj_builder.go | 72 ++++++++++ pkg/commands/oscommands/cmd_obj_runner.go | 79 +++++++++++ pkg/commands/oscommands/os.go | 150 +++------------------ pkg/commands/oscommands/os_test.go | 4 +- pkg/commands/patch_rebases.go | 6 +- pkg/commands/rebasing.go | 39 +++--- pkg/commands/remotes.go | 14 +- pkg/commands/stash_entries.go | 10 +- pkg/commands/submodules.go | 37 ++--- pkg/commands/sync.go | 10 +- pkg/commands/tags.go | 8 +- pkg/gui/commits_panel.go | 4 +- pkg/gui/custom_commands.go | 8 +- pkg/gui/diffing.go | 2 +- pkg/gui/files_panel.go | 6 +- pkg/gui/git_flow.go | 6 +- pkg/gui/gpg.go | 4 +- pkg/gui/rebase_options_panel.go | 2 +- pkg/gui/recent_repos_panel.go | 2 +- pkg/gui/stash_panel.go | 2 +- pkg/gui/sub_commits_panel.go | 2 +- pkg/gui/submodules_panel.go | 7 +- pkg/integration/integration.go | 6 +- pkg/updates/updates.go | 4 +- 41 files changed, 539 insertions(+), 419 deletions(-) create mode 100644 pkg/commands/git_cmd_obj_builder.go create mode 100644 pkg/commands/git_cmd_obj_runner.go create mode 100644 pkg/commands/oscommands/cmd_obj_builder.go create mode 100644 pkg/commands/oscommands/cmd_obj_runner.go diff --git a/pkg/app/app.go b/pkg/app/app.go index 747f5205f..4fcbdc5b8 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -151,7 +151,7 @@ func NewApp(config config.AppConfigurer, filterPath string) (*App, error) { } func (app *App) validateGitVersion() error { - output, err := app.OSCommand.RunWithOutput(app.OSCommand.NewCmdObj("git --version")) + output, err := app.OSCommand.Cmd.New("git --version").RunWithOutput() // if we get an error anywhere here we'll show the same status minVersionError := errors.New(app.Tr.MinGitVersionError) if err != nil { @@ -236,7 +236,7 @@ func (app *App) setupRepo() (bool, error) { os.Exit(1) } - if err := app.OSCommand.Run(app.OSCommand.NewCmdObj("git init")); err != nil { + if err := app.OSCommand.Cmd.New("git init").Run(); err != nil { return false, err } } diff --git a/pkg/commands/branches.go b/pkg/commands/branches.go index 274168311..e3c40d343 100644 --- a/pkg/commands/branches.go +++ b/pkg/commands/branches.go @@ -11,19 +11,19 @@ import ( // NewBranch create new branch func (c *GitCommand) NewBranch(name string, base string) error { - return c.Run(c.NewCmdObj(fmt.Sprintf("git checkout -b %s %s", c.OSCommand.Quote(name), c.OSCommand.Quote(base)))) + return c.Cmd.New(fmt.Sprintf("git checkout -b %s %s", c.OSCommand.Quote(name), c.OSCommand.Quote(base))).Run() } // CurrentBranchName get the current branch name and displayname. // the first returned string is the name and the second is the displayname // e.g. name is 123asdf and displayname is '(HEAD detached at 123asdf)' func (c *GitCommand) CurrentBranchName() (string, string, error) { - branchName, err := c.RunWithOutput(c.NewCmdObj("git symbolic-ref --short HEAD")) + branchName, err := c.Cmd.New("git symbolic-ref --short HEAD").RunWithOutput() if err == nil && branchName != "HEAD\n" { trimmedBranchName := strings.TrimSpace(branchName) return trimmedBranchName, trimmedBranchName, nil } - output, err := c.RunWithOutput(c.NewCmdObj("git branch --contains")) + output, err := c.Cmd.New("git branch --contains").RunWithOutput() if err != nil { return "", "", err } @@ -47,7 +47,7 @@ func (c *GitCommand) DeleteBranch(branch string, force bool) error { command = "git branch -D" } - return c.OSCommand.Run(c.OSCommand.NewCmdObj(fmt.Sprintf("%s %s", command, c.OSCommand.Quote(branch)))) + return c.Cmd.New(fmt.Sprintf("%s %s", command, c.OSCommand.Quote(branch))).Run() } // Checkout checks out a branch (or commit), with --force if you set the force arg to true @@ -62,24 +62,23 @@ func (c *GitCommand) Checkout(branch string, options CheckoutOptions) error { forceArg = " --force" } - cmdObj := c.NewCmdObj(fmt.Sprintf("git checkout%s %s", forceArg, c.OSCommand.Quote(branch))). + return c.Cmd.New(fmt.Sprintf("git checkout%s %s", forceArg, c.OSCommand.Quote(branch))). // prevents git from prompting us for input which would freeze the program // TODO: see if this is actually needed here AddEnvVars("GIT_TERMINAL_PROMPT=0"). - AddEnvVars(options.EnvVars...) - - return c.OSCommand.Run(cmdObj) + AddEnvVars(options.EnvVars...). + Run() } // GetBranchGraph gets the color-formatted graph of the log for the given branch // Currently it limits the result to 100 commits, but when we get async stuff // working we can do lazy loading func (c *GitCommand) GetBranchGraph(branchName string) (string, error) { - return c.OSCommand.RunWithOutput(c.GetBranchGraphCmdObj(branchName)) + return c.GetBranchGraphCmdObj(branchName).RunWithOutput() } func (c *GitCommand) GetUpstreamForBranch(branchName string) (string, error) { - output, err := c.RunWithOutput(c.NewCmdObj(fmt.Sprintf("git rev-parse --abbrev-ref --symbolic-full-name %s@{u}", c.OSCommand.Quote(branchName)))) + output, err := c.Cmd.New(fmt.Sprintf("git rev-parse --abbrev-ref --symbolic-full-name %s@{u}", c.OSCommand.Quote(branchName))).RunWithOutput() return strings.TrimSpace(output), err } @@ -88,15 +87,15 @@ func (c *GitCommand) GetBranchGraphCmdObj(branchName string) oscommands.ICmdObj templateValues := map[string]string{ "branchName": c.OSCommand.Quote(branchName), } - return c.NewCmdObj(utils.ResolvePlaceholderString(branchLogCmdTemplate, templateValues)) + return c.Cmd.New(utils.ResolvePlaceholderString(branchLogCmdTemplate, templateValues)) } func (c *GitCommand) SetUpstreamBranch(upstream string) error { - return c.Run(c.NewCmdObj("git branch -u " + c.OSCommand.Quote(upstream))) + return c.Cmd.New("git branch -u " + c.OSCommand.Quote(upstream)).Run() } func (c *GitCommand) SetBranchUpstream(remoteName string, remoteBranchName string, branchName string) error { - return c.Run(c.NewCmdObj(fmt.Sprintf("git branch --set-upstream-to=%s/%s %s", c.OSCommand.Quote(remoteName), c.OSCommand.Quote(remoteBranchName), c.OSCommand.Quote(branchName)))) + return c.Cmd.New(fmt.Sprintf("git branch --set-upstream-to=%s/%s %s", c.OSCommand.Quote(remoteName), c.OSCommand.Quote(remoteBranchName), c.OSCommand.Quote(branchName))).Run() } func (c *GitCommand) GetCurrentBranchUpstreamDifferenceCount() (string, string) { @@ -111,11 +110,11 @@ func (c *GitCommand) GetBranchUpstreamDifferenceCount(branchName string) (string // current branch func (c *GitCommand) GetCommitDifferences(from, to string) (string, string) { command := "git rev-list %s..%s --count" - pushableCount, err := c.RunWithOutput(c.NewCmdObj(fmt.Sprintf(command, to, from))) + pushableCount, err := c.Cmd.New(fmt.Sprintf(command, to, from)).RunWithOutput() if err != nil { return "?", "?" } - pullableCount, err := c.RunWithOutput(c.NewCmdObj(fmt.Sprintf(command, from, to))) + pullableCount, err := c.Cmd.New(fmt.Sprintf(command, from, to)).RunWithOutput() if err != nil { return "?", "?" } @@ -135,37 +134,37 @@ func (c *GitCommand) Merge(branchName string, opts MergeOpts) error { command = fmt.Sprintf("%s --ff-only", command) } - return c.OSCommand.Run(c.OSCommand.NewCmdObj(command)) + return c.OSCommand.Cmd.New(command).Run() } // AbortMerge abort merge func (c *GitCommand) AbortMerge() error { - return c.Run(c.NewCmdObj("git merge --abort")) + return c.Cmd.New("git merge --abort").Run() } func (c *GitCommand) IsHeadDetached() bool { - err := c.Run(c.NewCmdObj("git symbolic-ref -q HEAD")) + err := c.Cmd.New("git symbolic-ref -q HEAD").Run() return err != nil } // ResetHardHead runs `git reset --hard` func (c *GitCommand) ResetHard(ref string) error { - return c.Run(c.NewCmdObj("git reset --hard " + c.OSCommand.Quote(ref))) + return c.Cmd.New("git reset --hard " + c.OSCommand.Quote(ref)).Run() } // ResetSoft runs `git reset --soft HEAD` func (c *GitCommand) ResetSoft(ref string) error { - return c.Run(c.NewCmdObj("git reset --soft " + c.OSCommand.Quote(ref))) + return c.Cmd.New("git reset --soft " + c.OSCommand.Quote(ref)).Run() } func (c *GitCommand) ResetMixed(ref string) error { - return c.Run(c.NewCmdObj("git reset --mixed " + c.OSCommand.Quote(ref))) + return c.Cmd.New("git reset --mixed " + c.OSCommand.Quote(ref)).Run() } func (c *GitCommand) RenameBranch(oldName string, newName string) error { - return c.Run(c.NewCmdObj(fmt.Sprintf("git branch --move %s %s", c.OSCommand.Quote(oldName), c.OSCommand.Quote(newName)))) + return c.Cmd.New(fmt.Sprintf("git branch --move %s %s", c.OSCommand.Quote(oldName), c.OSCommand.Quote(newName))).Run() } func (c *GitCommand) GetRawBranches() (string, error) { - return c.RunWithOutput(c.NewCmdObj(`git for-each-ref --sort=-committerdate --format="%(HEAD)|%(refname:short)|%(upstream:short)|%(upstream:track)" refs/heads`)) + return c.Cmd.New(`git for-each-ref --sort=-committerdate --format="%(HEAD)|%(refname:short)|%(upstream:short)|%(upstream:track)" refs/heads`).RunWithOutput() } diff --git a/pkg/commands/branches_test.go b/pkg/commands/branches_test.go index 7064f4968..45cfb8315 100644 --- a/pkg/commands/branches_test.go +++ b/pkg/commands/branches_test.go @@ -210,7 +210,7 @@ func TestGitCommandGetAllBranchGraph(t *testing.T) { return secureexec.Command("echo") } cmdStr := gitCmd.UserConfig.Git.AllBranchesLogCmd - _, err := gitCmd.OSCommand.RunWithOutput(gitCmd.NewCmdObj(cmdStr)) + _, err := gitCmd.Cmd.New(cmdStr).RunWithOutput() assert.NoError(t, err) } diff --git a/pkg/commands/commits.go b/pkg/commands/commits.go index 447e9adb8..689220529 100644 --- a/pkg/commands/commits.go +++ b/pkg/commands/commits.go @@ -10,18 +10,17 @@ import ( // RenameCommit renames the topmost commit with the given name func (c *GitCommand) RenameCommit(name string) error { - return c.Run(c.NewCmdObj("git commit --allow-empty --amend --only -m " + c.OSCommand.Quote(name))) + return c.Cmd.New("git commit --allow-empty --amend --only -m " + c.OSCommand.Quote(name)).Run() } // ResetToCommit reset to commit func (c *GitCommand) ResetToCommit(sha string, strength string, envVars []string) error { - cmdObj := c.NewCmdObj(fmt.Sprintf("git reset --%s %s", strength, sha)). + return c.Cmd.New(fmt.Sprintf("git reset --%s %s", strength, sha)). // prevents git from prompting us for input which would freeze the program // TODO: see if this is actually needed here AddEnvVars("GIT_TERMINAL_PROMPT=0"). - AddEnvVars(envVars...) - - return c.OSCommand.Run(cmdObj) + AddEnvVars(envVars...). + Run() } func (c *GitCommand) CommitCmdObj(message string, flags string) oscommands.ICmdObj { @@ -36,33 +35,33 @@ func (c *GitCommand) CommitCmdObj(message string, flags string) oscommands.ICmdO flagsStr = fmt.Sprintf(" %s", flags) } - return c.NewCmdObj(fmt.Sprintf("git commit%s%s", flagsStr, lineArgs)) + return c.Cmd.New(fmt.Sprintf("git commit%s%s", flagsStr, lineArgs)) } // Get the subject of the HEAD commit func (c *GitCommand) GetHeadCommitMessage() (string, error) { - message, err := c.RunWithOutput(c.NewCmdObj("git log -1 --pretty=%s")) + message, err := c.Cmd.New("git log -1 --pretty=%s").RunWithOutput() return strings.TrimSpace(message), err } func (c *GitCommand) GetCommitMessage(commitSha string) (string, error) { cmdStr := "git rev-list --format=%B --max-count=1 " + commitSha - messageWithHeader, err := c.RunWithOutput(c.NewCmdObj(cmdStr)) + messageWithHeader, err := c.Cmd.New(cmdStr).RunWithOutput() message := strings.Join(strings.SplitAfter(messageWithHeader, "\n")[1:], "\n") return strings.TrimSpace(message), err } func (c *GitCommand) GetCommitMessageFirstLine(sha string) (string, error) { - return c.RunWithOutput(c.NewCmdObj(fmt.Sprintf("git show --no-patch --pretty=format:%%s %s", sha))) + return c.Cmd.New(fmt.Sprintf("git show --no-patch --pretty=format:%%s %s", sha)).RunWithOutput() } // AmendHead amends HEAD with whatever is staged in your working tree func (c *GitCommand) AmendHead() error { - return c.OSCommand.Run(c.AmendHeadCmdObj()) + return c.AmendHeadCmdObj().Run() } func (c *GitCommand) AmendHeadCmdObj() oscommands.ICmdObj { - return c.NewCmdObj("git commit --amend --no-edit --allow-empty") + return c.Cmd.New("git commit --amend --no-edit --allow-empty") } func (c *GitCommand) ShowCmdObj(sha string, filterPath string) oscommands.ICmdObj { @@ -73,16 +72,16 @@ func (c *GitCommand) ShowCmdObj(sha string, filterPath string) oscommands.ICmdOb } cmdStr := fmt.Sprintf("git show --submodule --color=%s --unified=%d --no-renames --stat -p %s %s", c.colorArg(), contextSize, sha, filterPathArg) - return c.NewCmdObj(cmdStr) + return c.Cmd.New(cmdStr) } // Revert reverts the selected commit by sha func (c *GitCommand) Revert(sha string) error { - return c.Run(c.NewCmdObj(fmt.Sprintf("git revert %s", sha))) + return c.Cmd.New(fmt.Sprintf("git revert %s", sha)).Run() } func (c *GitCommand) RevertMerge(sha string, parentNumber int) error { - return c.Run(c.NewCmdObj(fmt.Sprintf("git revert %s -m %d", sha, parentNumber))) + return c.Cmd.New(fmt.Sprintf("git revert %s -m %d", sha, parentNumber)).Run() } // CherryPickCommits begins an interactive rebase with the given shas being cherry picked onto HEAD @@ -92,15 +91,15 @@ func (c *GitCommand) CherryPickCommits(commits []*models.Commit) error { todo = "pick " + commit.Sha + " " + commit.Name + "\n" + todo } - cmd, err := c.PrepareInteractiveRebaseCommand("HEAD", todo, false) + cmdObj, err := c.PrepareInteractiveRebaseCommand("HEAD", todo, false) if err != nil { return err } - return c.OSCommand.Run(cmd) + return cmdObj.Run() } // CreateFixupCommit creates a commit that fixes up a previous commit func (c *GitCommand) CreateFixupCommit(sha string) error { - return c.Run(c.NewCmdObj(fmt.Sprintf("git commit --fixup=%s", sha))) + return c.Cmd.New(fmt.Sprintf("git commit --fixup=%s", sha)).Run() } diff --git a/pkg/commands/files.go b/pkg/commands/files.go index 28f37612f..da451e80c 100644 --- a/pkg/commands/files.go +++ b/pkg/commands/files.go @@ -25,26 +25,26 @@ func (c *GitCommand) CatFile(fileName string) (string, error) { } func (c *GitCommand) OpenMergeToolCmdObj() oscommands.ICmdObj { - return c.NewCmdObj("git mergetool") + return c.Cmd.New("git mergetool") } func (c *GitCommand) OpenMergeTool() error { - return c.Run(c.OpenMergeToolCmdObj()) + return c.OpenMergeToolCmdObj().Run() } // StageFile stages a file func (c *GitCommand) StageFile(fileName string) error { - return c.Run(c.NewCmdObj("git add -- " + c.OSCommand.Quote(fileName))) + return c.Cmd.New("git add -- " + c.OSCommand.Quote(fileName)).Run() } // StageAll stages all files func (c *GitCommand) StageAll() error { - return c.Run(c.NewCmdObj("git add -A")) + return c.Cmd.New("git add -A").Run() } // UnstageAll unstages all files func (c *GitCommand) UnstageAll() error { - return c.Run(c.NewCmdObj("git reset")) + return c.Cmd.New("git reset").Run() } // UnStageFile unstages a file @@ -57,8 +57,8 @@ func (c *GitCommand) UnStageFile(fileNames []string, reset bool) error { } for _, name := range fileNames { - cmdObj := c.NewCmdObj(fmt.Sprintf(command, c.OSCommand.Quote(name))) - if err := c.Run(cmdObj); err != nil { + err := c.Cmd.New(fmt.Sprintf(command, c.OSCommand.Quote(name))).Run() + if err != nil { return err } } @@ -122,22 +122,22 @@ func (c *GitCommand) DiscardAllFileChanges(file *models.File) error { quotedFileName := c.OSCommand.Quote(file.Name) if file.ShortStatus == "AA" { - if err := c.Run(c.NewCmdObj("git checkout --ours -- " + quotedFileName)); err != nil { + if err := c.Cmd.New("git checkout --ours -- " + quotedFileName).Run(); err != nil { return err } - if err := c.Run(c.NewCmdObj("git add -- " + quotedFileName)); err != nil { + if err := c.Cmd.New("git add -- " + quotedFileName).Run(); err != nil { return err } return nil } if file.ShortStatus == "DU" { - return c.Run(c.NewCmdObj("git rm -- " + quotedFileName)) + return c.Cmd.New("git rm -- " + quotedFileName).Run() } // if the file isn't tracked, we assume you want to delete it if file.HasStagedChanges || file.HasMergeConflicts { - if err := c.Run(c.NewCmdObj("git reset -- " + quotedFileName)); err != nil { + if err := c.Cmd.New("git reset -- " + quotedFileName).Run(); err != nil { return err } } @@ -163,7 +163,7 @@ func (c *GitCommand) DiscardUnstagedDirChanges(node *filetree.FileNode) error { } quotedPath := c.OSCommand.Quote(node.GetPath()) - if err := c.Run(c.NewCmdObj("git checkout -- " + quotedPath)); err != nil { + if err := c.Cmd.New("git checkout -- " + quotedPath).Run(); err != nil { return err } @@ -188,7 +188,7 @@ func (c *GitCommand) RemoveUntrackedDirFiles(node *filetree.FileNode) error { // DiscardUnstagedFileChanges directly func (c *GitCommand) DiscardUnstagedFileChanges(file *models.File) error { quotedFileName := c.OSCommand.Quote(file.Name) - return c.Run(c.NewCmdObj("git checkout -- " + quotedFileName)) + return c.Cmd.New("git checkout -- " + quotedFileName).Run() } // Ignore adds a file to the gitignore for the repo @@ -199,7 +199,7 @@ func (c *GitCommand) Ignore(filename string) error { // WorktreeFileDiff returns the diff of a file func (c *GitCommand) WorktreeFileDiff(file *models.File, plain bool, cached bool, ignoreWhitespace bool) string { // for now we assume an error means the file was deleted - s, _ := c.OSCommand.RunWithOutput(c.WorktreeFileDiffCmdObj(file, plain, cached, ignoreWhitespace)) + s, _ := c.WorktreeFileDiffCmdObj(file, plain, cached, ignoreWhitespace).RunWithOutput() return s } @@ -225,7 +225,7 @@ func (c *GitCommand) WorktreeFileDiffCmdObj(node models.IFile, plain bool, cache cmdStr := fmt.Sprintf("git diff --submodule --no-ext-diff --unified=%d --color=%s %s %s %s %s", contextSize, colorArg, ignoreWhitespaceArg, cachedArg, trackedArg, quotedPath) - return c.NewCmdObj(cmdStr) + return c.Cmd.New(cmdStr) } func (c *GitCommand) ApplyPatch(patch string, flags ...string) error { @@ -240,14 +240,13 @@ func (c *GitCommand) ApplyPatch(patch string, flags ...string) error { flagStr += " --" + flag } - return c.Run(c.NewCmdObj(fmt.Sprintf("git apply %s %s", flagStr, c.OSCommand.Quote(filepath)))) + return c.Cmd.New(fmt.Sprintf("git apply %s %s", flagStr, c.OSCommand.Quote(filepath))).Run() } // ShowFileDiff get the diff of specified from and to. Typically this will be used for a single commit so it'll be 123abc^..123abc // but when we're in diff mode it could be any 'from' to any 'to'. The reverse flag is also here thanks to diff mode. func (c *GitCommand) ShowFileDiff(from string, to string, reverse bool, fileName string, plain bool) (string, error) { - cmdObj := c.ShowFileDiffCmdObj(from, to, reverse, fileName, plain) - return c.RunWithOutput(cmdObj) + return c.ShowFileDiffCmdObj(from, to, reverse, fileName, plain).RunWithOutput() } func (c *GitCommand) ShowFileDiffCmdObj(from string, to string, reverse bool, fileName string, plain bool) oscommands.ICmdObj { @@ -262,12 +261,12 @@ func (c *GitCommand) ShowFileDiffCmdObj(from string, to string, reverse bool, fi reverseFlag = " -R " } - return c.NewCmdObj(fmt.Sprintf("git diff --submodule --no-ext-diff --unified=%d --no-renames --color=%s %s %s %s -- %s", contextSize, colorArg, from, to, reverseFlag, c.OSCommand.Quote(fileName))) + return c.Cmd.New(fmt.Sprintf("git diff --submodule --no-ext-diff --unified=%d --no-renames --color=%s %s %s %s -- %s", contextSize, colorArg, from, to, reverseFlag, c.OSCommand.Quote(fileName))) } // CheckoutFile checks out the file for the given commit func (c *GitCommand) CheckoutFile(commitSha, fileName string) error { - return c.Run(c.NewCmdObj(fmt.Sprintf("git checkout %s -- %s", commitSha, c.OSCommand.Quote(fileName)))) + return c.Cmd.New(fmt.Sprintf("git checkout %s -- %s", commitSha, c.OSCommand.Quote(fileName))).Run() } // DiscardOldFileChanges discards changes to a file from an old commit @@ -277,7 +276,7 @@ func (c *GitCommand) DiscardOldFileChanges(commits []*models.Commit, commitIndex } // check if file exists in previous commit (this command returns an error if the file doesn't exist) - if err := c.Run(c.NewCmdObj("git cat-file -e HEAD^:" + c.OSCommand.Quote(fileName))); err != nil { + if err := c.Cmd.New("git cat-file -e HEAD^:" + c.OSCommand.Quote(fileName)).Run(); err != nil { if err := c.OSCommand.Remove(fileName); err != nil { return err } @@ -300,17 +299,17 @@ func (c *GitCommand) DiscardOldFileChanges(commits []*models.Commit, commitIndex // DiscardAnyUnstagedFileChanges discards any unstages file changes via `git checkout -- .` func (c *GitCommand) DiscardAnyUnstagedFileChanges() error { - return c.Run(c.NewCmdObj("git checkout -- .")) + return c.Cmd.New("git checkout -- .").Run() } // RemoveTrackedFiles will delete the given file(s) even if they are currently tracked func (c *GitCommand) RemoveTrackedFiles(name string) error { - return c.Run(c.NewCmdObj("git rm -r --cached -- " + c.OSCommand.Quote(name))) + return c.Cmd.New("git rm -r --cached -- " + c.OSCommand.Quote(name)).Run() } // RemoveUntrackedFiles runs `git clean -fd` func (c *GitCommand) RemoveUntrackedFiles() error { - return c.Run(c.NewCmdObj("git clean -fd")) + return c.Cmd.New("git clean -fd").Run() } // ResetAndClean removes all unstaged changes and removes all untracked files @@ -350,7 +349,7 @@ func (c *GitCommand) EditFileCmdStr(filename string, lineNumber int) (string, er editor = c.OSCommand.Getenv("EDITOR") } if editor == "" { - if err := c.OSCommand.Run(c.NewCmdObj("which vi")); err == nil { + if err := c.OSCommand.Cmd.New("which vi").Run(); err == nil { editor = "vi" } } diff --git a/pkg/commands/git.go b/pkg/commands/git.go index dd3879b82..b83fa1b73 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) @@ -215,44 +219,5 @@ func findDotGitDir(stat func(string) (os.FileInfo, error), readFile func(filenam } func VerifyInGitRepo(osCommand *oscommands.OSCommand) error { - return osCommand.Run(osCommand.NewCmdObj("git rev-parse --git-dir")) -} - -func (c *GitCommand) Run(cmdObj oscommands.ICmdObj) error { - _, err := c.RunWithOutput(cmdObj) - return err -} - -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 - } -} - -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 osCommand.Cmd.New("git rev-parse --git-dir").Run() } 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/loading_branches.go b/pkg/commands/loading_branches.go index 0c850649f..802bdb30e 100644 --- a/pkg/commands/loading_branches.go +++ b/pkg/commands/loading_branches.go @@ -42,6 +42,51 @@ func NewBranchListBuilder( } } +// Build the list of branches for the current repo +func (b *BranchListBuilder) Build() []*models.Branch { + branches := b.obtainBranches() + + reflogBranches := b.obtainReflogBranches() + + // loop through reflog branches. If there is a match, merge them, then remove it from the branches and keep it in the reflog branches + branchesWithRecency := make([]*models.Branch, 0) +outer: + for _, reflogBranch := range reflogBranches { + for j, branch := range branches { + if branch.Head { + continue + } + if strings.EqualFold(reflogBranch.Name, branch.Name) { + branch.Recency = reflogBranch.Recency + branchesWithRecency = append(branchesWithRecency, branch) + branches = append(branches[0:j], branches[j+1:]...) + continue outer + } + } + } + + branches = append(branchesWithRecency, branches...) + + foundHead := false + for i, branch := range branches { + if branch.Head { + foundHead = true + branch.Recency = " *" + branches = append(branches[0:i], branches[i+1:]...) + branches = append([]*models.Branch{branch}, branches...) + break + } + } + if !foundHead { + currentBranchName, currentBranchDisplayName, err := b.getCurrentBranchName() + if err != nil { + panic(err) + } + branches = append([]*models.Branch{{Name: currentBranchName, DisplayName: currentBranchDisplayName, Head: true, Recency: " *"}}, branches...) + } + return branches +} + func (b *BranchListBuilder) obtainBranches() []*models.Branch { output, err := b.getRawBranches() if err != nil { @@ -103,51 +148,6 @@ func (b *BranchListBuilder) obtainBranches() []*models.Branch { return branches } -// Build the list of branches for the current repo -func (b *BranchListBuilder) Build() []*models.Branch { - branches := b.obtainBranches() - - reflogBranches := b.obtainReflogBranches() - - // loop through reflog branches. If there is a match, merge them, then remove it from the branches and keep it in the reflog branches - branchesWithRecency := make([]*models.Branch, 0) -outer: - for _, reflogBranch := range reflogBranches { - for j, branch := range branches { - if branch.Head { - continue - } - if strings.EqualFold(reflogBranch.Name, branch.Name) { - branch.Recency = reflogBranch.Recency - branchesWithRecency = append(branchesWithRecency, branch) - branches = append(branches[0:j], branches[j+1:]...) - continue outer - } - } - } - - branches = append(branchesWithRecency, branches...) - - foundHead := false - for i, branch := range branches { - if branch.Head { - foundHead = true - branch.Recency = " *" - branches = append(branches[0:i], branches[i+1:]...) - branches = append([]*models.Branch{branch}, branches...) - break - } - } - if !foundHead { - currentBranchName, currentBranchDisplayName, err := b.getCurrentBranchName() - if err != nil { - panic(err) - } - branches = append([]*models.Branch{{Name: currentBranchName, DisplayName: currentBranchDisplayName, Head: true, Recency: " *"}}, branches...) - } - return branches -} - // TODO: only look at the new reflog commits, and otherwise store the recencies in // int form against the branch to recalculate the time ago func (b *BranchListBuilder) obtainReflogBranches() []*models.Branch { diff --git a/pkg/commands/loading_commit_files.go b/pkg/commands/loading_commit_files.go index 95d98d668..fa6d3c8a2 100644 --- a/pkg/commands/loading_commit_files.go +++ b/pkg/commands/loading_commit_files.go @@ -14,7 +14,7 @@ func (c *GitCommand) GetFilesInDiff(from string, to string, reverse bool) ([]*mo reverseFlag = " -R " } - filenames, err := c.RunWithOutput(c.NewCmdObj(fmt.Sprintf("git diff --submodule --no-ext-diff --name-status -z --no-renames %s %s %s", reverseFlag, from, to))) + filenames, err := c.Cmd.New(fmt.Sprintf("git diff --submodule --no-ext-diff --name-status -z --no-renames %s %s %s", reverseFlag, from, to)).RunWithOutput() if err != nil { return nil, err } diff --git a/pkg/commands/loading_commits.go b/pkg/commands/loading_commits.go index 52955eef3..d7673d334 100644 --- a/pkg/commands/loading_commits.go +++ b/pkg/commands/loading_commits.go @@ -11,9 +11,8 @@ import ( "github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/commands/oscommands" + "github.com/jesseduffield/lazygit/pkg/common" "github.com/jesseduffield/lazygit/pkg/gui/style" - "github.com/jesseduffield/lazygit/pkg/i18n" - "github.com/sirupsen/logrus" ) // context: @@ -29,24 +28,28 @@ const SEPARATION_CHAR = "|" // CommitListBuilder returns a list of Branch objects for the current repo type CommitListBuilder struct { - Log *logrus.Entry - GitCommand *GitCommand - OSCommand *oscommands.OSCommand - Tr *i18n.TranslationSet + *common.Common + cmd oscommands.ICmdObjBuilder + + getCurrentBranchName func() (string, string, error) + getRebaseMode func() (string, error) + readFile func(filename string) ([]byte, error) + dotGitDir string } // NewCommitListBuilder builds a new commit list builder func NewCommitListBuilder( - log *logrus.Entry, + cmn *common.Common, gitCommand *GitCommand, osCommand *oscommands.OSCommand, - tr *i18n.TranslationSet, ) *CommitListBuilder { return &CommitListBuilder{ - Log: log, - GitCommand: gitCommand, - OSCommand: osCommand, - Tr: tr, + Common: cmn, + cmd: gitCommand.Cmd, + getCurrentBranchName: gitCommand.CurrentBranchName, + getRebaseMode: gitCommand.RebaseMode, + dotGitDir: gitCommand.DotGitDir, + readFile: ioutil.ReadFile, } } @@ -106,7 +109,7 @@ func (c *CommitListBuilder) MergeRebasingCommits(commits []*models.Commit) ([]*m } } - rebaseMode, err := c.GitCommand.RebaseMode() + rebaseMode, err := c.getRebaseMode() if err != nil { return nil, err } @@ -131,7 +134,7 @@ func (c *CommitListBuilder) MergeRebasingCommits(commits []*models.Commit) ([]*m func (c *CommitListBuilder) GetCommits(opts GetCommitsOptions) ([]*models.Commit, error) { commits := []*models.Commit{} var rebasingCommits []*models.Commit - rebaseMode, err := c.GitCommand.RebaseMode() + rebaseMode, err := c.getRebaseMode() if err != nil { return nil, err } @@ -152,9 +155,7 @@ func (c *CommitListBuilder) GetCommits(opts GetCommitsOptions) ([]*models.Commit passedFirstPushedCommit = true } - cmdObj := c.getLogCmd(opts) - - err = c.OSCommand.RunLineOutputCmd(cmdObj, func(line string) (bool, error) { + err = c.getLogCmd(opts).RunLineOutputCmd(func(line string) (bool, error) { if canExtractCommit(line) { commit := c.extractCommitFromLine(line) if commit.Sha == firstPushedCommit { @@ -200,7 +201,7 @@ func (c *CommitListBuilder) getHydratedRebasingCommits(rebaseMode string) ([]*mo // note that we're not filtering these as we do non-rebasing commits just because // I suspect that will cause some damage - cmdObj := c.OSCommand.NewCmdObj( + cmdObj := c.cmd.New( fmt.Sprintf( "git show %s --no-patch --oneline %s --abbrev=%d", strings.Join(commitShas, " "), @@ -211,7 +212,7 @@ func (c *CommitListBuilder) getHydratedRebasingCommits(rebaseMode string) ([]*mo hydratedCommits := make([]*models.Commit, 0, len(commits)) i := 0 - err = c.OSCommand.RunLineOutputCmd(cmdObj, func(line string) (bool, error) { + err = cmdObj.RunLineOutputCmd(func(line string) (bool, error) { if canExtractCommit(line) { commit := c.extractCommitFromLine(line) matchingCommit := commits[i] @@ -242,7 +243,7 @@ func (c *CommitListBuilder) getRebasingCommits(rebaseMode string) ([]*models.Com func (c *CommitListBuilder) getNormalRebasingCommits() ([]*models.Commit, error) { rewrittenCount := 0 - bytesContent, err := ioutil.ReadFile(filepath.Join(c.GitCommand.DotGitDir, "rebase-apply/rewritten")) + bytesContent, err := c.readFile(filepath.Join(c.dotGitDir, "rebase-apply/rewritten")) if err == nil { content := string(bytesContent) rewrittenCount = len(strings.Split(content, "\n")) @@ -250,7 +251,7 @@ func (c *CommitListBuilder) getNormalRebasingCommits() ([]*models.Commit, error) // we know we're rebasing, so lets get all the files whose names have numbers commits := []*models.Commit{} - err = filepath.Walk(filepath.Join(c.GitCommand.DotGitDir, "rebase-apply"), func(path string, f os.FileInfo, err error) error { + err = filepath.Walk(filepath.Join(c.dotGitDir, "rebase-apply"), func(path string, f os.FileInfo, err error) error { if rewrittenCount > 0 { rewrittenCount-- return nil @@ -262,7 +263,7 @@ func (c *CommitListBuilder) getNormalRebasingCommits() ([]*models.Commit, error) if !re.MatchString(f.Name()) { return nil } - bytesContent, err := ioutil.ReadFile(path) + bytesContent, err := c.readFile(path) if err != nil { return err } @@ -294,7 +295,7 @@ func (c *CommitListBuilder) getNormalRebasingCommits() ([]*models.Commit, error) // and extracts out the sha and names of commits that we still have to go // in the rebase: func (c *CommitListBuilder) getInteractiveRebasingCommits() ([]*models.Commit, error) { - bytesContent, err := ioutil.ReadFile(filepath.Join(c.GitCommand.DotGitDir, "rebase-merge/git-rebase-todo")) + bytesContent, err := c.readFile(filepath.Join(c.dotGitDir, "rebase-merge/git-rebase-todo")) if err != nil { c.Log.Error(fmt.Sprintf("error occurred reading git-rebase-todo: %s", err.Error())) // we assume an error means the file doesn't exist so we just return @@ -362,7 +363,7 @@ func (c *CommitListBuilder) setCommitMergedStatuses(refName string, commits []*m } func (c *CommitListBuilder) getMergeBase(refName string) (string, error) { - currentBranch, _, err := c.GitCommand.CurrentBranchName() + currentBranch, _, err := c.getCurrentBranchName() if err != nil { return "", err } @@ -373,7 +374,7 @@ func (c *CommitListBuilder) getMergeBase(refName string) (string, error) { } // swallowing error because it's not a big deal; probably because there are no commits yet - output, _ := c.OSCommand.RunWithOutput(c.OSCommand.NewCmdObj(fmt.Sprintf("git merge-base %s %s", c.OSCommand.Quote(refName), c.OSCommand.Quote(baseBranch)))) + output, _ := c.cmd.New(fmt.Sprintf("git merge-base %s %s", c.cmd.Quote(refName), c.cmd.Quote(baseBranch))).RunWithOutput() return ignoringWarnings(output), nil } @@ -390,7 +391,7 @@ func ignoringWarnings(commandOutput string) string { // getFirstPushedCommit returns the first commit SHA which has been pushed to the ref's upstream. // all commits above this are deemed unpushed and marked as such. func (c *CommitListBuilder) getFirstPushedCommit(refName string) (string, error) { - output, err := c.OSCommand.RunWithOutput(c.OSCommand.NewCmdObj(fmt.Sprintf("git merge-base %s %s@{u}", c.OSCommand.Quote(refName), c.OSCommand.Quote(refName)))) + output, err := c.cmd.New(fmt.Sprintf("git merge-base %s %s@{u}", c.cmd.Quote(refName), c.cmd.Quote(refName))).RunWithOutput() if err != nil { return "", err } @@ -407,10 +408,10 @@ func (c *CommitListBuilder) getLogCmd(opts GetCommitsOptions) oscommands.ICmdObj filterFlag := "" if opts.FilterPath != "" { - filterFlag = fmt.Sprintf(" --follow -- %s", c.OSCommand.Quote(opts.FilterPath)) + filterFlag = fmt.Sprintf(" --follow -- %s", c.cmd.Quote(opts.FilterPath)) } - config := c.GitCommand.UserConfig.Git.Log + config := c.UserConfig.Git.Log orderFlag := "--" + config.Order allFlag := "" @@ -418,10 +419,10 @@ func (c *CommitListBuilder) getLogCmd(opts GetCommitsOptions) oscommands.ICmdObj allFlag = " --all" } - return c.OSCommand.NewCmdObj( + return c.cmd.New( fmt.Sprintf( "git log %s %s %s --oneline %s %s --abbrev=%d %s", - c.OSCommand.Quote(opts.RefName), + c.cmd.Quote(opts.RefName), orderFlag, allFlag, prettyFormat, diff --git a/pkg/commands/loading_files.go b/pkg/commands/loading_files.go index df7be0dfe..e28613c07 100644 --- a/pkg/commands/loading_files.go +++ b/pkg/commands/loading_files.go @@ -80,7 +80,7 @@ func (c *GitCommand) GitStatus(opts GitStatusOptions) ([]FileStatus, error) { noRenamesFlag = "--no-renames" } - statusLines, err := c.RunWithOutput(c.NewCmdObj(fmt.Sprintf("git status %s --porcelain -z %s", opts.UntrackedFilesArg, noRenamesFlag))) + statusLines, err := c.Cmd.New(fmt.Sprintf("git status %s --porcelain -z %s", opts.UntrackedFilesArg, noRenamesFlag)).RunWithOutput() if err != nil { return []FileStatus{}, err } diff --git a/pkg/commands/loading_reflog_commits.go b/pkg/commands/loading_reflog_commits.go index 80c2e815d..6e14ab0ce 100644 --- a/pkg/commands/loading_reflog_commits.go +++ b/pkg/commands/loading_reflog_commits.go @@ -18,9 +18,9 @@ func (c *GitCommand) GetReflogCommits(lastReflogCommit *models.Commit, filterPat filterPathArg = fmt.Sprintf(" --follow -- %s", c.OSCommand.Quote(filterPath)) } - cmdObj := c.OSCommand.NewCmdObj(fmt.Sprintf(`git log -g --abbrev=20 --format="%%h %%ct %%gs" %s`, filterPathArg)) + cmdObj := c.OSCommand.Cmd.New(fmt.Sprintf(`git log -g --abbrev=20 --format="%%h %%ct %%gs" %s`, filterPathArg)) onlyObtainedNewReflogCommits := false - err := c.OSCommand.RunLineOutputCmd(cmdObj, func(line string) (bool, error) { + err := cmdObj.RunLineOutputCmd(func(line string) (bool, error) { fields := strings.SplitN(line, " ", 3) if len(fields) <= 2 { return false, nil diff --git a/pkg/commands/loading_remotes.go b/pkg/commands/loading_remotes.go index af460034a..0a581fff5 100644 --- a/pkg/commands/loading_remotes.go +++ b/pkg/commands/loading_remotes.go @@ -10,7 +10,7 @@ import ( ) func (c *GitCommand) GetRemotes() ([]*models.Remote, error) { - remoteBranchesStr, err := c.RunWithOutput(c.NewCmdObj("git branch -r")) + remoteBranchesStr, err := c.Cmd.New("git branch -r").RunWithOutput() if err != nil { return nil, err } diff --git a/pkg/commands/loading_stash.go b/pkg/commands/loading_stash.go index 40867a26b..634dd87fb 100644 --- a/pkg/commands/loading_stash.go +++ b/pkg/commands/loading_stash.go @@ -10,7 +10,7 @@ import ( ) func (c *GitCommand) getUnfilteredStashEntries() []*models.StashEntry { - rawString, _ := c.RunWithOutput(c.NewCmdObj("git stash list --pretty='%gs'")) + rawString, _ := c.Cmd.New("git stash list --pretty='%gs'").RunWithOutput() stashEntries := []*models.StashEntry{} for i, line := range utils.SplitLines(rawString) { stashEntries = append(stashEntries, stashEntryFromLine(line, i)) @@ -24,7 +24,7 @@ func (c *GitCommand) GetStashEntries(filterPath string) []*models.StashEntry { return c.getUnfilteredStashEntries() } - rawString, err := c.RunWithOutput(c.NewCmdObj("git stash list --name-only")) + rawString, err := c.Cmd.New("git stash list --name-only").RunWithOutput() if err != nil { return c.getUnfilteredStashEntries() } diff --git a/pkg/commands/loading_tags.go b/pkg/commands/loading_tags.go index f97985ce9..1bac83e9d 100644 --- a/pkg/commands/loading_tags.go +++ b/pkg/commands/loading_tags.go @@ -10,7 +10,7 @@ import ( func (c *GitCommand) GetTags() ([]*models.Tag, error) { // get remote branches, sorted by creation date (descending) // see: https://git-scm.com/docs/git-tag#Documentation/git-tag.txt---sortltkeygt - remoteBranchesStr, err := c.OSCommand.RunWithOutput(c.NewCmdObj(`git tag --list --sort=-creatordate`)) + remoteBranchesStr, err := c.Cmd.New(`git tag --list --sort=-creatordate`).RunWithOutput() if err != nil { return nil, err } diff --git a/pkg/commands/oscommands/cmd_obj.go b/pkg/commands/oscommands/cmd_obj.go index 6381cf257..0abbcb6b0 100644 --- a/pkg/commands/oscommands/cmd_obj.go +++ b/pkg/commands/oscommands/cmd_obj.go @@ -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) +} 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 931be80b7..5a21413f1 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,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") } diff --git a/pkg/commands/oscommands/os_test.go b/pkg/commands/oscommands/os_test.go index 2c2a58802..424aa72b3 100644 --- a/pkg/commands/oscommands/os_test.go +++ b/pkg/commands/oscommands/os_test.go @@ -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() } } diff --git a/pkg/commands/patch_rebases.go b/pkg/commands/patch_rebases.go index d9947b18e..8f33479bf 100644 --- a/pkg/commands/patch_rebases.go +++ b/pkg/commands/patch_rebases.go @@ -85,12 +85,12 @@ func (c *GitCommand) MovePatchToSelectedCommit(commits []*models.Commit, sourceC todo = a + " " + commit.Sha + " " + commit.Name + "\n" + todo } - cmd, err := c.PrepareInteractiveRebaseCommand(commits[baseIndex].Sha, todo, true) + cmdObj, err := c.PrepareInteractiveRebaseCommand(commits[baseIndex].Sha, todo, true) if err != nil { return err } - if err := c.OSCommand.Run(cmd); err != nil { + if err := cmdObj.Run(); err != nil { return err } @@ -217,7 +217,7 @@ func (c *GitCommand) PullPatchIntoNewCommit(commits []*models.Commit, commitIdx head_message, _ := c.GetHeadCommitMessage() new_message := fmt.Sprintf("Split from \"%s\"", head_message) - err := c.OSCommand.Run(c.CommitCmdObj(new_message, "")) + err := c.CommitCmdObj(new_message, "").Run() if err != nil { return err } diff --git a/pkg/commands/rebasing.go b/pkg/commands/rebasing.go index eb1db85cb..8827246ab 100644 --- a/pkg/commands/rebasing.go +++ b/pkg/commands/rebasing.go @@ -33,12 +33,12 @@ func (c *GitCommand) MoveCommitDown(commits []*models.Commit, index int) error { todo = "pick " + commit.Sha + " " + commit.Name + "\n" + todo } - cmd, err := c.PrepareInteractiveRebaseCommand(commits[index+2].Sha, todo, true) + cmdObj, err := c.PrepareInteractiveRebaseCommand(commits[index+2].Sha, todo, true) if err != nil { return err } - return c.OSCommand.Run(cmd) + return cmdObj.Run() } func (c *GitCommand) InteractiveRebase(commits []*models.Commit, index int, action string) error { @@ -47,12 +47,12 @@ func (c *GitCommand) InteractiveRebase(commits []*models.Commit, index int, acti return err } - cmd, err := c.PrepareInteractiveRebaseCommand(sha, todo, true) + cmdObj, err := c.PrepareInteractiveRebaseCommand(sha, todo, true) if err != nil { return err } - return c.OSCommand.Run(cmd) + return cmdObj.Run() } // PrepareInteractiveRebaseCommand returns the cmd for an interactive rebase @@ -69,7 +69,7 @@ func (c *GitCommand) PrepareInteractiveRebaseCommand(baseSha string, todo string cmdStr := fmt.Sprintf("git rebase --interactive --autostash --keep-empty %s", baseSha) c.Log.WithField("command", cmdStr).Info("RunCommand") - cmdObj := c.NewCmdObj(cmdStr) + cmdObj := c.Cmd.New(cmdStr) gitSequenceEditor := ex if todo == "" { @@ -217,26 +217,22 @@ func (c *GitCommand) BeginInteractiveRebaseForCommit(commits []*models.Commit, c return err } - cmd, err := c.PrepareInteractiveRebaseCommand(sha, todo, true) + cmdObj, err := c.PrepareInteractiveRebaseCommand(sha, todo, true) if err != nil { return err } - if err := c.OSCommand.Run(cmd); err != nil { - return err - } - - return nil + return cmdObj.Run() } // RebaseBranch interactive rebases onto a branch func (c *GitCommand) RebaseBranch(branchName string) error { - cmd, err := c.PrepareInteractiveRebaseCommand(branchName, "", false) + cmdObj, err := c.PrepareInteractiveRebaseCommand(branchName, "", false) if err != nil { return err } - return c.OSCommand.Run(cmd) + return cmdObj.Run() } // GenericMerge takes a commandType of "merge" or "rebase" and a command of "abort", "skip" or "continue" @@ -271,13 +267,14 @@ func (c *GitCommand) GenericMergeOrRebaseAction(commandType string, command stri } func (c *GitCommand) runSkipEditorCommand(command string) error { - cmdObj := c.OSCommand.NewCmdObj(command) + cmdObj := c.OSCommand.Cmd.New(command) lazyGitPath := c.OSCommand.GetLazygitPath() - cmdObj.AddEnvVars( - "LAZYGIT_CLIENT_COMMAND=EXIT_IMMEDIATELY", - "GIT_EDITOR="+lazyGitPath, - "EDITOR="+lazyGitPath, - "VISUAL="+lazyGitPath, - ) - return c.OSCommand.Run(cmdObj) + return cmdObj. + AddEnvVars( + "LAZYGIT_CLIENT_COMMAND=EXIT_IMMEDIATELY", + "GIT_EDITOR="+lazyGitPath, + "EDITOR="+lazyGitPath, + "VISUAL="+lazyGitPath, + ). + Run() } diff --git a/pkg/commands/remotes.go b/pkg/commands/remotes.go index 553f222ad..79d38c029 100644 --- a/pkg/commands/remotes.go +++ b/pkg/commands/remotes.go @@ -7,24 +7,24 @@ import ( ) func (c *GitCommand) AddRemote(name string, url string) error { - return c.Run(c.NewCmdObj(fmt.Sprintf("git remote add %s %s", c.OSCommand.Quote(name), c.OSCommand.Quote(url)))) + return c.Cmd.New(fmt.Sprintf("git remote add %s %s", c.OSCommand.Quote(name), c.OSCommand.Quote(url))).Run() } func (c *GitCommand) RemoveRemote(name string) error { - return c.Run(c.NewCmdObj(fmt.Sprintf("git remote remove %s", c.OSCommand.Quote(name)))) + return c.Cmd.New(fmt.Sprintf("git remote remove %s", c.OSCommand.Quote(name))).Run() } func (c *GitCommand) RenameRemote(oldRemoteName string, newRemoteName string) error { - return c.Run(c.NewCmdObj(fmt.Sprintf("git remote rename %s %s", c.OSCommand.Quote(oldRemoteName), c.OSCommand.Quote(newRemoteName)))) + return c.Cmd.New(fmt.Sprintf("git remote rename %s %s", c.OSCommand.Quote(oldRemoteName), c.OSCommand.Quote(newRemoteName))).Run() } func (c *GitCommand) UpdateRemoteUrl(remoteName string, updatedUrl string) error { - return c.Run(c.NewCmdObj(fmt.Sprintf("git remote set-url %s %s", c.OSCommand.Quote(remoteName), c.OSCommand.Quote(updatedUrl)))) + return c.Cmd.New(fmt.Sprintf("git remote set-url %s %s", c.OSCommand.Quote(remoteName), c.OSCommand.Quote(updatedUrl))).Run() } 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.NewCmdObj(command) + cmdObj := c.Cmd.New(command) return c.DetectUnamePass(cmdObj, promptUserForCredential) } @@ -34,10 +34,10 @@ func (c *GitCommand) DetectUnamePass(cmdObj oscommands.ICmdObj, promptUserForCre // CheckRemoteBranchExists Returns remote branch func (c *GitCommand) CheckRemoteBranchExists(branchName string) bool { - _, err := c.RunWithOutput(c.NewCmdObj( + _, err := c.Cmd.New( fmt.Sprintf("git show-ref --verify -- refs/remotes/origin/%s", c.OSCommand.Quote(branchName), - ))) + )).RunWithOutput() return err == nil } diff --git a/pkg/commands/stash_entries.go b/pkg/commands/stash_entries.go index f80b7a858..16b0806bc 100644 --- a/pkg/commands/stash_entries.go +++ b/pkg/commands/stash_entries.go @@ -4,13 +4,13 @@ import "fmt" // StashDo modify stash func (c *GitCommand) StashDo(index int, method string) error { - return c.Run(c.NewCmdObj(fmt.Sprintf("git stash %s stash@{%d}", method, index))) + return c.Cmd.New(fmt.Sprintf("git stash %s stash@{%d}", method, index)).Run() } // StashSave save stash // TODO: before calling this, check if there is anything to save func (c *GitCommand) StashSave(message string) error { - return c.Run(c.NewCmdObj("git stash save " + c.OSCommand.Quote(message))) + return c.Cmd.New("git stash save " + c.OSCommand.Quote(message)).Run() } // GetStashEntryDiff stash diff @@ -22,7 +22,7 @@ func (c *GitCommand) ShowStashEntryCmdStr(index int) string { // shoutouts to Joe on https://stackoverflow.com/questions/14759748/stashing-only-staged-changes-in-git-is-it-possible func (c *GitCommand) StashSaveStagedChanges(message string) error { // wrap in 'writing', which uses a mutex - if err := c.Run(c.NewCmdObj("git stash --keep-index")); err != nil { + if err := c.Cmd.New("git stash --keep-index").Run(); err != nil { return err } @@ -30,7 +30,7 @@ func (c *GitCommand) StashSaveStagedChanges(message string) error { return err } - if err := c.Run(c.NewCmdObj("git stash apply stash@{1}")); err != nil { + if err := c.Cmd.New("git stash apply stash@{1}").Run(); err != nil { return err } @@ -38,7 +38,7 @@ func (c *GitCommand) StashSaveStagedChanges(message string) error { return err } - if err := c.Run(c.NewCmdObj("git stash drop stash@{1}")); err != nil { + if err := c.Cmd.New("git stash drop stash@{1}").Run(); err != nil { return err } diff --git a/pkg/commands/submodules.go b/pkg/commands/submodules.go index d4343ae19..7e957234b 100644 --- a/pkg/commands/submodules.go +++ b/pkg/commands/submodules.go @@ -71,28 +71,28 @@ func (c *GitCommand) SubmoduleStash(submodule *models.SubmoduleConfig) error { return nil } - return c.Run(c.NewCmdObj("git -C " + c.OSCommand.Quote(submodule.Path) + " stash --include-untracked")) + return c.Cmd.New("git -C " + c.OSCommand.Quote(submodule.Path) + " stash --include-untracked").Run() } func (c *GitCommand) SubmoduleReset(submodule *models.SubmoduleConfig) error { - return c.Run(c.NewCmdObj("git submodule update --init --force -- " + c.OSCommand.Quote(submodule.Path))) + return c.Cmd.New("git submodule update --init --force -- " + c.OSCommand.Quote(submodule.Path)).Run() } func (c *GitCommand) SubmoduleUpdateAll() error { // not doing an --init here because the user probably doesn't want that - return c.Run(c.NewCmdObj("git submodule update --force")) + return c.Cmd.New("git submodule update --force").Run() } func (c *GitCommand) SubmoduleDelete(submodule *models.SubmoduleConfig) error { // based on https://gist.github.com/myusuf3/7f645819ded92bda6677 - if err := c.Run(c.NewCmdObj("git submodule deinit --force -- " + c.OSCommand.Quote(submodule.Path))); err != nil { + if err := c.Cmd.New("git submodule deinit --force -- " + c.OSCommand.Quote(submodule.Path)).Run(); err != nil { if strings.Contains(err.Error(), "did not match any file(s) known to git") { - if err := c.Run(c.NewCmdObj("git config --file .gitmodules --remove-section submodule." + c.OSCommand.Quote(submodule.Name))); err != nil { + if err := c.Cmd.New("git config --file .gitmodules --remove-section submodule." + c.OSCommand.Quote(submodule.Name)).Run(); err != nil { return err } - if err := c.Run(c.NewCmdObj("git config --remove-section submodule." + c.OSCommand.Quote(submodule.Name))); err != nil { + if err := c.Cmd.New("git config --remove-section submodule." + c.OSCommand.Quote(submodule.Name)).Run(); err != nil { return err } @@ -102,7 +102,7 @@ func (c *GitCommand) SubmoduleDelete(submodule *models.SubmoduleConfig) error { } } - if err := c.Run(c.NewCmdObj("git rm --force -r " + submodule.Path)); err != nil { + if err := c.Cmd.New("git rm --force -r " + submodule.Path).Run(); err != nil { // if the directory isn't there then that's fine c.Log.Error(err) } @@ -111,23 +111,24 @@ 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 { // the set-url command is only for later git versions so we're doing it manually here - if err := c.Run(c.NewCmdObj("git config --file .gitmodules submodule." + c.OSCommand.Quote(name) + ".url " + c.OSCommand.Quote(newUrl))); err != nil { + if err := c.Cmd.New("git config --file .gitmodules submodule." + c.OSCommand.Quote(name) + ".url " + c.OSCommand.Quote(newUrl)).Run(); err != nil { return err } - if err := c.Run(c.NewCmdObj("git submodule sync -- " + c.OSCommand.Quote(path))); err != nil { + if err := c.Cmd.New("git submodule sync -- " + c.OSCommand.Quote(path)).Run(); err != nil { return err } @@ -135,27 +136,27 @@ func (c *GitCommand) SubmoduleUpdateUrl(name string, path string, newUrl string) } func (c *GitCommand) SubmoduleInit(path string) error { - return c.Run(c.NewCmdObj("git submodule init -- " + c.OSCommand.Quote(path))) + return c.Cmd.New("git submodule init -- " + c.OSCommand.Quote(path)).Run() } func (c *GitCommand) SubmoduleUpdate(path string) error { - return c.Run(c.NewCmdObj("git submodule update --init -- " + c.OSCommand.Quote(path))) + return c.Cmd.New("git submodule update --init -- " + c.OSCommand.Quote(path)).Run() } func (c *GitCommand) SubmoduleBulkInitCmdObj() oscommands.ICmdObj { - return c.NewCmdObj("git submodule init") + return c.Cmd.New("git submodule init") } func (c *GitCommand) SubmoduleBulkUpdateCmdObj() oscommands.ICmdObj { - return c.NewCmdObj("git submodule update") + return c.Cmd.New("git submodule update") } func (c *GitCommand) SubmoduleForceBulkUpdateCmdObj() oscommands.ICmdObj { - return c.NewCmdObj("git submodule update --force") + return c.Cmd.New("git submodule update --force") } func (c *GitCommand) SubmoduleBulkDeinitCmdObj() oscommands.ICmdObj { - return c.NewCmdObj("git submodule deinit --all --force") + return c.Cmd.New("git submodule deinit --all --force") } func (c *GitCommand) ResetSubmodules(submodules []*models.SubmoduleConfig) error { diff --git a/pkg/commands/sync.go b/pkg/commands/sync.go index 65a70bd0d..05dc745d8 100644 --- a/pkg/commands/sync.go +++ b/pkg/commands/sync.go @@ -37,7 +37,7 @@ func (c *GitCommand) Push(opts PushOpts) error { cmdStr += " " + c.OSCommand.Quote(opts.UpstreamBranch) } - cmdObj := c.NewCmdObj(cmdStr) + cmdObj := c.Cmd.New(cmdStr) return c.DetectUnamePass(cmdObj, opts.PromptUserForCredential) } @@ -58,7 +58,7 @@ func (c *GitCommand) Fetch(opts FetchOptions) error { cmdStr = fmt.Sprintf("%s %s", cmdStr, c.OSCommand.Quote(opts.BranchName)) } - cmdObj := c.NewCmdObj(cmdStr) + cmdObj := c.Cmd.New(cmdStr) return c.DetectUnamePass(cmdObj, func(question string) string { if opts.PromptUserForCredential != nil { return opts.PromptUserForCredential(question) @@ -94,18 +94,18 @@ 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.NewCmdObj(cmdStr).AddEnvVars("GIT_SEQUENCE_EDITOR=:") + cmdObj := c.Cmd.New(cmdStr).AddEnvVars("GIT_SEQUENCE_EDITOR=:") 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.NewCmdObj(cmdStr) + cmdObj := c.Cmd.New(cmdStr) 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.NewCmdObj(cmdStr) + cmdObj := c.Cmd.New(cmdStr) return c.DetectUnamePass(cmdObj, promptUserForCredential) } diff --git a/pkg/commands/tags.go b/pkg/commands/tags.go index b4728d68c..795a44b79 100644 --- a/pkg/commands/tags.go +++ b/pkg/commands/tags.go @@ -5,19 +5,19 @@ import ( ) func (c *GitCommand) CreateLightweightTag(tagName string, commitSha string) error { - return c.Run(c.NewCmdObj(fmt.Sprintf("git tag -- %s %s", c.OSCommand.Quote(tagName), commitSha))) + return c.Cmd.New(fmt.Sprintf("git tag -- %s %s", c.OSCommand.Quote(tagName), commitSha)).Run() } func (c *GitCommand) CreateAnnotatedTag(tagName, commitSha, msg string) error { - return c.Run(c.NewCmdObj(fmt.Sprintf("git tag %s %s -m %s", tagName, commitSha, c.OSCommand.Quote(msg)))) + return c.Cmd.New(fmt.Sprintf("git tag %s %s -m %s", tagName, commitSha, c.OSCommand.Quote(msg))).Run() } func (c *GitCommand) DeleteTag(tagName string) error { - return c.Run(c.NewCmdObj(fmt.Sprintf("git tag -d %s", c.OSCommand.Quote(tagName)))) + return c.Cmd.New(fmt.Sprintf("git tag -d %s", c.OSCommand.Quote(tagName))).Run() } 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.NewCmdObj(cmdStr) + cmdObj := c.Cmd.New(cmdStr) return c.DetectUnamePass(cmdObj, promptUserForCredential) } diff --git a/pkg/gui/commits_panel.go b/pkg/gui/commits_panel.go index edff7a055..f88557cdd 100644 --- a/pkg/gui/commits_panel.go +++ b/pkg/gui/commits_panel.go @@ -119,7 +119,7 @@ func (gui *Gui) refreshCommitsWithLimit() error { gui.Mutexes.BranchCommitsMutex.Lock() defer gui.Mutexes.BranchCommitsMutex.Unlock() - builder := commands.NewCommitListBuilder(gui.Log, gui.GitCommand, gui.OSCommand, gui.Tr) + builder := commands.NewCommitListBuilder(gui.Common, gui.GitCommand, gui.OSCommand) commits, err := builder.GetCommits( commands.GetCommitsOptions{ @@ -142,7 +142,7 @@ func (gui *Gui) refreshRebaseCommits() error { gui.Mutexes.BranchCommitsMutex.Lock() defer gui.Mutexes.BranchCommitsMutex.Unlock() - builder := commands.NewCommitListBuilder(gui.Log, gui.GitCommand, gui.OSCommand, gui.Tr) + builder := commands.NewCommitListBuilder(gui.Common, gui.GitCommand, gui.OSCommand) updatedCommits, err := builder.MergeRebasingCommits(gui.State.Commits) if err != nil { diff --git a/pkg/gui/custom_commands.go b/pkg/gui/custom_commands.go index 3abeee28a..60d7f806c 100644 --- a/pkg/gui/custom_commands.go +++ b/pkg/gui/custom_commands.go @@ -203,7 +203,7 @@ func (gui *Gui) menuPromptFromCommand(prompt config.CustomCommandPrompt, promptR } // Run and save output - message, err := gui.GitCommand.RunWithOutput(gui.GitCommand.NewCmdObj(cmdStr)) + message, err := gui.GitCommand.Cmd.New(cmdStr).RunWithOutput() if err != nil { return gui.surfaceError(err) } @@ -244,7 +244,7 @@ func (gui *Gui) handleCustomCommandKeybinding(customCommand config.CustomCommand } if customCommand.Subprocess { - return gui.runSubprocessWithSuspenseAndRefresh(gui.OSCommand.NewShellCmdObjFromString2(cmdStr)) + return gui.runSubprocessWithSuspenseAndRefresh(gui.OSCommand.Cmd.NewShell(cmdStr)) } loadingText := customCommand.LoadingText @@ -252,8 +252,8 @@ func (gui *Gui) handleCustomCommandKeybinding(customCommand config.CustomCommand loadingText = gui.Tr.LcRunningCustomCommandStatus } return gui.WithWaitingStatus(loadingText, func() error { - cmdObj := gui.OSCommand.NewShellCmdObjFromString(cmdStr) - if err := gui.OSCommand.WithSpan(gui.Tr.Spans.CustomCommand).Run(cmdObj); err != nil { + err := gui.OSCommand.WithSpan(gui.Tr.Spans.CustomCommand).Cmd.NewShell(cmdStr).Run() + if err != nil { return gui.surfaceError(err) } return gui.refreshSidePanels(refreshOptions{}) diff --git a/pkg/gui/diffing.go b/pkg/gui/diffing.go index 8b54e21af..e3b888066 100644 --- a/pkg/gui/diffing.go +++ b/pkg/gui/diffing.go @@ -13,7 +13,7 @@ func (gui *Gui) exitDiffMode() error { } func (gui *Gui) renderDiff() error { - cmdObj := gui.OSCommand.NewCmdObj( + cmdObj := gui.OSCommand.Cmd.New( fmt.Sprintf("git diff --submodule --no-ext-diff --color %s", gui.diffStr()), ) task := NewRunPtyTask(cmdObj.GetCmd()) diff --git a/pkg/gui/files_panel.go b/pkg/gui/files_panel.go index 977f23cae..f1ac1d1a5 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).Cmd.New(cmdStr).Log(), ) } @@ -511,7 +511,7 @@ func (gui *Gui) editFileAtLine(filename string, lineNumber int) error { } return gui.runSubprocessWithSuspenseAndRefresh( - gui.OSCommand.WithSpan(gui.Tr.Spans.EditFile).NewShellCmdObjFromString(cmdStr), + gui.OSCommand.WithSpan(gui.Tr.Spans.EditFile).Cmd.NewShell(cmdStr), ) } @@ -923,7 +923,7 @@ func (gui *Gui) handleCustomCommand() error { gui.OnRunCommand(oscommands.NewCmdLogEntry(command, gui.Tr.Spans.CustomCommand, true)) return gui.runSubprocessWithSuspenseAndRefresh( - gui.OSCommand.NewShellCmdObjFromString2(command), + gui.OSCommand.Cmd.NewShell(command), ) }, }) diff --git a/pkg/gui/git_flow.go b/pkg/gui/git_flow.go index 29e0b2335..3c8066527 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).Cmd.New("git flow " + branchType + " finish " + suffix).Log(), ) } @@ -43,7 +43,7 @@ func (gui *Gui) handleCreateGitFlowMenu() error { } // get config - gitFlowConfig, err := gui.GitCommand.RunWithOutput(gui.GitCommand.NewCmdObj("git config --local --get-regexp gitflow")) + gitFlowConfig, err := gui.GitCommand.Cmd.New("git config --local --get-regexp gitflow").RunWithOutput() if err != nil { return gui.createErrorPanel("You need to install git-flow and enable it in this repo to use git-flow features") } @@ -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).Cmd.New("git flow " + branchType + " start " + name).Log(), ) }, }) diff --git a/pkg/gui/gpg.go b/pkg/gui/gpg.go index 942a5bb89..fe40f8cb8 100644 --- a/pkg/gui/gpg.go +++ b/pkg/gui/gpg.go @@ -15,7 +15,7 @@ import ( func (gui *Gui) withGpgHandling(cmdObj oscommands.ICmdObj, waitingStatus string, onSuccess func() error) error { useSubprocess := gui.GitCommand.UsingGpg() if useSubprocess { - success, err := gui.runSubprocessWithSuspense(gui.OSCommand.NewShellCmdObjFromString(cmdObj.ToString())) + success, err := gui.runSubprocessWithSuspense(gui.OSCommand.Cmd.NewShell(cmdObj.ToString())) if success && onSuccess != nil { if err := onSuccess(); err != nil { return err @@ -33,7 +33,7 @@ func (gui *Gui) withGpgHandling(cmdObj oscommands.ICmdObj, waitingStatus string, func (gui *Gui) RunAndStream(cmdObj oscommands.ICmdObj, waitingStatus string, onSuccess func() error) error { return gui.WithWaitingStatus(waitingStatus, func() error { - cmdObj := gui.OSCommand.NewShellCmdObjFromString(cmdObj.ToString()) + cmdObj := gui.OSCommand.Cmd.NewShell(cmdObj.ToString()) cmdObj.AddEnvVars("TERM=dumb") cmdWriter := gui.getCmdWriter() cmd := cmdObj.GetCmd() diff --git a/pkg/gui/rebase_options_panel.go b/pkg/gui/rebase_options_panel.go index 90108258e..205b9df92 100644 --- a/pkg/gui/rebase_options_panel.go +++ b/pkg/gui/rebase_options_panel.go @@ -58,7 +58,7 @@ func (gui *Gui) genericMergeCommand(command string) error { // it's impossible for a rebase to require a commit so we'll use a subprocess only if it's a merge if status == commands.REBASE_MODE_MERGING && command != REBASE_OPTION_ABORT && gui.UserConfig.Git.Merging.ManualCommit { - sub := gitCommand.NewCmdObj("git " + commandType + " --" + command) + sub := gitCommand.Cmd.New("git " + commandType + " --" + command) if sub != nil { return gui.runSubprocessWithSuspenseAndRefresh(sub) } diff --git a/pkg/gui/recent_repos_panel.go b/pkg/gui/recent_repos_panel.go index 64f99c611..5a7c58edb 100644 --- a/pkg/gui/recent_repos_panel.go +++ b/pkg/gui/recent_repos_panel.go @@ -38,7 +38,7 @@ func (gui *Gui) handleCreateRecentReposMenu() error { } func (gui *Gui) handleShowAllBranchLogs() error { - cmdObj := gui.OSCommand.NewCmdObj( + cmdObj := gui.OSCommand.Cmd.New( gui.UserConfig.Git.AllBranchesLogCmd, ) task := NewRunPtyTask(cmdObj.GetCmd()) diff --git a/pkg/gui/stash_panel.go b/pkg/gui/stash_panel.go index 3064b5a62..1f23ccc37 100644 --- a/pkg/gui/stash_panel.go +++ b/pkg/gui/stash_panel.go @@ -22,7 +22,7 @@ func (gui *Gui) stashRenderToMain() error { if stashEntry == nil { task = NewRenderStringTask(gui.Tr.NoStashEntries) } else { - cmdObj := gui.OSCommand.NewCmdObj( + cmdObj := gui.OSCommand.Cmd.New( gui.GitCommand.ShowStashEntryCmdStr(stashEntry.Index), ) task = NewRunPtyTask(cmdObj.GetCmd()) diff --git a/pkg/gui/sub_commits_panel.go b/pkg/gui/sub_commits_panel.go index fe2d24c40..c212e3caa 100644 --- a/pkg/gui/sub_commits_panel.go +++ b/pkg/gui/sub_commits_panel.go @@ -75,7 +75,7 @@ func (gui *Gui) handleViewSubCommitFiles() error { func (gui *Gui) switchToSubCommitsContext(refName string) error { // need to populate my sub commits - builder := commands.NewCommitListBuilder(gui.Log, gui.GitCommand, gui.OSCommand, gui.Tr) + builder := commands.NewCommitListBuilder(gui.Common, gui.GitCommand, gui.OSCommand) commits, err := builder.GetCommits( commands.GetCommitsOptions{ diff --git a/pkg/gui/submodules_panel.go b/pkg/gui/submodules_panel.go index 01cd130e0..91b28b509 100644 --- a/pkg/gui/submodules_panel.go +++ b/pkg/gui/submodules_panel.go @@ -214,7 +214,8 @@ func (gui *Gui) handleBulkSubmoduleActionsMenu() error { displayStrings: []string{gui.Tr.LcBulkInitSubmodules, style.FgGreen.Sprint(gui.GitCommand.SubmoduleBulkInitCmdObj().ToString())}, onPress: func() error { return gui.WithWaitingStatus(gui.Tr.LcRunningCommand, func() error { - if err := gui.OSCommand.WithSpan(gui.Tr.Spans.BulkInitialiseSubmodules).Run(gui.GitCommand.SubmoduleBulkInitCmdObj()); err != nil { + err := gui.GitCommand.WithSpan(gui.Tr.Spans.BulkInitialiseSubmodules).SubmoduleBulkInitCmdObj().Run() + if err != nil { return gui.surfaceError(err) } @@ -226,7 +227,7 @@ func (gui *Gui) handleBulkSubmoduleActionsMenu() error { displayStrings: []string{gui.Tr.LcBulkUpdateSubmodules, style.FgYellow.Sprint(gui.GitCommand.SubmoduleBulkUpdateCmdObj().ToString())}, onPress: func() error { return gui.WithWaitingStatus(gui.Tr.LcRunningCommand, func() error { - if err := gui.OSCommand.WithSpan(gui.Tr.Spans.BulkUpdateSubmodules).Run(gui.GitCommand.SubmoduleBulkUpdateCmdObj()); err != nil { + if err := gui.GitCommand.WithSpan(gui.Tr.Spans.BulkUpdateSubmodules).SubmoduleBulkUpdateCmdObj().Run(); err != nil { return gui.surfaceError(err) } @@ -250,7 +251,7 @@ func (gui *Gui) handleBulkSubmoduleActionsMenu() error { displayStrings: []string{gui.Tr.LcBulkDeinitSubmodules, style.FgRed.Sprint(gui.GitCommand.SubmoduleBulkDeinitCmdObj().ToString())}, onPress: func() error { return gui.WithWaitingStatus(gui.Tr.LcRunningCommand, func() error { - if err := gui.OSCommand.WithSpan(gui.Tr.Spans.BulkDeinitialiseSubmodules).Run(gui.GitCommand.SubmoduleBulkDeinitCmdObj()); err != nil { + if err := gui.GitCommand.WithSpan(gui.Tr.Spans.BulkDeinitialiseSubmodules).SubmoduleBulkDeinitCmdObj().Run(); err != nil { return gui.surfaceError(err) } diff --git a/pkg/integration/integration.go b/pkg/integration/integration.go index ba49d3bd7..3ae60f740 100644 --- a/pkg/integration/integration.go +++ b/pkg/integration/integration.go @@ -45,7 +45,7 @@ func RunTests( testDir := filepath.Join(rootDir, "test", "integration") osCommand := oscommands.NewDummyOSCommand() - err = osCommand.Run(osCommand.NewCmdObj("go build -o " + tempLazygitPath())) + err = osCommand.Cmd.New("go build -o " + tempLazygitPath()).Run() if err != nil { return err } @@ -319,7 +319,7 @@ func generateSnapshot(dir string) (string, error) { for _, cmdStr := range cmdStrs { // ignoring error for now. If there's an error it could be that there are no results - output, _ := osCommand.RunWithOutput(osCommand.NewCmdObj(cmdStr)) + output, _ := osCommand.Cmd.New(cmdStr).RunWithOutput() snapshot += output + "\n" } @@ -428,7 +428,7 @@ func getLazygitCommand(testPath string, rootDir string, record bool, speed float cmdStr := fmt.Sprintf("%s -debug --use-config-dir=%s --path=%s %s", tempLazygitPath(), configDir, actualDir, extraCmdArgs) - cmdObj := osCommand.NewCmdObj(cmdStr) + cmdObj := osCommand.Cmd.New(cmdStr) cmdObj.AddEnvVars(fmt.Sprintf("SPEED=%f", speed)) if record { diff --git a/pkg/updates/updates.go b/pkg/updates/updates.go index 16397bb9b..ef873fdf7 100644 --- a/pkg/updates/updates.go +++ b/pkg/updates/updates.go @@ -295,8 +295,8 @@ func (u *Updater) downloadAndInstall(rawUrl string) error { } u.Log.Info("untarring tarball/unzipping zip file") - cmdObj := u.OSCommand.NewCmdObj(fmt.Sprintf("tar -zxf %s %s", u.OSCommand.Quote(zipPath), "lazygit")) - if err := u.OSCommand.Run(cmdObj); err != nil { + err = u.OSCommand.Cmd.New(fmt.Sprintf("tar -zxf %s %s", u.OSCommand.Quote(zipPath), "lazygit")).Run() + if err != nil { return err }