From f503ff1ecbfda00dfa4e68e38d41aceaf9b4400c Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Sun, 2 Jan 2022 10:34:33 +1100 Subject: [PATCH] start breaking up git struct --- pkg/app/app.go | 14 +- pkg/commands/branches.go | 153 ++--- pkg/commands/branches_test.go | 42 +- pkg/commands/commits.go | 91 +-- pkg/commands/commits_test.go | 12 +- pkg/commands/config.go | 58 +- pkg/commands/dummies.go | 21 +- pkg/commands/files.go | 363 ++---------- pkg/commands/files_test.go | 594 +------------------ pkg/commands/git.go | 166 +++--- pkg/commands/loaders/branches.go | 12 +- pkg/commands/loaders/commits.go | 20 +- pkg/commands/loaders/files.go | 11 +- pkg/commands/oscommands/cmd_obj.go | 41 ++ pkg/commands/oscommands/cmd_obj_runner.go | 40 +- pkg/commands/oscommands/dummies.go | 4 +- pkg/commands/oscommands/exec_live.go | 31 +- pkg/commands/oscommands/exec_live_default.go | 11 +- pkg/commands/oscommands/exec_live_win.go | 10 +- pkg/commands/oscommands/gui_io.go | 49 ++ pkg/commands/oscommands/os.go | 16 +- pkg/commands/patch_rebases.go | 150 +++-- pkg/commands/rebasing.go | 169 ++++-- pkg/commands/rebasing_test.go | 73 ++- pkg/commands/remotes.go | 64 +- pkg/commands/stash_entries.go | 70 ++- pkg/commands/stash_entries_test.go | 26 +- pkg/commands/status.go | 43 +- pkg/commands/submodules.go | 92 +-- pkg/commands/sync.go | 95 +-- pkg/commands/sync_test.go | 2 +- pkg/commands/tags.go | 34 +- pkg/commands/working_tree.go | 347 +++++++++++ pkg/commands/working_tree_test.go | 558 +++++++++++++++++ pkg/gui/branches_panel.go | 32 +- pkg/gui/cherry_picking.go | 2 +- pkg/gui/commit_files_panel.go | 30 +- pkg/gui/commit_message_panel.go | 2 +- pkg/gui/commits_panel.go | 50 +- pkg/gui/diff_context_size.go | 2 +- pkg/gui/diff_context_size_test.go | 6 +- pkg/gui/discard_changes_menu_panel.go | 8 +- pkg/gui/dummies.go | 3 +- pkg/gui/files_panel.go | 67 ++- pkg/gui/global_handlers.go | 5 +- pkg/gui/gpg.go | 2 +- pkg/gui/gui.go | 34 +- pkg/gui/keybindings.go | 4 +- pkg/gui/layout.go | 2 - pkg/gui/line_by_line_panel.go | 2 +- pkg/gui/list_context_config.go | 2 +- pkg/gui/merge_panel.go | 4 +- pkg/gui/modes.go | 6 +- pkg/gui/patch_building_panel.go | 20 +- pkg/gui/patch_options_panel.go | 24 +- pkg/gui/pty.go | 2 +- pkg/gui/pull_request_menu_panel.go | 2 +- pkg/gui/rebase_options_panel.go | 10 +- pkg/gui/recent_repos_panel.go | 2 +- pkg/gui/reflog_panel.go | 6 +- pkg/gui/remote_branches_panel.go | 6 +- pkg/gui/remotes_panel.go | 10 +- pkg/gui/reset_menu_panel.go | 2 +- pkg/gui/staging_panel.go | 6 +- pkg/gui/stash_panel.go | 57 +- pkg/gui/status_panel.go | 6 +- pkg/gui/sub_commits_panel.go | 2 +- pkg/gui/submodules_panel.go | 36 +- pkg/gui/tags_panel.go | 6 +- pkg/gui/undoing.go | 8 +- pkg/gui/workspace_reset_options_panel.go | 12 +- pkg/i18n/chinese.go | 5 +- pkg/i18n/dutch.go | 5 +- pkg/i18n/english.go | 10 +- pkg/i18n/polish.go | 5 +- pkg/utils/dummies.go | 5 + 76 files changed, 2234 insertions(+), 1758 deletions(-) create mode 100644 pkg/commands/oscommands/gui_io.go create mode 100644 pkg/commands/working_tree.go create mode 100644 pkg/commands/working_tree_test.go diff --git a/pkg/app/app.go b/pkg/app/app.go index c1a28ff1c..99218b0c8 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -32,7 +32,6 @@ type App struct { closers []io.Closer Config config.AppConfigurer OSCommand *oscommands.OSCommand - GitCommand *commands.GitCommand Gui *gui.Gui Updater *updates.Updater // may only need this on the Gui ClientContext string @@ -122,7 +121,7 @@ func NewApp(config config.AppConfigurer, filterPath string) (*App, error) { return app, nil } - app.OSCommand = oscommands.NewOSCommand(app.Common, oscommands.GetPlatform()) + app.OSCommand = oscommands.NewOSCommand(app.Common, oscommands.GetPlatform(), oscommands.NewNullGuiIO(log)) app.Updater, err = updates.NewUpdater(app.Common, config, app.OSCommand) if err != nil { @@ -134,16 +133,9 @@ func NewApp(config config.AppConfigurer, filterPath string) (*App, error) { return app, err } - app.GitCommand, err = commands.NewGitCommand( - app.Common, - app.OSCommand, - git_config.NewStdCachedGitConfig(app.Log), - ) - if err != nil { - return app, err - } + gitConfig := git_config.NewStdCachedGitConfig(app.Log) - app.Gui, err = gui.NewGui(app.Common, app.GitCommand, app.OSCommand, config, app.Updater, filterPath, showRecentRepos) + app.Gui, err = gui.NewGui(app.Common, config, gitConfig, app.Updater, filterPath, showRecentRepos) if err != nil { return app, err } diff --git a/pkg/commands/branches.go b/pkg/commands/branches.go index 2276425cb..2980ae722 100644 --- a/pkg/commands/branches.go +++ b/pkg/commands/branches.go @@ -6,24 +6,47 @@ import ( "strings" "github.com/jesseduffield/lazygit/pkg/commands/oscommands" + "github.com/jesseduffield/lazygit/pkg/common" "github.com/jesseduffield/lazygit/pkg/utils" ) -// NewBranch create new branch -func (c *GitCommand) NewBranch(name string, base string) error { - return c.Cmd.New(fmt.Sprintf("git checkout -b %s %s", c.OSCommand.Quote(name), c.OSCommand.Quote(base))).Run() +// this takes something like: +// * (HEAD detached at 264fc6f5) +// remotes +// and returns '264fc6f5' as the second match +const CurrentBranchNameRegex = `(?m)^\*.*?([^ ]*?)\)?$` + +type BranchCommands struct { + *common.Common + + cmd oscommands.ICmdObjBuilder +} + +func NewBranchCommands( + common *common.Common, + cmd oscommands.ICmdObjBuilder, +) *BranchCommands { + return &BranchCommands{ + Common: common, + cmd: cmd, + } +} + +// New creates a new branch +func (self *BranchCommands) New(name string, base string) error { + return self.cmd.New(fmt.Sprintf("git checkout -b %s %s", self.cmd.Quote(name), self.cmd.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.Cmd.New("git symbolic-ref --short HEAD").DontLog().RunWithOutput() +func (self *BranchCommands) CurrentBranchName() (string, string, error) { + branchName, err := self.cmd.New("git symbolic-ref --short HEAD").DontLog().RunWithOutput() if err == nil && branchName != "HEAD\n" { trimmedBranchName := strings.TrimSpace(branchName) return trimmedBranchName, trimmedBranchName, nil } - output, err := c.Cmd.New("git branch --contains").DontLog().RunWithOutput() + output, err := self.cmd.New("git branch --contains").DontLog().RunWithOutput() if err != nil { return "", "", err } @@ -39,15 +62,15 @@ func (c *GitCommand) CurrentBranchName() (string, string, error) { return "HEAD", "HEAD", nil } -// DeleteBranch delete branch -func (c *GitCommand) DeleteBranch(branch string, force bool) error { +// Delete delete branch +func (self *BranchCommands) Delete(branch string, force bool) error { command := "git branch -d" if force { command = "git branch -D" } - return c.Cmd.New(fmt.Sprintf("%s %s", command, c.OSCommand.Quote(branch))).Run() + return self.cmd.New(fmt.Sprintf("%s %s", command, self.cmd.Quote(branch))).Run() } // Checkout checks out a branch (or commit), with --force if you set the force arg to true @@ -56,13 +79,13 @@ type CheckoutOptions struct { EnvVars []string } -func (c *GitCommand) Checkout(branch string, options CheckoutOptions) error { +func (self *BranchCommands) Checkout(branch string, options CheckoutOptions) error { forceArg := "" if options.Force { forceArg = " --force" } - return c.Cmd.New(fmt.Sprintf("git checkout%s %s", forceArg, c.OSCommand.Quote(branch))). + return self.cmd.New(fmt.Sprintf("git checkout%s %s", forceArg, self.cmd.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"). @@ -70,104 +93,84 @@ func (c *GitCommand) Checkout(branch string, options CheckoutOptions) error { Run() } -// GetBranchGraph gets the color-formatted graph of the log for the given branch +// GetGraph 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.GetBranchGraphCmdObj(branchName).DontLog().RunWithOutput() +func (self *BranchCommands) GetGraph(branchName string) (string, error) { + return self.GetGraphCmdObj(branchName).DontLog().RunWithOutput() } -func (c *GitCommand) GetUpstreamForBranch(branchName string) (string, error) { - output, err := c.Cmd.New(fmt.Sprintf("git rev-parse --abbrev-ref --symbolic-full-name %s@{u}", c.OSCommand.Quote(branchName))).DontLog().RunWithOutput() +func (self *BranchCommands) GetGraphCmdObj(branchName string) oscommands.ICmdObj { + branchLogCmdTemplate := self.UserConfig.Git.BranchLogCmd + templateValues := map[string]string{ + "branchName": self.cmd.Quote(branchName), + } + return self.cmd.New(utils.ResolvePlaceholderString(branchLogCmdTemplate, templateValues)).DontLog() +} + +func (self *BranchCommands) SetCurrentBranchUpstream(upstream string) error { + return self.cmd.New("git branch --set-upstream-to=" + self.cmd.Quote(upstream)).Run() +} + +func (self *BranchCommands) GetUpstream(branchName string) (string, error) { + output, err := self.cmd.New(fmt.Sprintf("git rev-parse --abbrev-ref --symbolic-full-name %s@{u}", self.cmd.Quote(branchName))).DontLog().RunWithOutput() return strings.TrimSpace(output), err } -func (c *GitCommand) GetBranchGraphCmdObj(branchName string) oscommands.ICmdObj { - branchLogCmdTemplate := c.UserConfig.Git.BranchLogCmd - templateValues := map[string]string{ - "branchName": c.OSCommand.Quote(branchName), - } - return c.Cmd.New(utils.ResolvePlaceholderString(branchLogCmdTemplate, templateValues)).DontLog() +func (self *BranchCommands) SetUpstream(remoteName string, remoteBranchName string, branchName string) error { + return self.cmd.New(fmt.Sprintf("git branch --set-upstream-to=%s/%s %s", self.cmd.Quote(remoteName), self.cmd.Quote(remoteBranchName), self.cmd.Quote(branchName))).Run() } -func (c *GitCommand) SetUpstreamBranch(upstream string) error { - return c.Cmd.New("git branch -u " + c.OSCommand.Quote(upstream)).Run() +func (self *BranchCommands) GetCurrentBranchUpstreamDifferenceCount() (string, string) { + return self.GetCommitDifferences("HEAD", "HEAD@{u}") } -func (c *GitCommand) SetBranchUpstream(remoteName string, remoteBranchName string, branchName string) error { - 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) { - return c.GetCommitDifferences("HEAD", "HEAD@{u}") -} - -func (c *GitCommand) GetBranchUpstreamDifferenceCount(branchName string) (string, string) { - return c.GetCommitDifferences(branchName, branchName+"@{u}") +func (self *BranchCommands) GetUpstreamDifferenceCount(branchName string) (string, string) { + return self.GetCommitDifferences(branchName, branchName+"@{u}") } // GetCommitDifferences checks how many pushables/pullables there are for the // current branch -func (c *GitCommand) GetCommitDifferences(from, to string) (string, string) { +func (self *BranchCommands) GetCommitDifferences(from, to string) (string, string) { command := "git rev-list %s..%s --count" - pushableCount, err := c.Cmd.New(fmt.Sprintf(command, to, from)).DontLog().RunWithOutput() + pushableCount, err := self.cmd.New(fmt.Sprintf(command, to, from)).DontLog().RunWithOutput() if err != nil { return "?", "?" } - pullableCount, err := c.Cmd.New(fmt.Sprintf(command, from, to)).DontLog().RunWithOutput() + pullableCount, err := self.cmd.New(fmt.Sprintf(command, from, to)).DontLog().RunWithOutput() if err != nil { return "?", "?" } return strings.TrimSpace(pushableCount), strings.TrimSpace(pullableCount) } +func (self *BranchCommands) IsHeadDetached() bool { + err := self.cmd.New("git symbolic-ref -q HEAD").DontLog().Run() + return err != nil +} + +func (self *BranchCommands) Rename(oldName string, newName string) error { + return self.cmd.New(fmt.Sprintf("git branch --move %s %s", self.cmd.Quote(oldName), self.cmd.Quote(newName))).Run() +} + +func (self *BranchCommands) GetRawBranches() (string, error) { + return self.cmd.New(`git for-each-ref --sort=-committerdate --format="%(HEAD)|%(refname:short)|%(upstream:short)|%(upstream:track)" refs/heads`).DontLog().RunWithOutput() +} + type MergeOpts struct { FastForwardOnly bool } -// Merge merge -func (c *GitCommand) Merge(branchName string, opts MergeOpts) error { +func (self *BranchCommands) Merge(branchName string, opts MergeOpts) error { mergeArg := "" - if c.UserConfig.Git.Merging.Args != "" { - mergeArg = " " + c.UserConfig.Git.Merging.Args + if self.UserConfig.Git.Merging.Args != "" { + mergeArg = " " + self.UserConfig.Git.Merging.Args } - command := fmt.Sprintf("git merge --no-edit%s %s", mergeArg, c.OSCommand.Quote(branchName)) + command := fmt.Sprintf("git merge --no-edit%s %s", mergeArg, self.cmd.Quote(branchName)) if opts.FastForwardOnly { command = fmt.Sprintf("%s --ff-only", command) } - return c.OSCommand.Cmd.New(command).Run() -} - -// AbortMerge abort merge -func (c *GitCommand) AbortMerge() error { - return c.Cmd.New("git merge --abort").Run() -} - -func (c *GitCommand) IsHeadDetached() bool { - err := c.Cmd.New("git symbolic-ref -q HEAD").DontLog().Run() - return err != nil -} - -// ResetHardHead runs `git reset --hard` -func (c *GitCommand) ResetHard(ref string) error { - 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.Cmd.New("git reset --soft " + c.OSCommand.Quote(ref)).Run() -} - -func (c *GitCommand) ResetMixed(ref string) error { - return c.Cmd.New("git reset --mixed " + c.OSCommand.Quote(ref)).Run() -} - -func (c *GitCommand) RenameBranch(oldName string, newName string) error { - 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.Cmd.New(`git for-each-ref --sort=-committerdate --format="%(HEAD)|%(refname:short)|%(upstream:short)|%(upstream:track)" refs/heads`).DontLog().RunWithOutput() + return self.cmd.New(command).Run() } diff --git a/pkg/commands/branches_test.go b/pkg/commands/branches_test.go index 633d4784e..6a2918a6b 100644 --- a/pkg/commands/branches_test.go +++ b/pkg/commands/branches_test.go @@ -42,7 +42,7 @@ func TestGitCommandGetCommitDifferences(t *testing.T) { for _, s := range scenarios { t.Run(s.testName, func(t *testing.T) { gitCmd := NewDummyGitCommandWithRunner(s.runner) - pushables, pullables := gitCmd.GetCommitDifferences("HEAD", "@{u}") + pushables, pullables := gitCmd.Branch.GetCommitDifferences("HEAD", "@{u}") assert.EqualValues(t, s.expectedPushables, pushables) assert.EqualValues(t, s.expectedPullables, pullables) s.runner.CheckForMissingCalls() @@ -55,7 +55,7 @@ func TestGitCommandNewBranch(t *testing.T) { Expect(`git checkout -b "test" "master"`, "", nil) gitCmd := NewDummyGitCommandWithRunner(runner) - assert.NoError(t, gitCmd.NewBranch("test", "master")) + assert.NoError(t, gitCmd.Branch.New("test", "master")) runner.CheckForMissingCalls() } @@ -90,7 +90,7 @@ func TestGitCommandDeleteBranch(t *testing.T) { t.Run(s.testName, func(t *testing.T) { gitCmd := NewDummyGitCommandWithRunner(s.runner) - s.test(gitCmd.DeleteBranch("test", s.force)) + s.test(gitCmd.Branch.Delete("test", s.force)) s.runner.CheckForMissingCalls() }) } @@ -101,7 +101,7 @@ func TestGitCommandMerge(t *testing.T) { Expect(`git merge --no-edit "test"`, "", nil) gitCmd := NewDummyGitCommandWithRunner(runner) - assert.NoError(t, gitCmd.Merge("test", MergeOpts{})) + assert.NoError(t, gitCmd.Branch.Merge("test", MergeOpts{})) runner.CheckForMissingCalls() } @@ -135,7 +135,7 @@ func TestGitCommandCheckout(t *testing.T) { for _, s := range scenarios { t.Run(s.testName, func(t *testing.T) { gitCmd := NewDummyGitCommandWithRunner(s.runner) - s.test(gitCmd.Checkout("test", CheckoutOptions{Force: s.force})) + s.test(gitCmd.Branch.Checkout("test", CheckoutOptions{Force: s.force})) s.runner.CheckForMissingCalls() }) } @@ -146,7 +146,7 @@ func TestGitCommandGetBranchGraph(t *testing.T) { "log", "--graph", "--color=always", "--abbrev-commit", "--decorate", "--date=relative", "--pretty=medium", "test", "--", }, "", nil) gitCmd := NewDummyGitCommandWithRunner(runner) - _, err := gitCmd.GetBranchGraph("test") + _, err := gitCmd.Branch.GetGraph("test") assert.NoError(t, err) } @@ -215,36 +215,8 @@ func TestGitCommandCurrentBranchName(t *testing.T) { for _, s := range scenarios { t.Run(s.testName, func(t *testing.T) { gitCmd := NewDummyGitCommandWithRunner(s.runner) - s.test(gitCmd.CurrentBranchName()) + s.test(gitCmd.Branch.CurrentBranchName()) s.runner.CheckForMissingCalls() }) } } - -func TestGitCommandResetHard(t *testing.T) { - type scenario struct { - testName string - ref string - runner *oscommands.FakeCmdObjRunner - test func(error) - } - - scenarios := []scenario{ - { - "valid case", - "HEAD", - oscommands.NewFakeRunner(t). - Expect(`git reset --hard "HEAD"`, "", nil), - func(err error) { - assert.NoError(t, err) - }, - }, - } - - for _, s := range scenarios { - t.Run(s.testName, func(t *testing.T) { - gitCmd := NewDummyGitCommandWithRunner(s.runner) - s.test(gitCmd.ResetHard(s.ref)) - }) - } -} diff --git a/pkg/commands/commits.go b/pkg/commands/commits.go index ec58de297..ba61df5a5 100644 --- a/pkg/commands/commits.go +++ b/pkg/commands/commits.go @@ -4,18 +4,34 @@ import ( "fmt" "strings" - "github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/commands/oscommands" + "github.com/jesseduffield/lazygit/pkg/common" ) -// RenameCommit renames the topmost commit with the given name -func (c *GitCommand) RenameCommit(name string) error { - return c.Cmd.New("git commit --allow-empty --amend --only -m " + c.OSCommand.Quote(name)).Run() +type CommitCommands struct { + *common.Common + + cmd oscommands.ICmdObjBuilder +} + +func NewCommitCommands( + common *common.Common, + cmd oscommands.ICmdObjBuilder, +) *CommitCommands { + return &CommitCommands{ + Common: common, + cmd: cmd, + } +} + +// RewordLastCommit renames the topmost commit with the given name +func (self *CommitCommands) RewordLastCommit(name string) error { + return self.cmd.New("git commit --allow-empty --amend --only -m " + self.cmd.Quote(name)).Run() } // ResetToCommit reset to commit -func (c *GitCommand) ResetToCommit(sha string, strength string, envVars []string) error { - return c.Cmd.New(fmt.Sprintf("git reset --%s %s", strength, sha)). +func (self *CommitCommands) ResetToCommit(sha string, strength string, envVars []string) error { + return self.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"). @@ -23,11 +39,11 @@ func (c *GitCommand) ResetToCommit(sha string, strength string, envVars []string Run() } -func (c *GitCommand) CommitCmdObj(message string, flags string) oscommands.ICmdObj { +func (self *CommitCommands) CommitCmdObj(message string, flags string) oscommands.ICmdObj { splitMessage := strings.Split(message, "\n") lineArgs := "" for _, line := range splitMessage { - lineArgs += fmt.Sprintf(" -m %s", c.OSCommand.Quote(line)) + lineArgs += fmt.Sprintf(" -m %s", self.cmd.Quote(line)) } flagsStr := "" @@ -35,71 +51,56 @@ func (c *GitCommand) CommitCmdObj(message string, flags string) oscommands.ICmdO flagsStr = fmt.Sprintf(" %s", flags) } - return c.Cmd.New(fmt.Sprintf("git commit%s%s", flagsStr, lineArgs)) + return self.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.Cmd.New("git log -1 --pretty=%s").DontLog().RunWithOutput() +func (self *CommitCommands) GetHeadCommitMessage() (string, error) { + message, err := self.cmd.New("git log -1 --pretty=%s").DontLog().RunWithOutput() return strings.TrimSpace(message), err } -func (c *GitCommand) GetCommitMessage(commitSha string) (string, error) { +func (self *CommitCommands) GetCommitMessage(commitSha string) (string, error) { cmdStr := "git rev-list --format=%B --max-count=1 " + commitSha - messageWithHeader, err := c.Cmd.New(cmdStr).DontLog().RunWithOutput() + messageWithHeader, err := self.cmd.New(cmdStr).DontLog().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.Cmd.New(fmt.Sprintf("git show --no-patch --pretty=format:%%s %s", sha)).DontLog().RunWithOutput() +func (self *CommitCommands) GetCommitMessageFirstLine(sha string) (string, error) { + return self.cmd.New(fmt.Sprintf("git show --no-patch --pretty=format:%%s %s", sha)).DontLog().RunWithOutput() } // AmendHead amends HEAD with whatever is staged in your working tree -func (c *GitCommand) AmendHead() error { - return c.AmendHeadCmdObj().Run() +func (self *CommitCommands) AmendHead() error { + return self.AmendHeadCmdObj().Run() } -func (c *GitCommand) AmendHeadCmdObj() oscommands.ICmdObj { - return c.Cmd.New("git commit --amend --no-edit --allow-empty") +func (self *CommitCommands) AmendHeadCmdObj() oscommands.ICmdObj { + return self.cmd.New("git commit --amend --no-edit --allow-empty") } -func (c *GitCommand) ShowCmdObj(sha string, filterPath string) oscommands.ICmdObj { - contextSize := c.UserConfig.Git.DiffContextSize +func (self *CommitCommands) ShowCmdObj(sha string, filterPath string) oscommands.ICmdObj { + contextSize := self.UserConfig.Git.DiffContextSize filterPathArg := "" if filterPath != "" { - filterPathArg = fmt.Sprintf(" -- %s", c.OSCommand.Quote(filterPath)) + filterPathArg = fmt.Sprintf(" -- %s", self.cmd.Quote(filterPath)) } - cmdStr := fmt.Sprintf("git show --submodule --color=%s --unified=%d --no-renames --stat -p %s %s", c.colorArg(), contextSize, sha, filterPathArg) - return c.Cmd.New(cmdStr).DontLog() + cmdStr := fmt.Sprintf("git show --submodule --color=%s --unified=%d --no-renames --stat -p %s %s", self.UserConfig.Git.Paging.ColorArg, contextSize, sha, filterPathArg) + return self.cmd.New(cmdStr).DontLog() } // Revert reverts the selected commit by sha -func (c *GitCommand) Revert(sha string) error { - return c.Cmd.New(fmt.Sprintf("git revert %s", sha)).Run() +func (self *CommitCommands) Revert(sha string) error { + return self.cmd.New(fmt.Sprintf("git revert %s", sha)).Run() } -func (c *GitCommand) RevertMerge(sha string, parentNumber int) error { - 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 -func (c *GitCommand) CherryPickCommits(commits []*models.Commit) error { - todo := "" - for _, commit := range commits { - todo = "pick " + commit.Sha + " " + commit.Name + "\n" + todo - } - - cmdObj, err := c.PrepareInteractiveRebaseCommand("HEAD", todo, false) - if err != nil { - return err - } - - return cmdObj.Run() +func (self *CommitCommands) RevertMerge(sha string, parentNumber int) error { + return self.cmd.New(fmt.Sprintf("git revert %s -m %d", sha, parentNumber)).Run() } // CreateFixupCommit creates a commit that fixes up a previous commit -func (c *GitCommand) CreateFixupCommit(sha string) error { - return c.Cmd.New(fmt.Sprintf("git commit --fixup=%s", sha)).Run() +func (self *CommitCommands) CreateFixupCommit(sha string) error { + return self.cmd.New(fmt.Sprintf("git commit --fixup=%s", sha)).Run() } diff --git a/pkg/commands/commits_test.go b/pkg/commands/commits_test.go index bdefc4c0e..c994a04f5 100644 --- a/pkg/commands/commits_test.go +++ b/pkg/commands/commits_test.go @@ -7,12 +7,12 @@ import ( "github.com/stretchr/testify/assert" ) -func TestGitCommandRenameCommit(t *testing.T) { +func TestGitCommandRewordCommit(t *testing.T) { runner := oscommands.NewFakeRunner(t). ExpectGitArgs([]string{"commit", "--allow-empty", "--amend", "--only", "-m", "test"}, "", nil) gitCmd := NewDummyGitCommandWithRunner(runner) - assert.NoError(t, gitCmd.RenameCommit("test")) + assert.NoError(t, gitCmd.Commit.RewordLastCommit("test")) runner.CheckForMissingCalls() } @@ -21,7 +21,7 @@ func TestGitCommandResetToCommit(t *testing.T) { ExpectGitArgs([]string{"reset", "--hard", "78976bc"}, "", nil) gitCmd := NewDummyGitCommandWithRunner(runner) - assert.NoError(t, gitCmd.ResetToCommit("78976bc", "hard", []string{})) + assert.NoError(t, gitCmd.Commit.ResetToCommit("78976bc", "hard", []string{})) runner.CheckForMissingCalls() } @@ -57,7 +57,7 @@ func TestGitCommandCommitObj(t *testing.T) { for _, s := range scenarios { t.Run(s.testName, func(t *testing.T) { gitCmd := NewDummyGitCommand() - cmdStr := gitCmd.CommitCmdObj(s.message, s.flags).ToString() + cmdStr := gitCmd.Commit.CommitCmdObj(s.message, s.flags).ToString() assert.Equal(t, s.expected, cmdStr) }) } @@ -86,7 +86,7 @@ func TestGitCommandCreateFixupCommit(t *testing.T) { for _, s := range scenarios { t.Run(s.testName, func(t *testing.T) { gitCmd := NewDummyGitCommandWithRunner(s.runner) - s.test(gitCmd.CreateFixupCommit(s.sha)) + s.test(gitCmd.Commit.CreateFixupCommit(s.sha)) s.runner.CheckForMissingCalls() }) } @@ -125,7 +125,7 @@ func TestGitCommandShowCmdObj(t *testing.T) { t.Run(s.testName, func(t *testing.T) { gitCmd := NewDummyGitCommand() gitCmd.UserConfig.Git.DiffContextSize = s.contextSize - cmdStr := gitCmd.ShowCmdObj("1234567890", s.filterPath).ToString() + cmdStr := gitCmd.Commit.ShowCmdObj("1234567890", s.filterPath).ToString() assert.Equal(t, s.expected, cmdStr) }) } diff --git a/pkg/commands/config.go b/pkg/commands/config.go index 34126788d..db918ec86 100644 --- a/pkg/commands/config.go +++ b/pkg/commands/config.go @@ -5,24 +5,42 @@ import ( "strconv" "strings" + "github.com/jesseduffield/lazygit/pkg/commands/git_config" + "github.com/jesseduffield/lazygit/pkg/common" "github.com/jesseduffield/lazygit/pkg/utils" ) -func (c *GitCommand) ConfiguredPager() string { +type ConfigCommands struct { + *common.Common + + gitConfig git_config.IGitConfig +} + +func NewConfigCommands( + common *common.Common, + gitConfig git_config.IGitConfig, +) *ConfigCommands { + return &ConfigCommands{ + Common: common, + gitConfig: gitConfig, + } +} + +func (self *ConfigCommands) ConfiguredPager() string { if os.Getenv("GIT_PAGER") != "" { return os.Getenv("GIT_PAGER") } if os.Getenv("PAGER") != "" { return os.Getenv("PAGER") } - output := c.GitConfig.Get("core.pager") + output := self.gitConfig.Get("core.pager") return strings.Split(output, "\n")[0] } -func (c *GitCommand) GetPager(width int) string { - useConfig := c.UserConfig.Git.Paging.UseConfig +func (self *ConfigCommands) GetPager(width int) string { + useConfig := self.UserConfig.Git.Paging.UseConfig if useConfig { - pager := c.ConfiguredPager() + pager := self.ConfiguredPager() return strings.Split(pager, "| less")[0] } @@ -30,21 +48,35 @@ func (c *GitCommand) GetPager(width int) string { "columnWidth": strconv.Itoa(width/2 - 6), } - pagerTemplate := c.UserConfig.Git.Paging.Pager + pagerTemplate := self.UserConfig.Git.Paging.Pager return utils.ResolvePlaceholderString(pagerTemplate, templateValues) } -func (c *GitCommand) colorArg() string { - return c.UserConfig.Git.Paging.ColorArg -} - // UsingGpg tells us whether the user has gpg enabled so that we can know // whether we need to run a subprocess to allow them to enter their password -func (c *GitCommand) UsingGpg() bool { - overrideGpg := c.UserConfig.Git.OverrideGpg +func (self *ConfigCommands) UsingGpg() bool { + overrideGpg := self.UserConfig.Git.OverrideGpg if overrideGpg { return false } - return c.GitConfig.GetBool("commit.gpgsign") + return self.gitConfig.GetBool("commit.gpgsign") +} + +func (self *ConfigCommands) GetCoreEditor() string { + return self.gitConfig.Get("core.editor") +} + +// GetRemoteURL returns current repo remote url +func (self *ConfigCommands) GetRemoteURL() string { + return self.gitConfig.Get("remote.origin.url") +} + +func (self *ConfigCommands) GetShowUntrackedFiles() string { + return self.gitConfig.Get("status.showUntrackedFiles") +} + +// this determines whether the user has configured to push to the remote branch of the same name as the current or not +func (self *ConfigCommands) GetPushToCurrent() bool { + return self.gitConfig.Get("push.default") == "current" } diff --git a/pkg/commands/dummies.go b/pkg/commands/dummies.go index ae6ba81e9..1f4b4c1ef 100644 --- a/pkg/commands/dummies.go +++ b/pkg/commands/dummies.go @@ -1,10 +1,6 @@ package commands import ( - "io" - "io/ioutil" - - "github.com/jesseduffield/lazygit/pkg/commands/git_config" "github.com/jesseduffield/lazygit/pkg/commands/oscommands" "github.com/jesseduffield/lazygit/pkg/utils" ) @@ -16,16 +12,13 @@ func NewDummyGitCommand() *GitCommand { // NewDummyGitCommandWithOSCommand creates a new dummy GitCommand for testing func NewDummyGitCommandWithOSCommand(osCommand *oscommands.OSCommand) *GitCommand { - runner := &oscommands.FakeCmdObjRunner{} - builder := oscommands.NewDummyCmdObjBuilder(runner) - - return &GitCommand{ - Common: utils.NewDummyCommon(), - Cmd: builder, - OSCommand: osCommand, - GitConfig: git_config.NewFakeGitConfig(map[string]string{}), - GetCmdWriter: func() io.Writer { return ioutil.Discard }, - } + return NewGitCommandAux( + utils.NewDummyCommon(), + osCommand, + utils.NewDummyGitConfig(), + ".git", + nil, + ) } func NewDummyGitCommandWithRunner(runner oscommands.ICmdObjRunner) *GitCommand { diff --git a/pkg/commands/files.go b/pkg/commands/files.go index acb503d7f..9e17e60e0 100644 --- a/pkg/commands/files.go +++ b/pkg/commands/files.go @@ -1,23 +1,43 @@ package commands import ( - "fmt" "io/ioutil" - "os" - "path/filepath" "strconv" - "time" "github.com/go-errors/errors" - "github.com/jesseduffield/lazygit/pkg/commands/loaders" - "github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/commands/oscommands" - "github.com/jesseduffield/lazygit/pkg/gui/filetree" + "github.com/jesseduffield/lazygit/pkg/common" "github.com/jesseduffield/lazygit/pkg/utils" ) -// CatFile obtains the content of a file -func (c *GitCommand) CatFile(fileName string) (string, error) { +type FileCommands struct { + *common.Common + + cmd oscommands.ICmdObjBuilder + config *ConfigCommands + os FileOSCommand +} + +type FileOSCommand interface { + Getenv(string) string +} + +func NewFileCommands( + common *common.Common, + cmd oscommands.ICmdObjBuilder, + config *ConfigCommands, + osCommand FileOSCommand, +) *FileCommands { + return &FileCommands{ + Common: common, + cmd: cmd, + config: config, + os: osCommand, + } +} + +// Cat obtains the content of a file +func (self *FileCommands) Cat(fileName string) (string, error) { buf, err := ioutil.ReadFile(fileName) if err != nil { return "", nil @@ -25,335 +45,24 @@ func (c *GitCommand) CatFile(fileName string) (string, error) { return string(buf), nil } -func (c *GitCommand) OpenMergeToolCmdObj() oscommands.ICmdObj { - return c.Cmd.New("git mergetool") -} - -func (c *GitCommand) OpenMergeTool() error { - return c.OpenMergeToolCmdObj().Run() -} - -// StageFile stages a file -func (c *GitCommand) StageFile(fileName string) error { - return c.Cmd.New("git add -- " + c.OSCommand.Quote(fileName)).Run() -} - -// StageAll stages all files -func (c *GitCommand) StageAll() error { - return c.Cmd.New("git add -A").Run() -} - -// UnstageAll unstages all files -func (c *GitCommand) UnstageAll() error { - return c.Cmd.New("git reset").Run() -} - -// UnStageFile unstages a file -// we accept an array of filenames for the cases where a file has been renamed i.e. -// we accept the current name and the previous name -func (c *GitCommand) UnStageFile(fileNames []string, reset bool) error { - command := "git rm --cached --force -- %s" - if reset { - command = "git reset HEAD -- %s" - } - - for _, name := range fileNames { - err := c.Cmd.New(fmt.Sprintf(command, c.OSCommand.Quote(name))).Run() - if err != nil { - return err - } - } - return nil -} - -func (c *GitCommand) BeforeAndAfterFileForRename(file *models.File) (*models.File, *models.File, error) { - - if !file.IsRename() { - return nil, nil, errors.New("Expected renamed file") - } - - // we've got a file that represents a rename from one file to another. Here we will refetch - // all files, passing the --no-renames flag and then recursively call the function - // again for the before file and after file. - - filesWithoutRenames := loaders. - NewFileLoader(c.Common, c.Cmd, c.GitConfig). - GetStatusFiles(loaders.GetStatusFileOptions{NoRenames: true}) - - var beforeFile *models.File - var afterFile *models.File - for _, f := range filesWithoutRenames { - if f.Name == file.PreviousName { - beforeFile = f - } - - if f.Name == file.Name { - afterFile = f - } - } - - if beforeFile == nil || afterFile == nil { - return nil, nil, errors.New("Could not find deleted file or new file for file rename") - } - - if beforeFile.IsRename() || afterFile.IsRename() { - // probably won't happen but we want to ensure we don't get an infinite loop - return nil, nil, errors.New("Nested rename found") - } - - return beforeFile, afterFile, nil -} - -// DiscardAllFileChanges directly -func (c *GitCommand) DiscardAllFileChanges(file *models.File) error { - if file.IsRename() { - beforeFile, afterFile, err := c.BeforeAndAfterFileForRename(file) - if err != nil { - return err - } - - if err := c.DiscardAllFileChanges(beforeFile); err != nil { - return err - } - - if err := c.DiscardAllFileChanges(afterFile); err != nil { - return err - } - - return nil - } - - quotedFileName := c.OSCommand.Quote(file.Name) - - if file.ShortStatus == "AA" { - if err := c.Cmd.New("git checkout --ours -- " + quotedFileName).Run(); err != nil { - return err - } - if err := c.Cmd.New("git add -- " + quotedFileName).Run(); err != nil { - return err - } - return nil - } - - if file.ShortStatus == "DU" { - 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.Cmd.New("git reset -- " + quotedFileName).Run(); err != nil { - return err - } - } - - if file.ShortStatus == "DD" || file.ShortStatus == "AU" { - return nil - } - - if file.Added { - return c.OSCommand.RemoveFile(file.Name) - } - return c.DiscardUnstagedFileChanges(file) -} - -func (c *GitCommand) DiscardAllDirChanges(node *filetree.FileNode) error { - // this could be more efficient but we would need to handle all the edge cases - return node.ForEachFile(c.DiscardAllFileChanges) -} - -func (c *GitCommand) DiscardUnstagedDirChanges(node *filetree.FileNode) error { - if err := c.RemoveUntrackedDirFiles(node); err != nil { - return err - } - - quotedPath := c.OSCommand.Quote(node.GetPath()) - if err := c.Cmd.New("git checkout -- " + quotedPath).Run(); err != nil { - return err - } - - return nil -} - -func (c *GitCommand) RemoveUntrackedDirFiles(node *filetree.FileNode) error { - untrackedFilePaths := node.GetPathsMatching( - func(n *filetree.FileNode) bool { return n.File != nil && !n.File.GetIsTracked() }, - ) - - for _, path := range untrackedFilePaths { - err := os.Remove(path) - if err != nil { - return err - } - } - - return nil -} - -// DiscardUnstagedFileChanges directly -func (c *GitCommand) DiscardUnstagedFileChanges(file *models.File) error { - quotedFileName := c.OSCommand.Quote(file.Name) - return c.Cmd.New("git checkout -- " + quotedFileName).Run() -} - -// Ignore adds a file to the gitignore for the repo -func (c *GitCommand) Ignore(filename string) error { - return c.OSCommand.AppendLineToFile(".gitignore", filename) -} - -// 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.WorktreeFileDiffCmdObj(file, plain, cached, ignoreWhitespace).RunWithOutput() - return s -} - -func (c *GitCommand) WorktreeFileDiffCmdObj(node models.IFile, plain bool, cached bool, ignoreWhitespace bool) oscommands.ICmdObj { - cachedArg := "" - trackedArg := "--" - colorArg := c.colorArg() - quotedPath := c.OSCommand.Quote(node.GetPath()) - ignoreWhitespaceArg := "" - contextSize := c.UserConfig.Git.DiffContextSize - if cached { - cachedArg = "--cached" - } - if !node.GetIsTracked() && !node.GetHasStagedChanges() && !cached { - trackedArg = "--no-index -- /dev/null" - } - if plain { - colorArg = "never" - } - if ignoreWhitespace { - ignoreWhitespaceArg = "--ignore-all-space" - } - - 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.Cmd.New(cmdStr).DontLog() -} - -func (c *GitCommand) ApplyPatch(patch string, flags ...string) error { - filepath := filepath.Join(oscommands.GetTempDir(), utils.GetCurrentRepoName(), time.Now().Format("Jan _2 15.04.05.000000000")+".patch") - c.Log.Infof("saving temporary patch to %s", filepath) - if err := c.OSCommand.CreateFileWithContent(filepath, patch); err != nil { - return err - } - - flagStr := "" - for _, flag := range flags { - flagStr += " --" + flag - } - - 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) { - 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 { - colorArg := c.colorArg() - contextSize := c.UserConfig.Git.DiffContextSize - if plain { - colorArg = "never" - } - - reverseFlag := "" - if reverse { - reverseFlag = " -R " - } - - 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))).DontLog() -} - -// CheckoutFile checks out the file for the given commit -func (c *GitCommand) CheckoutFile(commitSha, fileName string) error { - 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 -func (c *GitCommand) DiscardOldFileChanges(commits []*models.Commit, commitIndex int, fileName string) error { - if err := c.BeginInteractiveRebaseForCommit(commits, commitIndex); err != nil { - return err - } - - // check if file exists in previous commit (this command returns an error if the file doesn't exist) - if err := c.Cmd.New("git cat-file -e HEAD^:" + c.OSCommand.Quote(fileName)).DontLog().Run(); err != nil { - if err := c.OSCommand.Remove(fileName); err != nil { - return err - } - if err := c.StageFile(fileName); err != nil { - return err - } - } else if err := c.CheckoutFile("HEAD^", fileName); err != nil { - return err - } - - // amend the commit - err := c.AmendHead() - if err != nil { - return err - } - - // continue - return c.GenericMergeOrRebaseAction("rebase", "continue") -} - -// DiscardAnyUnstagedFileChanges discards any unstages file changes via `git checkout -- .` -func (c *GitCommand) DiscardAnyUnstagedFileChanges() error { - 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.Cmd.New("git rm -r --cached -- " + c.OSCommand.Quote(name)).Run() -} - -// RemoveUntrackedFiles runs `git clean -fd` -func (c *GitCommand) RemoveUntrackedFiles() error { - return c.Cmd.New("git clean -fd").Run() -} - -// ResetAndClean removes all unstaged changes and removes all untracked files -func (c *GitCommand) ResetAndClean() error { - submoduleConfigs, err := c.GetSubmoduleConfigs() - if err != nil { - return err - } - - if len(submoduleConfigs) > 0 { - if err := c.ResetSubmodules(submoduleConfigs); err != nil { - return err - } - } - - if err := c.ResetHard("HEAD"); err != nil { - return err - } - - return c.RemoveUntrackedFiles() -} - -func (c *GitCommand) EditFileCmdStr(filename string, lineNumber int) (string, error) { +func (c *FileCommands) GetEditCmdStr(filename string, lineNumber int) (string, error) { editor := c.UserConfig.OS.EditCommand if editor == "" { - editor = c.GitConfig.Get("core.editor") + editor = c.config.GetCoreEditor() } if editor == "" { - editor = c.OSCommand.Getenv("GIT_EDITOR") + editor = c.os.Getenv("GIT_EDITOR") } if editor == "" { - editor = c.OSCommand.Getenv("VISUAL") + editor = c.os.Getenv("VISUAL") } if editor == "" { - editor = c.OSCommand.Getenv("EDITOR") + editor = c.os.Getenv("EDITOR") } if editor == "" { - if err := c.OSCommand.Cmd.New("which vi").DontLog().Run(); err == nil { + if err := c.cmd.New("which vi").DontLog().Run(); err == nil { editor = "vi" } } @@ -363,7 +72,7 @@ func (c *GitCommand) EditFileCmdStr(filename string, lineNumber int) (string, er templateValues := map[string]string{ "editor": editor, - "filename": c.OSCommand.Quote(filename), + "filename": c.cmd.Quote(filename), "line": strconv.Itoa(lineNumber), } diff --git a/pkg/commands/files_test.go b/pkg/commands/files_test.go index 9c42e7b45..6f03125cb 100644 --- a/pkg/commands/files_test.go +++ b/pkg/commands/files_test.go @@ -1,602 +1,14 @@ package commands import ( - "fmt" - "io/ioutil" - "regexp" "testing" "github.com/go-errors/errors" "github.com/jesseduffield/lazygit/pkg/commands/git_config" - "github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/commands/oscommands" "github.com/stretchr/testify/assert" ) -func TestGitCommandStageFile(t *testing.T) { - runner := oscommands.NewFakeRunner(t). - ExpectGitArgs([]string{"add", "--", "test.txt"}, "", nil) - gitCmd := NewDummyGitCommandWithRunner(runner) - - assert.NoError(t, gitCmd.StageFile("test.txt")) - runner.CheckForMissingCalls() -} - -func TestGitCommandUnstageFile(t *testing.T) { - type scenario struct { - testName string - reset bool - runner *oscommands.FakeCmdObjRunner - test func(error) - } - - scenarios := []scenario{ - { - testName: "Remove an untracked file from staging", - reset: false, - runner: oscommands.NewFakeRunner(t). - ExpectGitArgs([]string{"rm", "--cached", "--force", "--", "test.txt"}, "", nil), - test: func(err error) { - assert.NoError(t, err) - }, - }, - { - testName: "Remove a tracked file from staging", - reset: true, - runner: oscommands.NewFakeRunner(t). - ExpectGitArgs([]string{"reset", "HEAD", "--", "test.txt"}, "", nil), - test: func(err error) { - assert.NoError(t, err) - }, - }, - } - - for _, s := range scenarios { - t.Run(s.testName, func(t *testing.T) { - gitCmd := NewDummyGitCommandWithRunner(s.runner) - s.test(gitCmd.UnStageFile([]string{"test.txt"}, s.reset)) - }) - } -} - -// these tests don't cover everything, in part because we already have an integration -// test which does cover everything. I don't want to unnecessarily assert on the 'how' -// when the 'what' is what matters -func TestGitCommandDiscardAllFileChanges(t *testing.T) { - type scenario struct { - testName string - file *models.File - removeFile func(string) error - runner *oscommands.FakeCmdObjRunner - expectedError string - } - - scenarios := []scenario{ - { - testName: "An error occurred when resetting", - file: &models.File{ - Name: "test", - HasStagedChanges: true, - }, - removeFile: func(string) error { return nil }, - runner: oscommands.NewFakeRunner(t). - ExpectGitArgs([]string{"reset", "--", "test"}, "", errors.New("error")), - expectedError: "error", - }, - { - testName: "An error occurred when removing file", - file: &models.File{ - Name: "test", - Tracked: false, - Added: true, - }, - removeFile: func(string) error { - return fmt.Errorf("an error occurred when removing file") - }, - runner: oscommands.NewFakeRunner(t), - expectedError: "an error occurred when removing file", - }, - { - testName: "An error occurred with checkout", - file: &models.File{ - Name: "test", - Tracked: true, - HasStagedChanges: false, - }, - removeFile: func(string) error { return nil }, - runner: oscommands.NewFakeRunner(t). - ExpectGitArgs([]string{"checkout", "--", "test"}, "", errors.New("error")), - expectedError: "error", - }, - { - testName: "Checkout only", - file: &models.File{ - Name: "test", - Tracked: true, - HasStagedChanges: false, - }, - removeFile: func(string) error { return nil }, - runner: oscommands.NewFakeRunner(t). - ExpectGitArgs([]string{"checkout", "--", "test"}, "", nil), - expectedError: "", - }, - { - testName: "Reset and checkout staged changes", - file: &models.File{ - Name: "test", - Tracked: true, - HasStagedChanges: true, - }, - removeFile: func(string) error { return nil }, - runner: oscommands.NewFakeRunner(t). - ExpectGitArgs([]string{"reset", "--", "test"}, "", nil). - ExpectGitArgs([]string{"checkout", "--", "test"}, "", nil), - expectedError: "", - }, - { - testName: "Reset and checkout merge conflicts", - file: &models.File{ - Name: "test", - Tracked: true, - HasMergeConflicts: true, - }, - removeFile: func(string) error { return nil }, - runner: oscommands.NewFakeRunner(t). - ExpectGitArgs([]string{"reset", "--", "test"}, "", nil). - ExpectGitArgs([]string{"checkout", "--", "test"}, "", nil), - expectedError: "", - }, - { - testName: "Reset and remove", - file: &models.File{ - Name: "test", - Tracked: false, - Added: true, - HasStagedChanges: true, - }, - removeFile: func(filename string) error { - assert.Equal(t, "test", filename) - return nil - }, - runner: oscommands.NewFakeRunner(t). - ExpectGitArgs([]string{"reset", "--", "test"}, "", nil), - expectedError: "", - }, - { - testName: "Remove only", - file: &models.File{ - Name: "test", - Tracked: false, - Added: true, - HasStagedChanges: false, - }, - removeFile: func(filename string) error { - assert.Equal(t, "test", filename) - return nil - }, - runner: oscommands.NewFakeRunner(t), - expectedError: "", - }, - } - - for _, s := range scenarios { - t.Run(s.testName, func(t *testing.T) { - gitCmd := NewDummyGitCommandWithRunner(s.runner) - gitCmd.OSCommand.SetRemoveFile(s.removeFile) - err := gitCmd.DiscardAllFileChanges(s.file) - - if s.expectedError == "" { - assert.Nil(t, err) - } else { - assert.Equal(t, s.expectedError, err.Error()) - } - s.runner.CheckForMissingCalls() - }) - } -} - -func TestGitCommandDiff(t *testing.T) { - type scenario struct { - testName string - file *models.File - plain bool - cached bool - ignoreWhitespace bool - contextSize int - runner *oscommands.FakeCmdObjRunner - } - - const expectedResult = "pretend this is an actual git diff" - - scenarios := []scenario{ - { - testName: "Default case", - file: &models.File{ - Name: "test.txt", - HasStagedChanges: false, - Tracked: true, - }, - plain: false, - cached: false, - ignoreWhitespace: false, - contextSize: 3, - runner: oscommands.NewFakeRunner(t). - ExpectGitArgs([]string{"diff", "--submodule", "--no-ext-diff", "--unified=3", "--color=always", "--", "test.txt"}, expectedResult, nil), - }, - { - testName: "cached", - file: &models.File{ - Name: "test.txt", - HasStagedChanges: false, - Tracked: true, - }, - plain: false, - cached: true, - ignoreWhitespace: false, - contextSize: 3, - runner: oscommands.NewFakeRunner(t). - ExpectGitArgs([]string{"diff", "--submodule", "--no-ext-diff", "--unified=3", "--color=always", "--cached", "--", "test.txt"}, expectedResult, nil), - }, - { - testName: "plain", - file: &models.File{ - Name: "test.txt", - HasStagedChanges: false, - Tracked: true, - }, - plain: true, - cached: false, - ignoreWhitespace: false, - contextSize: 3, - runner: oscommands.NewFakeRunner(t). - ExpectGitArgs([]string{"diff", "--submodule", "--no-ext-diff", "--unified=3", "--color=never", "--", "test.txt"}, expectedResult, nil), - }, - { - testName: "File not tracked and file has no staged changes", - file: &models.File{ - Name: "test.txt", - HasStagedChanges: false, - Tracked: false, - }, - plain: false, - cached: false, - ignoreWhitespace: false, - contextSize: 3, - runner: oscommands.NewFakeRunner(t). - ExpectGitArgs([]string{"diff", "--submodule", "--no-ext-diff", "--unified=3", "--color=always", "--no-index", "--", "/dev/null", "test.txt"}, expectedResult, nil), - }, - { - testName: "Default case (ignore whitespace)", - file: &models.File{ - Name: "test.txt", - HasStagedChanges: false, - Tracked: true, - }, - plain: false, - cached: false, - ignoreWhitespace: true, - contextSize: 3, - runner: oscommands.NewFakeRunner(t). - ExpectGitArgs([]string{"diff", "--submodule", "--no-ext-diff", "--unified=3", "--color=always", "--ignore-all-space", "--", "test.txt"}, expectedResult, nil), - }, - { - testName: "Show diff with custom context size", - file: &models.File{ - Name: "test.txt", - HasStagedChanges: false, - Tracked: true, - }, - plain: false, - cached: false, - ignoreWhitespace: false, - contextSize: 17, - runner: oscommands.NewFakeRunner(t). - ExpectGitArgs([]string{"diff", "--submodule", "--no-ext-diff", "--unified=17", "--color=always", "--", "test.txt"}, expectedResult, nil), - }, - } - - for _, s := range scenarios { - t.Run(s.testName, func(t *testing.T) { - gitCmd := NewDummyGitCommandWithRunner(s.runner) - gitCmd.UserConfig.Git.DiffContextSize = s.contextSize - result := gitCmd.WorktreeFileDiff(s.file, s.plain, s.cached, s.ignoreWhitespace) - assert.Equal(t, expectedResult, result) - s.runner.CheckForMissingCalls() - }) - } -} - -func TestGitCommandShowFileDiff(t *testing.T) { - type scenario struct { - testName string - from string - to string - reverse bool - plain bool - contextSize int - runner *oscommands.FakeCmdObjRunner - } - - const expectedResult = "pretend this is an actual git diff" - - scenarios := []scenario{ - { - testName: "Default case", - from: "1234567890", - to: "0987654321", - reverse: false, - plain: false, - contextSize: 3, - runner: oscommands.NewFakeRunner(t). - ExpectGitArgs([]string{"diff", "--submodule", "--no-ext-diff", "--unified=3", "--no-renames", "--color=always", "1234567890", "0987654321", "--", "test.txt"}, expectedResult, nil), - }, - { - testName: "Show diff with custom context size", - from: "1234567890", - to: "0987654321", - reverse: false, - plain: false, - contextSize: 123, - runner: oscommands.NewFakeRunner(t). - ExpectGitArgs([]string{"diff", "--submodule", "--no-ext-diff", "--unified=123", "--no-renames", "--color=always", "1234567890", "0987654321", "--", "test.txt"}, expectedResult, nil), - }, - } - - for _, s := range scenarios { - t.Run(s.testName, func(t *testing.T) { - gitCmd := NewDummyGitCommandWithRunner(s.runner) - gitCmd.UserConfig.Git.DiffContextSize = s.contextSize - result, err := gitCmd.ShowFileDiff(s.from, s.to, s.reverse, "test.txt", s.plain) - assert.NoError(t, err) - assert.Equal(t, expectedResult, result) - s.runner.CheckForMissingCalls() - }) - } -} - -func TestGitCommandCheckoutFile(t *testing.T) { - type scenario struct { - testName string - commitSha string - fileName string - runner *oscommands.FakeCmdObjRunner - test func(error) - } - - scenarios := []scenario{ - { - testName: "typical case", - commitSha: "11af912", - fileName: "test999.txt", - runner: oscommands.NewFakeRunner(t). - Expect(`git checkout 11af912 -- "test999.txt"`, "", nil), - test: func(err error) { - assert.NoError(t, err) - }, - }, - { - testName: "returns error if there is one", - commitSha: "11af912", - fileName: "test999.txt", - runner: oscommands.NewFakeRunner(t). - Expect(`git checkout 11af912 -- "test999.txt"`, "", errors.New("error")), - test: func(err error) { - assert.Error(t, err) - }, - }, - } - - for _, s := range scenarios { - t.Run(s.testName, func(t *testing.T) { - gitCmd := NewDummyGitCommandWithRunner(s.runner) - s.test(gitCmd.CheckoutFile(s.commitSha, s.fileName)) - s.runner.CheckForMissingCalls() - }) - } -} - -func TestGitCommandApplyPatch(t *testing.T) { - type scenario struct { - testName string - runner *oscommands.FakeCmdObjRunner - test func(error) - } - - expectFn := func(regexStr string, errToReturn error) func(cmdObj oscommands.ICmdObj) (string, error) { - return func(cmdObj oscommands.ICmdObj) (string, error) { - re := regexp.MustCompile(regexStr) - matches := re.FindStringSubmatch(cmdObj.ToString()) - assert.Equal(t, 2, len(matches)) - - filename := matches[1] - - content, err := ioutil.ReadFile(filename) - assert.NoError(t, err) - - assert.Equal(t, "test", string(content)) - - return "", errToReturn - } - } - - scenarios := []scenario{ - { - testName: "valid case", - runner: oscommands.NewFakeRunner(t). - ExpectFunc(expectFn(`git apply --cached "(.*)"`, nil)), - test: func(err error) { - assert.NoError(t, err) - }, - }, - { - testName: "command returns error", - runner: oscommands.NewFakeRunner(t). - ExpectFunc(expectFn(`git apply --cached "(.*)"`, errors.New("error"))), - test: func(err error) { - assert.Error(t, err) - }, - }, - } - - for _, s := range scenarios { - t.Run(s.testName, func(t *testing.T) { - gitCmd := NewDummyGitCommandWithRunner(s.runner) - s.test(gitCmd.ApplyPatch("test", "cached")) - s.runner.CheckForMissingCalls() - }) - } -} - -func TestGitCommandDiscardOldFileChanges(t *testing.T) { - type scenario struct { - testName string - gitConfigMockResponses map[string]string - commits []*models.Commit - commitIndex int - fileName string - runner *oscommands.FakeCmdObjRunner - test func(error) - } - - scenarios := []scenario{ - { - testName: "returns error when index outside of range of commits", - gitConfigMockResponses: nil, - commits: []*models.Commit{}, - commitIndex: 0, - fileName: "test999.txt", - runner: oscommands.NewFakeRunner(t), - test: func(err error) { - assert.Error(t, err) - }, - }, - { - testName: "returns error when using gpg", - gitConfigMockResponses: map[string]string{"commit.gpgsign": "true"}, - commits: []*models.Commit{{Name: "commit", Sha: "123456"}}, - commitIndex: 0, - fileName: "test999.txt", - runner: oscommands.NewFakeRunner(t), - test: func(err error) { - assert.Error(t, err) - }, - }, - { - testName: "checks out file if it already existed", - gitConfigMockResponses: nil, - commits: []*models.Commit{ - {Name: "commit", Sha: "123456"}, - {Name: "commit2", Sha: "abcdef"}, - }, - commitIndex: 0, - fileName: "test999.txt", - runner: oscommands.NewFakeRunner(t). - Expect(`git rebase --interactive --autostash --keep-empty abcdef`, "", nil). - Expect(`git cat-file -e HEAD^:"test999.txt"`, "", nil). - Expect(`git checkout HEAD^ -- "test999.txt"`, "", nil). - Expect(`git commit --amend --no-edit --allow-empty`, "", nil). - Expect(`git rebase --continue`, "", nil), - test: func(err error) { - assert.NoError(t, err) - }, - }, - // test for when the file was created within the commit requires a refactor to support proper mocks - // currently we'd need to mock out the os.Remove function and that's gonna introduce tech debt - } - - for _, s := range scenarios { - t.Run(s.testName, func(t *testing.T) { - gitCmd := NewDummyGitCommandWithRunner(s.runner) - gitCmd.GitConfig = git_config.NewFakeGitConfig(s.gitConfigMockResponses) - s.test(gitCmd.DiscardOldFileChanges(s.commits, s.commitIndex, s.fileName)) - s.runner.CheckForMissingCalls() - }) - } -} - -func TestGitCommandDiscardUnstagedFileChanges(t *testing.T) { - type scenario struct { - testName string - file *models.File - runner *oscommands.FakeCmdObjRunner - test func(error) - } - - scenarios := []scenario{ - { - testName: "valid case", - file: &models.File{Name: "test.txt"}, - runner: oscommands.NewFakeRunner(t). - Expect(`git checkout -- "test.txt"`, "", nil), - test: func(err error) { - assert.NoError(t, err) - }, - }, - } - - for _, s := range scenarios { - t.Run(s.testName, func(t *testing.T) { - gitCmd := NewDummyGitCommandWithRunner(s.runner) - s.test(gitCmd.DiscardUnstagedFileChanges(s.file)) - s.runner.CheckForMissingCalls() - }) - } -} - -func TestGitCommandDiscardAnyUnstagedFileChanges(t *testing.T) { - type scenario struct { - testName string - runner *oscommands.FakeCmdObjRunner - test func(error) - } - - scenarios := []scenario{ - { - testName: "valid case", - runner: oscommands.NewFakeRunner(t). - Expect(`git checkout -- .`, "", nil), - test: func(err error) { - assert.NoError(t, err) - }, - }, - } - - for _, s := range scenarios { - t.Run(s.testName, func(t *testing.T) { - gitCmd := NewDummyGitCommandWithRunner(s.runner) - s.test(gitCmd.DiscardAnyUnstagedFileChanges()) - s.runner.CheckForMissingCalls() - }) - } -} - -func TestGitCommandRemoveUntrackedFiles(t *testing.T) { - type scenario struct { - testName string - runner *oscommands.FakeCmdObjRunner - test func(error) - } - - scenarios := []scenario{ - { - testName: "valid case", - runner: oscommands.NewFakeRunner(t). - Expect(`git clean -fd`, "", nil), - test: func(err error) { - assert.NoError(t, err) - }, - }, - } - - for _, s := range scenarios { - t.Run(s.testName, func(t *testing.T) { - gitCmd := NewDummyGitCommandWithRunner(s.runner) - s.test(gitCmd.RemoveUntrackedFiles()) - s.runner.CheckForMissingCalls() - }) - } -} - func TestEditFileCmdStr(t *testing.T) { type scenario struct { filename string @@ -736,9 +148,9 @@ func TestEditFileCmdStr(t *testing.T) { gitCmd := NewDummyGitCommandWithRunner(s.runner) gitCmd.UserConfig.OS.EditCommand = s.configEditCommand gitCmd.UserConfig.OS.EditCommandTemplate = s.configEditCommandTemplate - gitCmd.OSCommand.Getenv = s.getenv - gitCmd.GitConfig = git_config.NewFakeGitConfig(s.gitConfigMockResponses) - s.test(gitCmd.EditFileCmdStr(s.filename, 1)) + gitCmd.OSCommand.GetenvFn = s.getenv + gitCmd.gitConfig = git_config.NewFakeGitConfig(s.gitConfigMockResponses) + s.test(gitCmd.File.GetEditCmdStr(s.filename, 1)) s.runner.CheckForMissingCalls() } } diff --git a/pkg/commands/git.go b/pkg/commands/git.go index 1026da242..4866655e0 100644 --- a/pkg/commands/git.go +++ b/pkg/commands/git.go @@ -1,7 +1,6 @@ package commands import ( - "io" "io/ioutil" "os" "path/filepath" @@ -19,11 +18,31 @@ import ( "github.com/jesseduffield/lazygit/pkg/utils" ) -// this takes something like: -// * (HEAD detached at 264fc6f5) -// remotes -// and returns '264fc6f5' as the second match -const CurrentBranchNameRegex = `(?m)^\*.*?([^ ]*?)\)?$` +// GitCommand is our main git interface +type GitCommand struct { + *common.Common + OSCommand *oscommands.OSCommand + + Repo *gogit.Repository + + Loaders Loaders + + Cmd oscommands.ICmdObjBuilder + + Submodule *SubmoduleCommands + Tag *TagCommands + WorkingTree *WorkingTreeCommands + File *FileCommands + Branch *BranchCommands + Commit *CommitCommands + Rebase *RebaseCommands + Stash *StashCommands + Status *StatusCommands + Config *ConfigCommands + Patch *PatchCommands + Remote *RemoteCommands + Sync *SyncCommands +} type Loaders struct { Commits *loaders.CommitLoader @@ -36,44 +55,17 @@ type Loaders struct { Tags *loaders.TagLoader } -// GitCommand is our main git interface -type GitCommand struct { - *common.Common - OSCommand *oscommands.OSCommand - Repo *gogit.Repository - DotGitDir string - onSuccessfulContinue func() error - PatchManager *patch.PatchManager - GitConfig git_config.IGitConfig - Loaders Loaders - - // Push to current determines whether the user has configured to push to the remote branch of the same name as the current or not - PushToCurrent bool - - // this is just a view that we write to when running certain commands. - // Coincidentally at the moment it's the same view that OnRunCommand logs to - // but that need not always be the case. - GetCmdWriter func() io.Writer - - Cmd oscommands.ICmdObjBuilder -} - -// NewGitCommand it runs git commands func NewGitCommand( cmn *common.Common, osCommand *oscommands.OSCommand, gitConfig git_config.IGitConfig, ) (*GitCommand, error) { - var repo *gogit.Repository - - pushToCurrent := gitConfig.Get("push.default") == "current" - if err := navigateToRepoRootDirectory(os.Stat, os.Chdir); err != nil { return nil, err } - var err error - if repo, err = setupRepository(gogit.PlainOpen, cmn.Tr.GitconfigParseErr); err != nil { + repo, err := setupRepository(gogit.PlainOpen, cmn.Tr.GitconfigParseErr) + if err != nil { return nil, err } @@ -82,33 +74,81 @@ func NewGitCommand( return nil, err } + return NewGitCommandAux( + cmn, + osCommand, + gitConfig, + dotGitDir, + repo, + ), nil +} + +func NewGitCommandAux( + cmn *common.Common, + osCommand *oscommands.OSCommand, + gitConfig git_config.IGitConfig, + dotGitDir string, + repo *gogit.Repository, +) *GitCommand { cmd := NewGitCmdObjBuilder(cmn.Log, osCommand.Cmd) - gitCommand := &GitCommand{ - Common: cmn, - OSCommand: osCommand, - Repo: repo, - DotGitDir: dotGitDir, - PushToCurrent: pushToCurrent, - GitConfig: gitConfig, - GetCmdWriter: func() io.Writer { return ioutil.Discard }, - Cmd: cmd, + configCommands := NewConfigCommands(cmn, gitConfig) + statusCommands := NewStatusCommands(cmn, osCommand, repo, dotGitDir) + fileLoader := loaders.NewFileLoader(cmn, cmd, configCommands) + remoteCommands := NewRemoteCommands(cmn, cmd) + branchCommands := NewBranchCommands(cmn, cmd) + syncCommands := NewSyncCommands(cmn, cmd) + tagCommands := NewTagCommands(cmn, cmd) + commitCommands := NewCommitCommands(cmn, cmd) + fileCommands := NewFileCommands(cmn, cmd, configCommands, osCommand) + submoduleCommands := NewSubmoduleCommands(cmn, cmd, dotGitDir) + workingTreeCommands := NewWorkingTreeCommands(cmn, cmd, submoduleCommands, osCommand, fileLoader) + rebaseCommands := NewRebaseCommands( + cmn, + cmd, + osCommand, + commitCommands, + workingTreeCommands, + configCommands, + dotGitDir, + ) + stashCommands := NewStashCommands(cmn, cmd, osCommand, fileLoader, workingTreeCommands) + // TODO: have patch manager take workingTreeCommands in its entirety + patchManager := patch.NewPatchManager(cmn.Log, workingTreeCommands.ApplyPatch, workingTreeCommands.ShowFileDiff) + patchCommands := NewPatchCommands(cmn, cmd, rebaseCommands, commitCommands, configCommands, statusCommands, patchManager) + + return &GitCommand{ + Common: cmn, + OSCommand: osCommand, + + Repo: repo, + + Cmd: cmd, + + Submodule: submoduleCommands, + Tag: tagCommands, + WorkingTree: workingTreeCommands, + File: fileCommands, + Branch: branchCommands, + Commit: commitCommands, + Rebase: rebaseCommands, + Config: configCommands, + Stash: stashCommands, + Status: statusCommands, + Patch: patchCommands, + Remote: remoteCommands, + Sync: syncCommands, + Loaders: Loaders{ + Commits: loaders.NewCommitLoader(cmn, cmd, dotGitDir, branchCommands.CurrentBranchName, statusCommands.RebaseMode), + Branches: loaders.NewBranchLoader(cmn, branchCommands.GetRawBranches, branchCommands.CurrentBranchName), + Files: fileLoader, + CommitFiles: loaders.NewCommitFileLoader(cmn, cmd), + Remotes: loaders.NewRemoteLoader(cmn, cmd, repo.Remotes), + ReflogCommits: loaders.NewReflogCommitLoader(cmn, cmd), + Stash: loaders.NewStashLoader(cmn, cmd), + Tags: loaders.NewTagLoader(cmn, cmd), + }, } - - gitCommand.Loaders = Loaders{ - Commits: loaders.NewCommitLoader(cmn, gitCommand), - Branches: loaders.NewBranchLoader(cmn, gitCommand), - Files: loaders.NewFileLoader(cmn, cmd, gitConfig), - CommitFiles: loaders.NewCommitFileLoader(cmn, cmd), - Remotes: loaders.NewRemoteLoader(cmn, cmd, gitCommand.Repo.Remotes), - ReflogCommits: loaders.NewReflogCommitLoader(cmn, cmd), - Stash: loaders.NewStashLoader(cmn, cmd), - Tags: loaders.NewTagLoader(cmn, cmd), - } - - gitCommand.PatchManager = patch.NewPatchManager(gitCommand.Log, gitCommand.ApplyPatch, gitCommand.ShowFileDiff) - - return gitCommand, nil } func navigateToRepoRootDirectory(stat func(string) (os.FileInfo, error), chdir func(string) error) error { @@ -224,11 +264,3 @@ func findDotGitDir(stat func(string) (os.FileInfo, error), readFile func(filenam func VerifyInGitRepo(osCommand *oscommands.OSCommand) error { return osCommand.Cmd.New("git rev-parse --git-dir").DontLog().Run() } - -func (c *GitCommand) GetDotGitDir() string { - return c.DotGitDir -} - -func (c *GitCommand) GetCmd() oscommands.ICmdObjBuilder { - return c.Cmd -} diff --git a/pkg/commands/loaders/branches.go b/pkg/commands/loaders/branches.go index f0e2e43b5..877b228f2 100644 --- a/pkg/commands/loaders/branches.go +++ b/pkg/commands/loaders/branches.go @@ -27,19 +27,15 @@ type BranchLoader struct { getCurrentBranchName func() (string, string, error) } -type BranchLoaderGitCommand interface { - GetRawBranches() (string, error) - CurrentBranchName() (string, string, error) -} - func NewBranchLoader( cmn *common.Common, - gitCommand BranchLoaderGitCommand, + getRawBranches func() (string, error), + getCurrentBranchName func() (string, string, error), ) *BranchLoader { return &BranchLoader{ Common: cmn, - getRawBranches: gitCommand.GetRawBranches, - getCurrentBranchName: gitCommand.CurrentBranchName, + getRawBranches: getRawBranches, + getCurrentBranchName: getCurrentBranchName, } } diff --git a/pkg/commands/loaders/commits.go b/pkg/commands/loaders/commits.go index 28bcbb3a7..cc2442c96 100644 --- a/pkg/commands/loaders/commits.go +++ b/pkg/commands/loaders/commits.go @@ -36,26 +36,22 @@ type CommitLoader struct { dotGitDir string } -type CommitLoaderGitCommand interface { - CurrentBranchName() (string, string, error) - RebaseMode() (enums.RebaseMode, error) - GetCmd() oscommands.ICmdObjBuilder - GetDotGitDir() string -} - // making our dependencies explicit for the sake of easier testing func NewCommitLoader( cmn *common.Common, - gitCommand CommitLoaderGitCommand, + cmd oscommands.ICmdObjBuilder, + dotGitDir string, + getCurrentBranchName func() (string, string, error), + getRebaseMode func() (enums.RebaseMode, error), ) *CommitLoader { return &CommitLoader{ Common: cmn, - cmd: gitCommand.GetCmd(), - getCurrentBranchName: gitCommand.CurrentBranchName, - getRebaseMode: gitCommand.RebaseMode, + cmd: cmd, + getCurrentBranchName: getCurrentBranchName, + getRebaseMode: getRebaseMode, readFile: ioutil.ReadFile, walkFiles: filepath.Walk, - dotGitDir: gitCommand.GetDotGitDir(), + dotGitDir: dotGitDir, } } diff --git a/pkg/commands/loaders/files.go b/pkg/commands/loaders/files.go index 8f6eb32f4..3baa84c7c 100644 --- a/pkg/commands/loaders/files.go +++ b/pkg/commands/loaders/files.go @@ -11,19 +11,24 @@ import ( "github.com/jesseduffield/lazygit/pkg/utils" ) +type FileLoaderConfig interface { + GetShowUntrackedFiles() string +} + type FileLoader struct { *common.Common cmd oscommands.ICmdObjBuilder + config FileLoaderConfig gitConfig git_config.IGitConfig getFileType func(string) string } -func NewFileLoader(cmn *common.Common, cmd oscommands.ICmdObjBuilder, gitConfig git_config.IGitConfig) *FileLoader { +func NewFileLoader(cmn *common.Common, cmd oscommands.ICmdObjBuilder, config FileLoaderConfig) *FileLoader { return &FileLoader{ Common: cmn, cmd: cmd, - gitConfig: gitConfig, getFileType: oscommands.FileType, + config: config, } } @@ -33,7 +38,7 @@ type GetStatusFileOptions struct { func (self *FileLoader) GetStatusFiles(opts GetStatusFileOptions) []*models.File { // check if config wants us ignoring untracked files - untrackedFilesSetting := self.gitConfig.Get("status.showUntrackedFiles") + untrackedFilesSetting := self.config.GetShowUntrackedFiles() if untrackedFilesSetting == "" { untrackedFilesSetting = "all" diff --git a/pkg/commands/oscommands/cmd_obj.go b/pkg/commands/oscommands/cmd_obj.go index 8fb7fa313..bc1e9dc74 100644 --- a/pkg/commands/oscommands/cmd_obj.go +++ b/pkg/commands/oscommands/cmd_obj.go @@ -34,6 +34,11 @@ type ICmdObj interface { // This returns false if DontLog() was called ShouldLog() bool + + PromptOnCredentialRequest() ICmdObj + FailOnCredentialRequest() ICmdObj + + GetCredentialStrategy() CredentialStrategy } type CmdObj struct { @@ -44,8 +49,28 @@ type CmdObj struct { // if set to true, we don't want to log the command to the user. dontLog bool + + // if set to true, it means we might be asked to enter a username/password by this command. + credentialStrategy CredentialStrategy } +type CredentialStrategy int + +const ( + // do not expect a credential request. If we end up getting one + // we'll be in trouble because the command will hang indefinitely + NONE CredentialStrategy = iota + // expect a credential request and if we get one, prompt the user to enter their username/password + PROMPT + // in this case we will check for a credential request (i.e. the command pauses to ask for + // username/password) and if we get one, we just submit a newline, forcing the + // command to fail. We use this e.g. for a background `git fetch` to prevent it + // from hanging indefinitely. + FAIL +) + +var _ ICmdObj = &CmdObj{} + func (self *CmdObj) GetCmd() *exec.Cmd { return self.cmd } @@ -84,3 +109,19 @@ func (self *CmdObj) RunWithOutput() (string, error) { func (self *CmdObj) RunAndProcessLines(onLine func(line string) (bool, error)) error { return self.runner.RunAndProcessLines(self, onLine) } + +func (self *CmdObj) PromptOnCredentialRequest() ICmdObj { + self.credentialStrategy = PROMPT + + return self +} + +func (self *CmdObj) FailOnCredentialRequest() ICmdObj { + self.credentialStrategy = FAIL + + return self +} + +func (self *CmdObj) GetCredentialStrategy() CredentialStrategy { + return self.credentialStrategy +} diff --git a/pkg/commands/oscommands/cmd_obj_runner.go b/pkg/commands/oscommands/cmd_obj_runner.go index 8eb6d437d..dbea23bf3 100644 --- a/pkg/commands/oscommands/cmd_obj_runner.go +++ b/pkg/commands/oscommands/cmd_obj_runner.go @@ -15,18 +15,46 @@ type ICmdObjRunner interface { } type cmdObjRunner struct { - log *logrus.Entry - logCmdObj func(ICmdObj) + log *logrus.Entry + guiIO *guiIO } var _ ICmdObjRunner = &cmdObjRunner{} +func (self *cmdObjRunner) runWithCredentialHandling(cmdObj ICmdObj) error { + switch cmdObj.GetCredentialStrategy() { + case PROMPT: + return self.RunCommandWithOutputLive(cmdObj, self.guiIO.promptForCredentialFn) + case FAIL: + return self.RunCommandWithOutputLive(cmdObj, func(s string) string { return "\n" }) + } + + // we should never land here + return errors.New("runWithCredentialHandling called but cmdObj does not have a a credential strategy") +} + func (self *cmdObjRunner) Run(cmdObj ICmdObj) error { - _, err := self.RunWithOutput(cmdObj) - return err + if cmdObj.GetCredentialStrategy() == NONE { + _, err := self.RunWithOutput(cmdObj) + return err + } else { + return self.runWithCredentialHandling(cmdObj) + } +} + +func (self *cmdObjRunner) logCmdObj(cmdObj ICmdObj) { + self.guiIO.logCommandFn(cmdObj.ToString(), true) } func (self *cmdObjRunner) RunWithOutput(cmdObj ICmdObj) (string, error) { + if cmdObj.GetCredentialStrategy() != NONE { + err := self.runWithCredentialHandling(cmdObj) + // for now we're not capturing output, just because it would take a little more + // effort and there's currently no use case for it. Some commands call RunWithOutput + // but ignore the output, hence why we've got this check here. + return "", err + } + if cmdObj.ShouldLog() { self.logCmdObj(cmdObj) } @@ -39,6 +67,10 @@ func (self *cmdObjRunner) RunWithOutput(cmdObj ICmdObj) (string, error) { } func (self *cmdObjRunner) RunAndProcessLines(cmdObj ICmdObj, onLine func(line string) (bool, error)) error { + if cmdObj.GetCredentialStrategy() != NONE { + return errors.New("cannot call RunAndProcessLines with credential strategy. If you're seeing this then a contributor to Lazygit has accidentally called this method! Please raise an issue") + } + if cmdObj.ShouldLog() { self.logCmdObj(cmdObj) } diff --git a/pkg/commands/oscommands/dummies.go b/pkg/commands/oscommands/dummies.go index 8a97589ed..3d8b1a833 100644 --- a/pkg/commands/oscommands/dummies.go +++ b/pkg/commands/oscommands/dummies.go @@ -6,7 +6,7 @@ import ( // NewDummyOSCommand creates a new dummy OSCommand for testing func NewDummyOSCommand() *OSCommand { - osCmd := NewOSCommand(utils.NewDummyCommon(), dummyPlatform) + osCmd := NewOSCommand(utils.NewDummyCommon(), dummyPlatform, NewNullGuiIO(utils.NewDummyLog())) return osCmd } @@ -27,7 +27,7 @@ var dummyPlatform = &Platform{ } func NewDummyOSCommandWithRunner(runner *FakeCmdObjRunner) *OSCommand { - osCommand := NewOSCommand(utils.NewDummyCommon(), dummyPlatform) + osCommand := NewOSCommand(utils.NewDummyCommon(), dummyPlatform, NewNullGuiIO(utils.NewDummyLog())) osCommand.Cmd = NewDummyCmdObjBuilder(runner) return osCommand diff --git a/pkg/commands/oscommands/exec_live.go b/pkg/commands/oscommands/exec_live.go index 36a7ad0ac..21c1c8d27 100644 --- a/pkg/commands/oscommands/exec_live.go +++ b/pkg/commands/oscommands/exec_live.go @@ -13,12 +13,12 @@ import ( "github.com/jesseduffield/lazygit/pkg/utils" ) -// DetectUnamePass detect a username / password / passphrase question in a command +// RunAndDetectCredentialRequest detect a username / password / passphrase question in a command // promptUserForCredential is a function that gets executed when this function detect you need to fillin a password or passphrase // The promptUserForCredential argument will be "username", "password" or "passphrase" and expects the user's password/passphrase or username back -func (c *OSCommand) DetectUnamePass(cmdObj ICmdObj, writer io.Writer, promptUserForCredential func(string) string) error { +func (self *cmdObjRunner) RunAndDetectCredentialRequest(cmdObj ICmdObj, promptUserForCredential func(string) string) error { ttyText := "" - errMessage := c.RunCommandWithOutputLive(cmdObj, writer, func(word string) string { + err := self.RunCommandWithOutputLive(cmdObj, func(word string) string { ttyText = ttyText + " " + word prompts := map[string]string{ @@ -37,13 +37,7 @@ func (c *OSCommand) DetectUnamePass(cmdObj ICmdObj, writer io.Writer, promptUser return "" }) - return errMessage -} - -// Due to a lack of pty support on windows we have RunCommandWithOutputLiveWrapper being defined -// separate for windows and other OS's -func (c *OSCommand) RunCommandWithOutputLive(cmdObj ICmdObj, writer io.Writer, handleOutput func(string) string) error { - return RunCommandWithOutputLiveWrapper(c, cmdObj, writer, handleOutput) + return err } type cmdHandler struct { @@ -56,23 +50,22 @@ type cmdHandler struct { // Output is a function that executes by every word that gets read by bufio // As return of output you need to give a string that will be written to stdin // NOTE: If the return data is empty it won't write anything to stdin -func RunCommandWithOutputLiveAux( - c *OSCommand, +func (self *cmdObjRunner) RunCommandWithOutputLiveAux( cmdObj ICmdObj, - writer io.Writer, // handleOutput takes a word from stdout and returns a string to be written to stdin. - // See DetectUnamePass above for how this is used to check for a username/password request + // See RunAndDetectCredentialRequest above for how this is used to check for a username/password request handleOutput func(string) string, startCmd func(cmd *exec.Cmd) (*cmdHandler, error), ) error { - c.Log.WithField("command", cmdObj.ToString()).Info("RunCommand") + cmdWriter := self.guiIO.newCmdWriterFn() + self.log.WithField("command", cmdObj.ToString()).Info("RunCommand") if cmdObj.ShouldLog() { - c.LogCommand(cmdObj.ToString(), true) + self.logCmdObj(cmdObj) } cmd := cmdObj.AddEnvVars("LANG=en_US.UTF-8", "LC_ALL=en_US.UTF-8").GetCmd() var stderr bytes.Buffer - cmd.Stderr = io.MultiWriter(writer, &stderr) + cmd.Stderr = io.MultiWriter(cmdWriter, &stderr) handler, err := startCmd(cmd) if err != nil { @@ -81,11 +74,11 @@ func RunCommandWithOutputLiveAux( defer func() { if closeErr := handler.close(); closeErr != nil { - c.Log.Error(closeErr) + self.log.Error(closeErr) } }() - tr := io.TeeReader(handler.stdoutPipe, writer) + tr := io.TeeReader(handler.stdoutPipe, cmdWriter) go utils.Safe(func() { scanner := bufio.NewScanner(tr) diff --git a/pkg/commands/oscommands/exec_live_default.go b/pkg/commands/oscommands/exec_live_default.go index 827db9ed3..5066653c1 100644 --- a/pkg/commands/oscommands/exec_live_default.go +++ b/pkg/commands/oscommands/exec_live_default.go @@ -4,22 +4,19 @@ package oscommands import ( - "io" "os/exec" "github.com/creack/pty" ) -func RunCommandWithOutputLiveWrapper( - c *OSCommand, +// we define this separately for windows and non-windows given that windows does +// not have great PTY support and we need a PTY to handle a credential request +func (self *cmdObjRunner) RunCommandWithOutputLive( cmdObj ICmdObj, - writer io.Writer, output func(string) string, ) error { - return RunCommandWithOutputLiveAux( - c, + return self.RunCommandWithOutputLiveAux( cmdObj, - writer, output, func(cmd *exec.Cmd) (*cmdHandler, error) { ptmx, err := pty.Start(cmd) diff --git a/pkg/commands/oscommands/exec_live_win.go b/pkg/commands/oscommands/exec_live_win.go index 5b61e478b..899e7c7d7 100644 --- a/pkg/commands/oscommands/exec_live_win.go +++ b/pkg/commands/oscommands/exec_live_win.go @@ -26,18 +26,14 @@ func (b *Buffer) Write(p []byte) (n int, err error) { return b.b.Write(p) } -// RunCommandWithOutputLiveWrapper runs a command live but because of windows compatibility this command can't be ran there +// RunCommandWithOutputLive runs a command live but because of windows compatibility this command can't be ran there // TODO: Remove this hack and replace it with a proper way to run commands live on windows. We still have an issue where if a password is requested, the request for a password is written straight to stdout because we can't control the stdout of a subprocess of a subprocess. Keep an eye on https://github.com/creack/pty/pull/109 -func RunCommandWithOutputLiveWrapper( - c *OSCommand, +func (self *cmdObjRunner) RunCommandWithOutputLive( cmdObj ICmdObj, - writer io.Writer, output func(string) string, ) error { - return RunCommandWithOutputLiveAux( - c, + return self.RunCommandWithOutputLiveAux( cmdObj, - writer, output, func(cmd *exec.Cmd) (*cmdHandler, error) { stdoutReader, stdoutWriter := io.Pipe() diff --git a/pkg/commands/oscommands/gui_io.go b/pkg/commands/oscommands/gui_io.go new file mode 100644 index 000000000..ec42f90b3 --- /dev/null +++ b/pkg/commands/oscommands/gui_io.go @@ -0,0 +1,49 @@ +package oscommands + +import ( + "io" + "io/ioutil" + + "github.com/sirupsen/logrus" +) + +// this struct captures some IO stuff +type guiIO struct { + // this is for logging anything we want. It'll be written to a log file for the sake + // of debugging. + log *logrus.Entry + + // this is for us to log the command we're about to run e.g. 'git push'. The GUI + // will write this to a log panel so that the user can see which commands are being + // run. + // The isCommandLineCommand arg is there so that we can style the log differently + // depending on whether we're directly outputting a command we're about to run that + // will be run on the command line, or if we're using something from Go's standard lib. + logCommandFn func(str string, isCommandLineCommand bool) + // this is for us to directly write the output of a command. We will do this for + // certain commands like 'git push'. The GUI will write this to a command output panel. + // We need a new cmd writer per command, hence it being a function. + newCmdWriterFn func() io.Writer + // this allows us to request info from the user like username/password, in the event + // that a command requests it. + // the 'credential' arg is something like 'username' or 'password' + promptForCredentialFn func(credential string) string +} + +func NewGuiIO(log *logrus.Entry, logCommandFn func(string, bool), newCmdWriterFn func() io.Writer, promptForCredentialFn func(string) string) *guiIO { + return &guiIO{ + log: log, + logCommandFn: logCommandFn, + newCmdWriterFn: newCmdWriterFn, + promptForCredentialFn: promptForCredentialFn, + } +} + +func NewNullGuiIO(log *logrus.Entry) *guiIO { + return &guiIO{ + log: log, + logCommandFn: func(string, bool) {}, + newCmdWriterFn: func() io.Writer { return ioutil.Discard }, + promptForCredentialFn: func(string) string { return "" }, + } +} diff --git a/pkg/commands/oscommands/os.go b/pkg/commands/oscommands/os.go index 5beb5da81..a66dfb366 100644 --- a/pkg/commands/oscommands/os.go +++ b/pkg/commands/oscommands/os.go @@ -20,7 +20,7 @@ import ( type OSCommand struct { *common.Common Platform *Platform - Getenv func(string) string + GetenvFn func(string) string // callback to run before running a command, i.e. for the purposes of logging. // the string argument is the command string e.g. 'git add .' and the bool is @@ -43,24 +43,20 @@ type Platform struct { } // NewOSCommand os command runner -func NewOSCommand(common *common.Common, platform *Platform) *OSCommand { +func NewOSCommand(common *common.Common, platform *Platform, guiIO *guiIO) *OSCommand { c := &OSCommand{ Common: common, Platform: platform, - Getenv: os.Getenv, + GetenvFn: os.Getenv, removeFile: os.RemoveAll, } - runner := &cmdObjRunner{log: common.Log, logCmdObj: c.LogCmdObj} + runner := &cmdObjRunner{log: common.Log, guiIO: guiIO} c.Cmd = &CmdObjBuilder{runner: runner, platform: platform} return c } -func (c *OSCommand) LogCmdObj(cmdObj ICmdObj) { - c.LogCommand(cmdObj.ToString(), true) -} - func (c *OSCommand) LogCommand(cmdStr string, commandLine bool) { c.Log.WithField("command", cmdStr).Info("RunCommand") @@ -270,6 +266,10 @@ func (c *OSCommand) RemoveFile(path string) error { return c.removeFile(path) } +func (c *OSCommand) Getenv(key string) string { + return c.GetenvFn(key) +} + func GetTempDir() string { return filepath.Join(os.TempDir(), "lazygit") } diff --git a/pkg/commands/patch_rebases.go b/pkg/commands/patch_rebases.go index c03c8d0a4..e28c2d8e6 100644 --- a/pkg/commands/patch_rebases.go +++ b/pkg/commands/patch_rebases.go @@ -5,64 +5,98 @@ import ( "github.com/go-errors/errors" "github.com/jesseduffield/lazygit/pkg/commands/models" + "github.com/jesseduffield/lazygit/pkg/commands/oscommands" "github.com/jesseduffield/lazygit/pkg/commands/patch" "github.com/jesseduffield/lazygit/pkg/commands/types/enums" + "github.com/jesseduffield/lazygit/pkg/common" ) +type PatchCommands struct { + *common.Common + + cmd oscommands.ICmdObjBuilder + rebase *RebaseCommands + commit *CommitCommands + config *ConfigCommands + stash *StashCommands + status *StatusCommands + PatchManager *patch.PatchManager +} + +func NewPatchCommands( + common *common.Common, + cmd oscommands.ICmdObjBuilder, + rebaseCommands *RebaseCommands, + commitCommands *CommitCommands, + configCommands *ConfigCommands, + statusCommands *StatusCommands, + patchManager *patch.PatchManager, +) *PatchCommands { + return &PatchCommands{ + Common: common, + cmd: cmd, + rebase: rebaseCommands, + commit: commitCommands, + config: configCommands, + status: statusCommands, + PatchManager: patchManager, + } +} + // DeletePatchesFromCommit applies a patch in reverse for a commit -func (c *GitCommand) DeletePatchesFromCommit(commits []*models.Commit, commitIndex int, p *patch.PatchManager) error { - if err := c.BeginInteractiveRebaseForCommit(commits, commitIndex); err != nil { +func (self *PatchCommands) DeletePatchesFromCommit(commits []*models.Commit, commitIndex int) error { + if err := self.rebase.BeginInteractiveRebaseForCommit(commits, commitIndex); err != nil { return err } // apply each patch in reverse - if err := p.ApplyPatches(true); err != nil { - if err := c.GenericMergeOrRebaseAction("rebase", "abort"); err != nil { + if err := self.PatchManager.ApplyPatches(true); err != nil { + if err := self.rebase.GenericMergeOrRebaseAction("rebase", "abort"); err != nil { return err } return err } // time to amend the selected commit - if err := c.AmendHead(); err != nil { + if err := self.commit.AmendHead(); err != nil { return err } - c.onSuccessfulContinue = func() error { - c.PatchManager.Reset() + self.rebase.onSuccessfulContinue = func() error { + self.PatchManager.Reset() return nil } // continue - return c.GenericMergeOrRebaseAction("rebase", "continue") + return self.rebase.GenericMergeOrRebaseAction("rebase", "continue") } -func (c *GitCommand) MovePatchToSelectedCommit(commits []*models.Commit, sourceCommitIdx int, destinationCommitIdx int, p *patch.PatchManager) error { +func (self *PatchCommands) MovePatchToSelectedCommit(commits []*models.Commit, sourceCommitIdx int, destinationCommitIdx int) error { if sourceCommitIdx < destinationCommitIdx { - if err := c.BeginInteractiveRebaseForCommit(commits, destinationCommitIdx); err != nil { + if err := self.rebase.BeginInteractiveRebaseForCommit(commits, destinationCommitIdx); err != nil { return err } // apply each patch forward - if err := p.ApplyPatches(false); err != nil { - if err := c.GenericMergeOrRebaseAction("rebase", "abort"); err != nil { + if err := self.PatchManager.ApplyPatches(false); err != nil { + if err := self.rebase.GenericMergeOrRebaseAction("rebase", "abort"); err != nil { return err } return err } // amend the destination commit - if err := c.AmendHead(); err != nil { + if err := self.commit.AmendHead(); err != nil { return err } - c.onSuccessfulContinue = func() error { - c.PatchManager.Reset() + self.rebase.onSuccessfulContinue = func() error { + self.PatchManager.Reset() return nil } // continue - return c.GenericMergeOrRebaseAction("rebase", "continue") + return self.rebase.GenericMergeOrRebaseAction("rebase", "continue") } if len(commits)-1 < sourceCommitIdx { @@ -72,8 +106,8 @@ func (c *GitCommand) MovePatchToSelectedCommit(commits []*models.Commit, sourceC // we can make this GPG thing possible it just means we need to do this in two parts: // one where we handle the possibility of a credential request, and the other // where we continue the rebase - if c.UsingGpg() { - return errors.New(c.Tr.DisabledForGPG) + if self.config.UsingGpg() { + return errors.New(self.Tr.DisabledForGPG) } baseIndex := sourceCommitIdx + 1 @@ -86,7 +120,7 @@ func (c *GitCommand) MovePatchToSelectedCommit(commits []*models.Commit, sourceC todo = a + " " + commit.Sha + " " + commit.Name + "\n" + todo } - cmdObj, err := c.PrepareInteractiveRebaseCommand(commits[baseIndex].Sha, todo, true) + cmdObj, err := self.rebase.PrepareInteractiveRebaseCommand(commits[baseIndex].Sha, todo, true) if err != nil { return err } @@ -96,62 +130,62 @@ func (c *GitCommand) MovePatchToSelectedCommit(commits []*models.Commit, sourceC } // apply each patch in reverse - if err := p.ApplyPatches(true); err != nil { - if err := c.GenericMergeOrRebaseAction("rebase", "abort"); err != nil { + if err := self.PatchManager.ApplyPatches(true); err != nil { + if err := self.rebase.GenericMergeOrRebaseAction("rebase", "abort"); err != nil { return err } return err } // amend the source commit - if err := c.AmendHead(); err != nil { + if err := self.commit.AmendHead(); err != nil { return err } - if c.onSuccessfulContinue != nil { + if self.rebase.onSuccessfulContinue != nil { return errors.New("You are midway through another rebase operation. Please abort to start again") } - c.onSuccessfulContinue = func() error { + self.rebase.onSuccessfulContinue = func() error { // now we should be up to the destination, so let's apply forward these patches to that. // ideally we would ensure we're on the right commit but I'm not sure if that check is necessary - if err := p.ApplyPatches(false); err != nil { - if err := c.GenericMergeOrRebaseAction("rebase", "abort"); err != nil { + if err := self.PatchManager.ApplyPatches(false); err != nil { + if err := self.rebase.GenericMergeOrRebaseAction("rebase", "abort"); err != nil { return err } return err } // amend the destination commit - if err := c.AmendHead(); err != nil { + if err := self.commit.AmendHead(); err != nil { return err } - c.onSuccessfulContinue = func() error { - c.PatchManager.Reset() + self.rebase.onSuccessfulContinue = func() error { + self.PatchManager.Reset() return nil } - return c.GenericMergeOrRebaseAction("rebase", "continue") + return self.rebase.GenericMergeOrRebaseAction("rebase", "continue") } - return c.GenericMergeOrRebaseAction("rebase", "continue") + return self.rebase.GenericMergeOrRebaseAction("rebase", "continue") } -func (c *GitCommand) MovePatchIntoIndex(commits []*models.Commit, commitIdx int, p *patch.PatchManager, stash bool) error { +func (self *PatchCommands) MovePatchIntoIndex(commits []*models.Commit, commitIdx int, stash bool) error { if stash { - if err := c.StashSave(c.Tr.StashPrefix + commits[commitIdx].Sha); err != nil { + if err := self.stash.Save(self.Tr.StashPrefix + commits[commitIdx].Sha); err != nil { return err } } - if err := c.BeginInteractiveRebaseForCommit(commits, commitIdx); err != nil { + if err := self.rebase.BeginInteractiveRebaseForCommit(commits, commitIdx); err != nil { return err } - if err := p.ApplyPatches(true); err != nil { - if c.WorkingTreeState() == enums.REBASE_MODE_REBASING { - if err := c.GenericMergeOrRebaseAction("rebase", "abort"); err != nil { + if err := self.PatchManager.ApplyPatches(true); err != nil { + if self.status.WorkingTreeState() == enums.REBASE_MODE_REBASING { + if err := self.rebase.GenericMergeOrRebaseAction("rebase", "abort"); err != nil { return err } } @@ -159,19 +193,19 @@ func (c *GitCommand) MovePatchIntoIndex(commits []*models.Commit, commitIdx int, } // amend the commit - if err := c.AmendHead(); err != nil { + if err := self.commit.AmendHead(); err != nil { return err } - if c.onSuccessfulContinue != nil { + if self.rebase.onSuccessfulContinue != nil { return errors.New("You are midway through another rebase operation. Please abort to start again") } - c.onSuccessfulContinue = func() error { + self.rebase.onSuccessfulContinue = func() error { // add patches to index - if err := p.ApplyPatches(false); err != nil { - if c.WorkingTreeState() == enums.REBASE_MODE_REBASING { - if err := c.GenericMergeOrRebaseAction("rebase", "abort"); err != nil { + if err := self.PatchManager.ApplyPatches(false); err != nil { + if self.status.WorkingTreeState() == enums.REBASE_MODE_REBASING { + if err := self.rebase.GenericMergeOrRebaseAction("rebase", "abort"); err != nil { return err } } @@ -179,54 +213,54 @@ func (c *GitCommand) MovePatchIntoIndex(commits []*models.Commit, commitIdx int, } if stash { - if err := c.StashDo(0, "apply"); err != nil { + if err := self.stash.Apply(0); err != nil { return err } } - c.PatchManager.Reset() + self.PatchManager.Reset() return nil } - return c.GenericMergeOrRebaseAction("rebase", "continue") + return self.rebase.GenericMergeOrRebaseAction("rebase", "continue") } -func (c *GitCommand) PullPatchIntoNewCommit(commits []*models.Commit, commitIdx int, p *patch.PatchManager) error { - if err := c.BeginInteractiveRebaseForCommit(commits, commitIdx); err != nil { +func (self *PatchCommands) PullPatchIntoNewCommit(commits []*models.Commit, commitIdx int) error { + if err := self.rebase.BeginInteractiveRebaseForCommit(commits, commitIdx); err != nil { return err } - if err := p.ApplyPatches(true); err != nil { - if err := c.GenericMergeOrRebaseAction("rebase", "abort"); err != nil { + if err := self.PatchManager.ApplyPatches(true); err != nil { + if err := self.rebase.GenericMergeOrRebaseAction("rebase", "abort"); err != nil { return err } return err } // amend the commit - if err := c.AmendHead(); err != nil { + if err := self.commit.AmendHead(); err != nil { return err } // add patches to index - if err := p.ApplyPatches(false); err != nil { - if err := c.GenericMergeOrRebaseAction("rebase", "abort"); err != nil { + if err := self.PatchManager.ApplyPatches(false); err != nil { + if err := self.rebase.GenericMergeOrRebaseAction("rebase", "abort"); err != nil { return err } return err } - head_message, _ := c.GetHeadCommitMessage() + head_message, _ := self.commit.GetHeadCommitMessage() new_message := fmt.Sprintf("Split from \"%s\"", head_message) - err := c.CommitCmdObj(new_message, "").Run() + err := self.commit.CommitCmdObj(new_message, "").Run() if err != nil { return err } - if c.onSuccessfulContinue != nil { + if self.rebase.onSuccessfulContinue != nil { return errors.New("You are midway through another rebase operation. Please abort to start again") } - c.PatchManager.Reset() - return c.GenericMergeOrRebaseAction("rebase", "continue") + self.PatchManager.Reset() + return self.rebase.GenericMergeOrRebaseAction("rebase", "continue") } diff --git a/pkg/commands/rebasing.go b/pkg/commands/rebasing.go index b930604ea..e845c5ed3 100644 --- a/pkg/commands/rebasing.go +++ b/pkg/commands/rebasing.go @@ -9,22 +9,56 @@ import ( "github.com/go-errors/errors" "github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/commands/oscommands" + "github.com/jesseduffield/lazygit/pkg/common" ) -func (c *GitCommand) RewordCommit(commits []*models.Commit, index int) (oscommands.ICmdObj, error) { - todo, sha, err := c.GenerateGenericRebaseTodo(commits, index, "reword") +type RebaseCommands struct { + *common.Common + + cmd oscommands.ICmdObjBuilder + osCommand *oscommands.OSCommand + + commit *CommitCommands + workingTree *WorkingTreeCommands + config *ConfigCommands + dotGitDir string + onSuccessfulContinue func() error +} + +func NewRebaseCommands( + common *common.Common, + cmd oscommands.ICmdObjBuilder, + osCommand *oscommands.OSCommand, + commitCommands *CommitCommands, + workingTreeCommands *WorkingTreeCommands, + configCommands *ConfigCommands, + dotGitDir string, +) *RebaseCommands { + return &RebaseCommands{ + Common: common, + cmd: cmd, + osCommand: osCommand, + commit: commitCommands, + workingTree: workingTreeCommands, + config: configCommands, + dotGitDir: dotGitDir, + } +} + +func (self *RebaseCommands) RewordCommit(commits []*models.Commit, index int) (oscommands.ICmdObj, error) { + todo, sha, err := self.GenerateGenericRebaseTodo(commits, index, "reword") if err != nil { return nil, err } - return c.PrepareInteractiveRebaseCommand(sha, todo, false) + return self.PrepareInteractiveRebaseCommand(sha, todo, false) } -func (c *GitCommand) MoveCommitDown(commits []*models.Commit, index int) error { +func (self *RebaseCommands) MoveCommitDown(commits []*models.Commit, index int) error { // we must ensure that we have at least two commits after the selected one if len(commits) <= index+2 { // assuming they aren't picking the bottom commit - return errors.New(c.Tr.NoRoom) + return errors.New(self.Tr.NoRoom) } todo := "" @@ -33,7 +67,7 @@ func (c *GitCommand) MoveCommitDown(commits []*models.Commit, index int) error { todo = "pick " + commit.Sha + " " + commit.Name + "\n" + todo } - cmdObj, err := c.PrepareInteractiveRebaseCommand(commits[index+2].Sha, todo, true) + cmdObj, err := self.PrepareInteractiveRebaseCommand(commits[index+2].Sha, todo, true) if err != nil { return err } @@ -41,13 +75,13 @@ func (c *GitCommand) MoveCommitDown(commits []*models.Commit, index int) error { return cmdObj.Run() } -func (c *GitCommand) InteractiveRebase(commits []*models.Commit, index int, action string) error { - todo, sha, err := c.GenerateGenericRebaseTodo(commits, index, action) +func (self *RebaseCommands) InteractiveRebase(commits []*models.Commit, index int, action string) error { + todo, sha, err := self.GenerateGenericRebaseTodo(commits, index, action) if err != nil { return err } - cmdObj, err := c.PrepareInteractiveRebaseCommand(sha, todo, true) + cmdObj, err := self.PrepareInteractiveRebaseCommand(sha, todo, true) if err != nil { return err } @@ -58,24 +92,24 @@ func (c *GitCommand) InteractiveRebase(commits []*models.Commit, index int, acti // PrepareInteractiveRebaseCommand returns the cmd for an interactive rebase // we tell git to run lazygit to edit the todo list, and we pass the client // lazygit a todo string to write to the todo file -func (c *GitCommand) PrepareInteractiveRebaseCommand(baseSha string, todo string, overrideEditor bool) (oscommands.ICmdObj, error) { +func (self *RebaseCommands) PrepareInteractiveRebaseCommand(baseSha string, todo string, overrideEditor bool) (oscommands.ICmdObj, error) { ex := oscommands.GetLazygitPath() debug := "FALSE" - if c.Debug { + if self.Debug { debug = "TRUE" } cmdStr := fmt.Sprintf("git rebase --interactive --autostash --keep-empty %s", baseSha) - c.Log.WithField("command", cmdStr).Info("RunCommand") + self.Log.WithField("command", cmdStr).Info("RunCommand") - cmdObj := c.Cmd.New(cmdStr) + cmdObj := self.cmd.New(cmdStr) gitSequenceEditor := ex if todo == "" { gitSequenceEditor = "true" } else { - c.OSCommand.LogCommand(fmt.Sprintf("Creating TODO file for interactive rebase: \n\n%s", todo), false) + self.osCommand.LogCommand(fmt.Sprintf("Creating TODO file for interactive rebase: \n\n%s", todo), false) } cmdObj.AddEnvVars( @@ -94,18 +128,18 @@ func (c *GitCommand) PrepareInteractiveRebaseCommand(baseSha string, todo string return cmdObj, nil } -func (c *GitCommand) GenerateGenericRebaseTodo(commits []*models.Commit, actionIndex int, action string) (string, string, error) { +func (self *RebaseCommands) GenerateGenericRebaseTodo(commits []*models.Commit, actionIndex int, action string) (string, string, error) { baseIndex := actionIndex + 1 if len(commits) <= baseIndex { - return "", "", errors.New(c.Tr.CannotRebaseOntoFirstCommit) + return "", "", errors.New(self.Tr.CannotRebaseOntoFirstCommit) } if action == "squash" || action == "fixup" { baseIndex++ if len(commits) <= baseIndex { - return "", "", errors.New(c.Tr.CannotSquashOntoSecondCommit) + return "", "", errors.New(self.Tr.CannotSquashOntoSecondCommit) } } @@ -129,24 +163,24 @@ func (c *GitCommand) GenerateGenericRebaseTodo(commits []*models.Commit, actionI } // AmendTo amends the given commit with whatever files are staged -func (c *GitCommand) AmendTo(sha string) error { - if err := c.CreateFixupCommit(sha); err != nil { +func (self *RebaseCommands) AmendTo(sha string) error { + if err := self.commit.CreateFixupCommit(sha); err != nil { return err } - return c.SquashAllAboveFixupCommits(sha) + return self.SquashAllAboveFixupCommits(sha) } // EditRebaseTodo sets the action at a given index in the git-rebase-todo file -func (c *GitCommand) EditRebaseTodo(index int, action string) error { - fileName := filepath.Join(c.DotGitDir, "rebase-merge/git-rebase-todo") +func (self *RebaseCommands) EditRebaseTodo(index int, action string) error { + fileName := filepath.Join(self.dotGitDir, "rebase-merge/git-rebase-todo") bytes, err := ioutil.ReadFile(fileName) if err != nil { return err } content := strings.Split(string(bytes), "\n") - commitCount := c.getTodoCommitCount(content) + commitCount := self.getTodoCommitCount(content) // we have the most recent commit at the bottom whereas the todo file has // it at the bottom, so we need to subtract our index from the commit count @@ -158,7 +192,7 @@ func (c *GitCommand) EditRebaseTodo(index int, action string) error { return ioutil.WriteFile(fileName, []byte(result), 0644) } -func (c *GitCommand) getTodoCommitCount(content []string) int { +func (self *RebaseCommands) getTodoCommitCount(content []string) int { // count lines that are not blank and are not comments commitCount := 0 for _, line := range content { @@ -170,15 +204,15 @@ func (c *GitCommand) getTodoCommitCount(content []string) int { } // MoveTodoDown moves a rebase todo item down by one position -func (c *GitCommand) MoveTodoDown(index int) error { - fileName := filepath.Join(c.DotGitDir, "rebase-merge/git-rebase-todo") +func (self *RebaseCommands) MoveTodoDown(index int) error { + fileName := filepath.Join(self.dotGitDir, "rebase-merge/git-rebase-todo") bytes, err := ioutil.ReadFile(fileName) if err != nil { return err } content := strings.Split(string(bytes), "\n") - commitCount := c.getTodoCommitCount(content) + commitCount := self.getTodoCommitCount(content) contentIndex := commitCount - 1 - index rearrangedContent := append(content[0:contentIndex-1], content[contentIndex], content[contentIndex-1]) @@ -189,8 +223,8 @@ func (c *GitCommand) MoveTodoDown(index int) error { } // SquashAllAboveFixupCommits squashes all fixup! commits above the given one -func (c *GitCommand) SquashAllAboveFixupCommits(sha string) error { - return c.runSkipEditorCommand( +func (self *RebaseCommands) SquashAllAboveFixupCommits(sha string) error { + return self.runSkipEditorCommand( fmt.Sprintf( "git rebase --interactive --autostash --autosquash %s^", sha, @@ -199,8 +233,8 @@ func (c *GitCommand) SquashAllAboveFixupCommits(sha string) error { } // BeginInteractiveRebaseForCommit starts an interactive rebase to edit the current -// commit and pick all others. After this you'll want to call `c.GenericMergeOrRebaseAction("rebase", "continue")` -func (c *GitCommand) BeginInteractiveRebaseForCommit(commits []*models.Commit, commitIndex int) error { +// commit and pick all others. After this you'll want to call `self.GenericMergeOrRebaseAction("rebase", "continue")` +func (self *RebaseCommands) BeginInteractiveRebaseForCommit(commits []*models.Commit, commitIndex int) error { if len(commits)-1 < commitIndex { return errors.New("index outside of range of commits") } @@ -208,16 +242,16 @@ func (c *GitCommand) BeginInteractiveRebaseForCommit(commits []*models.Commit, c // we can make this GPG thing possible it just means we need to do this in two parts: // one where we handle the possibility of a credential request, and the other // where we continue the rebase - if c.UsingGpg() { - return errors.New(c.Tr.DisabledForGPG) + if self.config.UsingGpg() { + return errors.New(self.Tr.DisabledForGPG) } - todo, sha, err := c.GenerateGenericRebaseTodo(commits, commitIndex, "edit") + todo, sha, err := self.GenerateGenericRebaseTodo(commits, commitIndex, "edit") if err != nil { return err } - cmdObj, err := c.PrepareInteractiveRebaseCommand(sha, todo, true) + cmdObj, err := self.PrepareInteractiveRebaseCommand(sha, todo, true) if err != nil { return err } @@ -226,8 +260,8 @@ func (c *GitCommand) BeginInteractiveRebaseForCommit(commits []*models.Commit, c } // RebaseBranch interactive rebases onto a branch -func (c *GitCommand) RebaseBranch(branchName string) error { - cmdObj, err := c.PrepareInteractiveRebaseCommand(branchName, "", false) +func (self *RebaseCommands) RebaseBranch(branchName string) error { + cmdObj, err := self.PrepareInteractiveRebaseCommand(branchName, "", false) if err != nil { return err } @@ -237,8 +271,8 @@ func (c *GitCommand) RebaseBranch(branchName string) error { // GenericMerge takes a commandType of "merge" or "rebase" and a command of "abort", "skip" or "continue" // By default we skip the editor in the case where a commit will be made -func (c *GitCommand) GenericMergeOrRebaseAction(commandType string, command string) error { - err := c.runSkipEditorCommand( +func (self *RebaseCommands) GenericMergeOrRebaseAction(commandType string, command string) error { + err := self.runSkipEditorCommand( fmt.Sprintf( "git %s --%s", commandType, @@ -249,25 +283,25 @@ func (c *GitCommand) GenericMergeOrRebaseAction(commandType string, command stri if !strings.Contains(err.Error(), "no rebase in progress") { return err } - c.Log.Warn(err) + self.Log.Warn(err) } // sometimes we need to do a sequence of things in a rebase but the user needs to // fix merge conflicts along the way. When this happens we queue up the next step // so that after the next successful rebase continue we can continue from where we left off - if commandType == "rebase" && command == "continue" && c.onSuccessfulContinue != nil { - f := c.onSuccessfulContinue - c.onSuccessfulContinue = nil + if commandType == "rebase" && command == "continue" && self.onSuccessfulContinue != nil { + f := self.onSuccessfulContinue + self.onSuccessfulContinue = nil return f() } if command == "abort" { - c.onSuccessfulContinue = nil + self.onSuccessfulContinue = nil } return nil } -func (c *GitCommand) runSkipEditorCommand(command string) error { - cmdObj := c.Cmd.New(command) +func (self *RebaseCommands) runSkipEditorCommand(command string) error { + cmdObj := self.cmd.New(command) lazyGitPath := oscommands.GetLazygitPath() return cmdObj. AddEnvVars( @@ -278,3 +312,46 @@ func (c *GitCommand) runSkipEditorCommand(command string) error { ). Run() } + +// DiscardOldFileChanges discards changes to a file from an old commit +func (self *RebaseCommands) DiscardOldFileChanges(commits []*models.Commit, commitIndex int, fileName string) error { + if err := self.BeginInteractiveRebaseForCommit(commits, commitIndex); err != nil { + return err + } + + // check if file exists in previous commit (this command returns an error if the file doesn't exist) + if err := self.cmd.New("git cat-file -e HEAD^:" + self.cmd.Quote(fileName)).Run(); err != nil { + if err := self.osCommand.Remove(fileName); err != nil { + return err + } + if err := self.workingTree.StageFile(fileName); err != nil { + return err + } + } else if err := self.workingTree.CheckoutFile("HEAD^", fileName); err != nil { + return err + } + + // amend the commit + err := self.commit.AmendHead() + if err != nil { + return err + } + + // continue + return self.GenericMergeOrRebaseAction("rebase", "continue") +} + +// CherryPickCommits begins an interactive rebase with the given shas being cherry picked onto HEAD +func (self *RebaseCommands) CherryPickCommits(commits []*models.Commit) error { + todo := "" + for _, commit := range commits { + todo = "pick " + commit.Sha + " " + commit.Name + "\n" + todo + } + + cmdObj, err := self.PrepareInteractiveRebaseCommand("HEAD", todo, false) + if err != nil { + return err + } + + return cmdObj.Run() +} diff --git a/pkg/commands/rebasing_test.go b/pkg/commands/rebasing_test.go index 3091da9ee..410b25f22 100644 --- a/pkg/commands/rebasing_test.go +++ b/pkg/commands/rebasing_test.go @@ -5,6 +5,8 @@ import ( "testing" "github.com/go-errors/errors" + "github.com/jesseduffield/lazygit/pkg/commands/git_config" + "github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/commands/oscommands" "github.com/jesseduffield/lazygit/pkg/utils" "github.com/stretchr/testify/assert" @@ -42,7 +44,7 @@ func TestGitCommandRebaseBranch(t *testing.T) { for _, s := range scenarios { t.Run(s.testName, func(t *testing.T) { gitCmd := NewDummyGitCommandWithRunner(s.runner) - s.test(gitCmd.RebaseBranch(s.arg)) + s.test(gitCmd.Rebase.RebaseBranch(s.arg)) }) } } @@ -70,7 +72,74 @@ func TestGitCommandSkipEditorCommand(t *testing.T) { return "", nil }) gitCmd := NewDummyGitCommandWithRunner(runner) - err := gitCmd.runSkipEditorCommand(commandStr) + err := gitCmd.Rebase.runSkipEditorCommand(commandStr) assert.NoError(t, err) runner.CheckForMissingCalls() } + +func TestGitCommandDiscardOldFileChanges(t *testing.T) { + type scenario struct { + testName string + gitConfigMockResponses map[string]string + commits []*models.Commit + commitIndex int + fileName string + runner *oscommands.FakeCmdObjRunner + test func(error) + } + + scenarios := []scenario{ + { + testName: "returns error when index outside of range of commits", + gitConfigMockResponses: nil, + commits: []*models.Commit{}, + commitIndex: 0, + fileName: "test999.txt", + runner: oscommands.NewFakeRunner(t), + test: func(err error) { + assert.Error(t, err) + }, + }, + { + testName: "returns error when using gpg", + gitConfigMockResponses: map[string]string{"commit.gpgsign": "true"}, + commits: []*models.Commit{{Name: "commit", Sha: "123456"}}, + commitIndex: 0, + fileName: "test999.txt", + runner: oscommands.NewFakeRunner(t), + test: func(err error) { + assert.Error(t, err) + }, + }, + { + testName: "checks out file if it already existed", + gitConfigMockResponses: nil, + commits: []*models.Commit{ + {Name: "commit", Sha: "123456"}, + {Name: "commit2", Sha: "abcdef"}, + }, + commitIndex: 0, + fileName: "test999.txt", + runner: oscommands.NewFakeRunner(t). + Expect(`git rebase --interactive --autostash --keep-empty abcdef`, "", nil). + Expect(`git cat-file -e HEAD^:"test999.txt"`, "", nil). + Expect(`git checkout HEAD^ -- "test999.txt"`, "", nil). + Expect(`git commit --amend --no-edit --allow-empty`, "", nil). + Expect(`git rebase --continue`, "", nil), + test: func(err error) { + assert.NoError(t, err) + }, + }, + // test for when the file was created within the commit requires a refactor to support proper mocks + // currently we'd need to mock out the os.Remove function and that's gonna introduce tech debt + } + + for _, s := range scenarios { + t.Run(s.testName, func(t *testing.T) { + gitCmd := NewDummyGitCommandWithRunner(s.runner) + gitCmd.gitConfig = git_config.NewFakeGitConfig(s.gitConfigMockResponses) + s.test(gitCmd.Rebase.DiscardOldFileChanges(s.commits, s.commitIndex, s.fileName)) + s.runner.CheckForMissingCalls() + }) + } +} diff --git a/pkg/commands/remotes.go b/pkg/commands/remotes.go index c79512dd9..46acd49d8 100644 --- a/pkg/commands/remotes.go +++ b/pkg/commands/remotes.go @@ -4,49 +4,60 @@ import ( "fmt" "github.com/jesseduffield/lazygit/pkg/commands/oscommands" + "github.com/jesseduffield/lazygit/pkg/common" ) -func (c *GitCommand) AddRemote(name string, url string) error { - return c.Cmd. - New(fmt.Sprintf("git remote add %s %s", c.Cmd.Quote(name), c.Cmd.Quote(url))). +type RemoteCommands struct { + *common.Common + + cmd oscommands.ICmdObjBuilder +} + +func NewRemoteCommands( + common *common.Common, + cmd oscommands.ICmdObjBuilder, +) *RemoteCommands { + return &RemoteCommands{ + Common: common, + cmd: cmd, + } +} + +func (self *RemoteCommands) AddRemote(name string, url string) error { + return self.cmd. + New(fmt.Sprintf("git remote add %s %s", self.cmd.Quote(name), self.cmd.Quote(url))). Run() } -func (c *GitCommand) RemoveRemote(name string) error { - return c.Cmd. - New(fmt.Sprintf("git remote remove %s", c.Cmd.Quote(name))). +func (self *RemoteCommands) RemoveRemote(name string) error { + return self.cmd. + New(fmt.Sprintf("git remote remove %s", self.cmd.Quote(name))). Run() } -func (c *GitCommand) RenameRemote(oldRemoteName string, newRemoteName string) error { - return c.Cmd. - New(fmt.Sprintf("git remote rename %s %s", c.Cmd.Quote(oldRemoteName), c.Cmd.Quote(newRemoteName))). +func (self *RemoteCommands) RenameRemote(oldRemoteName string, newRemoteName string) error { + return self.cmd. + New(fmt.Sprintf("git remote rename %s %s", self.cmd.Quote(oldRemoteName), self.cmd.Quote(newRemoteName))). Run() } -func (c *GitCommand) UpdateRemoteUrl(remoteName string, updatedUrl string) error { - return c.Cmd. - New(fmt.Sprintf("git remote set-url %s %s", c.Cmd.Quote(remoteName), c.Cmd.Quote(updatedUrl))). +func (self *RemoteCommands) UpdateRemoteUrl(remoteName string, updatedUrl string) error { + return self.cmd. + New(fmt.Sprintf("git remote set-url %s %s", self.cmd.Quote(remoteName), self.cmd.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.Cmd.Quote(remoteName), c.Cmd.Quote(branchName)) - cmdObj := c.Cmd. - New(command) - return c.DetectUnamePass(cmdObj, promptUserForCredential) -} - -func (c *GitCommand) DetectUnamePass(cmdObj oscommands.ICmdObj, promptUserForCredential func(string) string) error { - return c.OSCommand.DetectUnamePass(cmdObj, c.GetCmdWriter(), promptUserForCredential) +func (self *RemoteCommands) DeleteRemoteBranch(remoteName string, branchName string) error { + command := fmt.Sprintf("git push %s --delete %s", self.cmd.Quote(remoteName), self.cmd.Quote(branchName)) + return self.cmd.New(command).PromptOnCredentialRequest().Run() } // CheckRemoteBranchExists Returns remote branch -func (c *GitCommand) CheckRemoteBranchExists(branchName string) bool { - _, err := c.Cmd. +func (self *RemoteCommands) CheckRemoteBranchExists(branchName string) bool { + _, err := self.cmd. New( fmt.Sprintf("git show-ref --verify -- refs/remotes/origin/%s", - c.Cmd.Quote(branchName), + self.cmd.Quote(branchName), ), ). DontLog(). @@ -54,8 +65,3 @@ func (c *GitCommand) CheckRemoteBranchExists(branchName string) bool { return err == nil } - -// GetRemoteURL returns current repo remote url -func (c *GitCommand) GetRemoteURL() string { - return c.GitConfig.Get("remote.origin.url") -} diff --git a/pkg/commands/stash_entries.go b/pkg/commands/stash_entries.go index b4d16adb7..1e02ddaa5 100644 --- a/pkg/commands/stash_entries.go +++ b/pkg/commands/stash_entries.go @@ -5,59 +5,91 @@ import ( "github.com/jesseduffield/lazygit/pkg/commands/loaders" "github.com/jesseduffield/lazygit/pkg/commands/oscommands" + "github.com/jesseduffield/lazygit/pkg/common" ) -// StashDo modify stash -func (c *GitCommand) StashDo(index int, method string) error { - return c.Cmd.New(fmt.Sprintf("git stash %s stash@{%d}", method, index)).Run() +type StashCommands struct { + *common.Common + + cmd oscommands.ICmdObjBuilder + fileLoader *loaders.FileLoader + osCommand *oscommands.OSCommand + workingTree *WorkingTreeCommands } -// StashSave save stash +func NewStashCommands( + common *common.Common, + cmd oscommands.ICmdObjBuilder, + osCommand *oscommands.OSCommand, + fileLoader *loaders.FileLoader, + workingTree *WorkingTreeCommands, +) *StashCommands { + return &StashCommands{ + Common: common, + cmd: cmd, + fileLoader: fileLoader, + osCommand: osCommand, + workingTree: workingTree, + } +} + +func (self *StashCommands) Drop(index int) error { + return self.cmd.New(fmt.Sprintf("git stash drop stash@{%d}", index)).Run() +} + +func (self *StashCommands) Pop(index int) error { + return self.cmd.New(fmt.Sprintf("git stash pop stash@{%d}", index)).Run() +} + +func (self *StashCommands) Apply(index int) error { + return self.cmd.New(fmt.Sprintf("git stash apply stash@{%d}", index)).Run() +} + +// Save save stash // TODO: before calling this, check if there is anything to save -func (c *GitCommand) StashSave(message string) error { - return c.Cmd.New("git stash save " + c.OSCommand.Quote(message)).Run() +func (self *StashCommands) Save(message string) error { + return self.cmd.New("git stash save " + self.cmd.Quote(message)).Run() } -func (c *GitCommand) ShowStashEntryCmdObj(index int) oscommands.ICmdObj { - cmdStr := fmt.Sprintf("git stash show -p --stat --color=%s --unified=%d stash@{%d}", c.colorArg(), c.UserConfig.Git.DiffContextSize, index) +func (self *StashCommands) ShowStashEntryCmdObj(index int) oscommands.ICmdObj { + cmdStr := fmt.Sprintf("git stash show -p --stat --color=%s --unified=%d stash@{%d}", self.UserConfig.Git.Paging.ColorArg, self.UserConfig.Git.DiffContextSize, index) - return c.Cmd.New(cmdStr).DontLog() + return self.cmd.New(cmdStr).DontLog() } -// StashSaveStagedChanges stashes only the currently staged changes. This takes a few steps +// SaveStagedChanges stashes only the currently staged changes. This takes a few steps // 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 { +func (self *StashCommands) SaveStagedChanges(message string) error { // wrap in 'writing', which uses a mutex - if err := c.Cmd.New("git stash --keep-index").Run(); err != nil { + if err := self.cmd.New("git stash --keep-index").Run(); err != nil { return err } - if err := c.StashSave(message); err != nil { + if err := self.Save(message); err != nil { return err } - if err := c.Cmd.New("git stash apply stash@{1}").Run(); err != nil { + if err := self.cmd.New("git stash apply stash@{1}").Run(); err != nil { return err } - if err := c.OSCommand.PipeCommands("git stash show -p", "git apply -R"); err != nil { + if err := self.osCommand.PipeCommands("git stash show -p", "git apply -R"); err != nil { return err } - if err := c.Cmd.New("git stash drop stash@{1}").Run(); err != nil { + if err := self.cmd.New("git stash drop stash@{1}").Run(); err != nil { return err } // if you had staged an untracked file, that will now appear as 'AD' in git status // meaning it's deleted in your working tree but added in your index. Given that it's // now safely stashed, we need to remove it. - files := loaders. - NewFileLoader(c.Common, c.Cmd, c.GitConfig). + files := self.fileLoader. GetStatusFiles(loaders.GetStatusFileOptions{}) for _, file := range files { if file.ShortStatus == "AD" { - if err := c.UnStageFile(file.Names(), false); err != nil { + if err := self.workingTree.UnStageFile(file.Names(), false); err != nil { return err } } diff --git a/pkg/commands/stash_entries_test.go b/pkg/commands/stash_entries_test.go index 762b9dff8..f179c3fb1 100644 --- a/pkg/commands/stash_entries_test.go +++ b/pkg/commands/stash_entries_test.go @@ -7,12 +7,30 @@ import ( "github.com/stretchr/testify/assert" ) -func TestGitCommandStashDo(t *testing.T) { +func TestGitCommandStashDrop(t *testing.T) { runner := oscommands.NewFakeRunner(t). ExpectGitArgs([]string{"stash", "drop", "stash@{1}"}, "", nil) gitCmd := NewDummyGitCommandWithRunner(runner) - assert.NoError(t, gitCmd.StashDo(1, "drop")) + assert.NoError(t, gitCmd.Stash.Drop(1)) + runner.CheckForMissingCalls() +} + +func TestGitCommandStashApply(t *testing.T) { + runner := oscommands.NewFakeRunner(t). + ExpectGitArgs([]string{"stash", "apply", "stash@{1}"}, "", nil) + gitCmd := NewDummyGitCommandWithRunner(runner) + + assert.NoError(t, gitCmd.Stash.Apply(1)) + runner.CheckForMissingCalls() +} + +func TestGitCommandStashPop(t *testing.T) { + runner := oscommands.NewFakeRunner(t). + ExpectGitArgs([]string{"stash", "pop", "stash@{1}"}, "", nil) + gitCmd := NewDummyGitCommandWithRunner(runner) + + assert.NoError(t, gitCmd.Stash.Pop(1)) runner.CheckForMissingCalls() } @@ -21,7 +39,7 @@ func TestGitCommandStashSave(t *testing.T) { ExpectGitArgs([]string{"stash", "save", "A stash message"}, "", nil) gitCmd := NewDummyGitCommandWithRunner(runner) - assert.NoError(t, gitCmd.StashSave("A stash message")) + assert.NoError(t, gitCmd.Stash.Save("A stash message")) runner.CheckForMissingCalls() } @@ -52,7 +70,7 @@ func TestGitCommandShowStashEntryCmdObj(t *testing.T) { t.Run(s.testName, func(t *testing.T) { gitCmd := NewDummyGitCommand() gitCmd.UserConfig.Git.DiffContextSize = s.contextSize - cmdStr := gitCmd.ShowStashEntryCmdObj(s.index).ToString() + cmdStr := gitCmd.Stash.ShowStashEntryCmdObj(s.index).ToString() assert.Equal(t, s.expected, cmdStr) }) } diff --git a/pkg/commands/status.go b/pkg/commands/status.go index 392ce3797..c6f038ac8 100644 --- a/pkg/commands/status.go +++ b/pkg/commands/status.go @@ -4,20 +4,43 @@ import ( "path/filepath" gogit "github.com/jesseduffield/go-git/v5" + "github.com/jesseduffield/lazygit/pkg/commands/oscommands" "github.com/jesseduffield/lazygit/pkg/commands/types/enums" + "github.com/jesseduffield/lazygit/pkg/common" ) +type StatusCommands struct { + *common.Common + osCommand *oscommands.OSCommand + repo *gogit.Repository + dotGitDir string +} + +func NewStatusCommands( + common *common.Common, + osCommand *oscommands.OSCommand, + repo *gogit.Repository, + dotGitDir string, +) *StatusCommands { + return &StatusCommands{ + Common: common, + osCommand: osCommand, + repo: repo, + dotGitDir: dotGitDir, + } +} + // RebaseMode returns "" for non-rebase mode, "normal" for normal rebase // and "interactive" for interactive rebase -func (c *GitCommand) RebaseMode() (enums.RebaseMode, error) { - exists, err := c.OSCommand.FileExists(filepath.Join(c.DotGitDir, "rebase-apply")) +func (self *StatusCommands) RebaseMode() (enums.RebaseMode, error) { + exists, err := self.osCommand.FileExists(filepath.Join(self.dotGitDir, "rebase-apply")) if err != nil { return enums.REBASE_MODE_NONE, err } if exists { return enums.REBASE_MODE_NORMAL, nil } - exists, err = c.OSCommand.FileExists(filepath.Join(c.DotGitDir, "rebase-merge")) + exists, err = self.osCommand.FileExists(filepath.Join(self.dotGitDir, "rebase-merge")) if exists { return enums.REBASE_MODE_INTERACTIVE, err } else { @@ -25,12 +48,12 @@ func (c *GitCommand) RebaseMode() (enums.RebaseMode, error) { } } -func (c *GitCommand) WorkingTreeState() enums.RebaseMode { - rebaseMode, _ := c.RebaseMode() +func (self *StatusCommands) WorkingTreeState() enums.RebaseMode { + rebaseMode, _ := self.RebaseMode() if rebaseMode != enums.REBASE_MODE_NONE { return enums.REBASE_MODE_REBASING } - merging, _ := c.IsInMergeState() + merging, _ := self.IsInMergeState() if merging { return enums.REBASE_MODE_MERGING } @@ -38,12 +61,12 @@ func (c *GitCommand) WorkingTreeState() enums.RebaseMode { } // IsInMergeState states whether we are still mid-merge -func (c *GitCommand) IsInMergeState() (bool, error) { - return c.OSCommand.FileExists(filepath.Join(c.DotGitDir, "MERGE_HEAD")) +func (self *StatusCommands) IsInMergeState() (bool, error) { + return self.osCommand.FileExists(filepath.Join(self.dotGitDir, "MERGE_HEAD")) } -func (c *GitCommand) IsBareRepo() bool { +func (self *StatusCommands) IsBareRepo() bool { // note: could use `git rev-parse --is-bare-repository` if we wanna drop go-git - _, err := c.Repo.Worktree() + _, err := self.repo.Worktree() return err == gogit.ErrIsBareRepository } diff --git a/pkg/commands/submodules.go b/pkg/commands/submodules.go index 29a9ab700..9ccf29be2 100644 --- a/pkg/commands/submodules.go +++ b/pkg/commands/submodules.go @@ -10,6 +10,7 @@ import ( "github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/commands/oscommands" + "github.com/jesseduffield/lazygit/pkg/common" ) // .gitmodules looks like this: @@ -17,7 +18,22 @@ import ( // path = blah/mysubmodule // url = git@github.com:subbo.git -func (c *GitCommand) GetSubmoduleConfigs() ([]*models.SubmoduleConfig, error) { +type SubmoduleCommands struct { + *common.Common + + cmd oscommands.ICmdObjBuilder + dotGitDir string +} + +func NewSubmoduleCommands(common *common.Common, cmd oscommands.ICmdObjBuilder, dotGitDir string) *SubmoduleCommands { + return &SubmoduleCommands{ + Common: common, + cmd: cmd, + dotGitDir: dotGitDir, + } +} + +func (self *SubmoduleCommands) GetConfigs() ([]*models.SubmoduleConfig, error) { file, err := os.Open(".gitmodules") if err != nil { if os.IsNotExist(err) { @@ -63,36 +79,36 @@ func (c *GitCommand) GetSubmoduleConfigs() ([]*models.SubmoduleConfig, error) { return configs, nil } -func (c *GitCommand) SubmoduleStash(submodule *models.SubmoduleConfig) error { +func (self *SubmoduleCommands) Stash(submodule *models.SubmoduleConfig) error { // if the path does not exist then it hasn't yet been initialized so we'll swallow the error // because the intention here is to have no dirty worktree state if _, err := os.Stat(submodule.Path); os.IsNotExist(err) { - c.Log.Infof("submodule path %s does not exist, returning", submodule.Path) + self.Log.Infof("submodule path %s does not exist, returning", submodule.Path) return nil } - return c.Cmd.New("git -C " + c.Cmd.Quote(submodule.Path) + " stash --include-untracked").Run() + return self.cmd.New("git -C " + self.cmd.Quote(submodule.Path) + " stash --include-untracked").Run() } -func (c *GitCommand) SubmoduleReset(submodule *models.SubmoduleConfig) error { - return c.Cmd.New("git submodule update --init --force -- " + c.Cmd.Quote(submodule.Path)).Run() +func (self *SubmoduleCommands) Reset(submodule *models.SubmoduleConfig) error { + return self.cmd.New("git submodule update --init --force -- " + self.cmd.Quote(submodule.Path)).Run() } -func (c *GitCommand) SubmoduleUpdateAll() error { +func (self *SubmoduleCommands) UpdateAll() error { // not doing an --init here because the user probably doesn't want that - return c.Cmd.New("git submodule update --force").Run() + return self.cmd.New("git submodule update --force").Run() } -func (c *GitCommand) SubmoduleDelete(submodule *models.SubmoduleConfig) error { +func (self *SubmoduleCommands) Delete(submodule *models.SubmoduleConfig) error { // based on https://gist.github.com/myusuf3/7f645819ded92bda6677 - if err := c.Cmd.New("git submodule deinit --force -- " + c.Cmd.Quote(submodule.Path)).Run(); err != nil { + if err := self.cmd.New("git submodule deinit --force -- " + self.cmd.Quote(submodule.Path)).Run(); err != nil { if strings.Contains(err.Error(), "did not match any file(s) known to git") { - if err := c.Cmd.New("git config --file .gitmodules --remove-section submodule." + c.Cmd.Quote(submodule.Name)).Run(); err != nil { + if err := self.cmd.New("git config --file .gitmodules --remove-section submodule." + self.cmd.Quote(submodule.Name)).Run(); err != nil { return err } - if err := c.Cmd.New("git config --remove-section submodule." + c.Cmd.Quote(submodule.Name)).Run(); err != nil { + if err := self.cmd.New("git config --remove-section submodule." + self.cmd.Quote(submodule.Name)).Run(); err != nil { return err } @@ -102,69 +118,69 @@ func (c *GitCommand) SubmoduleDelete(submodule *models.SubmoduleConfig) error { } } - if err := c.Cmd.New("git rm --force -r " + submodule.Path).Run(); err != nil { + if err := self.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) + self.Log.Error(err) } - return os.RemoveAll(filepath.Join(c.DotGitDir, "modules", submodule.Path)) + return os.RemoveAll(filepath.Join(self.dotGitDir, "modules", submodule.Path)) } -func (c *GitCommand) SubmoduleAdd(name string, path string, url string) error { - return c.Cmd. +func (self *SubmoduleCommands) Add(name string, path string, url string) error { + return self.cmd. New( fmt.Sprintf( "git submodule add --force --name %s -- %s %s ", - c.Cmd.Quote(name), - c.Cmd.Quote(url), - c.Cmd.Quote(path), + self.cmd.Quote(name), + self.cmd.Quote(url), + self.cmd.Quote(path), )). Run() } -func (c *GitCommand) SubmoduleUpdateUrl(name string, path string, newUrl string) error { +func (self *SubmoduleCommands) UpdateUrl(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.Cmd.New("git config --file .gitmodules submodule." + c.Cmd.Quote(name) + ".url " + c.Cmd.Quote(newUrl)).Run(); err != nil { + if err := self.cmd.New("git config --file .gitmodules submodule." + self.cmd.Quote(name) + ".url " + self.cmd.Quote(newUrl)).Run(); err != nil { return err } - if err := c.Cmd.New("git submodule sync -- " + c.Cmd.Quote(path)).Run(); err != nil { + if err := self.cmd.New("git submodule sync -- " + self.cmd.Quote(path)).Run(); err != nil { return err } return nil } -func (c *GitCommand) SubmoduleInit(path string) error { - return c.Cmd.New("git submodule init -- " + c.Cmd.Quote(path)).Run() +func (self *SubmoduleCommands) Init(path string) error { + return self.cmd.New("git submodule init -- " + self.cmd.Quote(path)).Run() } -func (c *GitCommand) SubmoduleUpdate(path string) error { - return c.Cmd.New("git submodule update --init -- " + c.Cmd.Quote(path)).Run() +func (self *SubmoduleCommands) Update(path string) error { + return self.cmd.New("git submodule update --init -- " + self.cmd.Quote(path)).Run() } -func (c *GitCommand) SubmoduleBulkInitCmdObj() oscommands.ICmdObj { - return c.Cmd.New("git submodule init") +func (self *SubmoduleCommands) BulkInitCmdObj() oscommands.ICmdObj { + return self.cmd.New("git submodule init") } -func (c *GitCommand) SubmoduleBulkUpdateCmdObj() oscommands.ICmdObj { - return c.Cmd.New("git submodule update") +func (self *SubmoduleCommands) BulkUpdateCmdObj() oscommands.ICmdObj { + return self.cmd.New("git submodule update") } -func (c *GitCommand) SubmoduleForceBulkUpdateCmdObj() oscommands.ICmdObj { - return c.Cmd.New("git submodule update --force") +func (self *SubmoduleCommands) ForceBulkUpdateCmdObj() oscommands.ICmdObj { + return self.cmd.New("git submodule update --force") } -func (c *GitCommand) SubmoduleBulkDeinitCmdObj() oscommands.ICmdObj { - return c.Cmd.New("git submodule deinit --all --force") +func (self *SubmoduleCommands) BulkDeinitCmdObj() oscommands.ICmdObj { + return self.cmd.New("git submodule deinit --all --force") } -func (c *GitCommand) ResetSubmodules(submodules []*models.SubmoduleConfig) error { +func (self *SubmoduleCommands) ResetSubmodules(submodules []*models.SubmoduleConfig) error { for _, submodule := range submodules { - if err := c.SubmoduleStash(submodule); err != nil { + if err := self.Stash(submodule); err != nil { return err } } - return c.SubmoduleUpdateAll() + return self.UpdateAll() } diff --git a/pkg/commands/sync.go b/pkg/commands/sync.go index d9e7eb764..c926aef64 100644 --- a/pkg/commands/sync.go +++ b/pkg/commands/sync.go @@ -5,8 +5,25 @@ import ( "github.com/go-errors/errors" "github.com/jesseduffield/lazygit/pkg/commands/oscommands" + "github.com/jesseduffield/lazygit/pkg/common" ) +type SyncCommands struct { + *common.Common + + cmd oscommands.ICmdObjBuilder +} + +func NewSyncCommands( + common *common.Common, + cmd oscommands.ICmdObjBuilder, +) *SyncCommands { + return &SyncCommands{ + Common: common, + cmd: cmd, + } +} + // Push pushes to a branch type PushOpts struct { Force bool @@ -15,7 +32,7 @@ type PushOpts struct { SetUpstream bool } -func (c *GitCommand) PushCmdObj(opts PushOpts) (oscommands.ICmdObj, error) { +func (self *SyncCommands) PushCmdObj(opts PushOpts) (oscommands.ICmdObj, error) { cmdStr := "git push" if opts.Force { @@ -27,71 +44,62 @@ func (c *GitCommand) PushCmdObj(opts PushOpts) (oscommands.ICmdObj, error) { } if opts.UpstreamRemote != "" { - cmdStr += " " + c.OSCommand.Quote(opts.UpstreamRemote) + cmdStr += " " + self.cmd.Quote(opts.UpstreamRemote) } if opts.UpstreamBranch != "" { if opts.UpstreamRemote == "" { - return nil, errors.New(c.Tr.MustSpecifyOriginError) + return nil, errors.New(self.Tr.MustSpecifyOriginError) } - cmdStr += " " + c.OSCommand.Quote(opts.UpstreamBranch) + cmdStr += " " + self.cmd.Quote(opts.UpstreamBranch) } - cmdObj := c.Cmd.New(cmdStr) + cmdObj := self.cmd.New(cmdStr).PromptOnCredentialRequest() return cmdObj, nil } -func (c *GitCommand) Push(opts PushOpts, promptUserForCredential func(string) string) error { - cmdObj, err := c.PushCmdObj(opts) +func (self *SyncCommands) Push(opts PushOpts) error { + cmdObj, err := self.PushCmdObj(opts) if err != nil { return err } - return c.DetectUnamePass(cmdObj, promptUserForCredential) + return cmdObj.Run() } type FetchOptions struct { - PromptUserForCredential func(string) string - RemoteName string - BranchName string + Background bool + RemoteName string + BranchName string } // Fetch fetch git repo -func (c *GitCommand) Fetch(opts FetchOptions) error { +func (self *SyncCommands) Fetch(opts FetchOptions) error { cmdStr := "git fetch" if opts.RemoteName != "" { - cmdStr = fmt.Sprintf("%s %s", cmdStr, c.OSCommand.Quote(opts.RemoteName)) + cmdStr = fmt.Sprintf("%s %s", cmdStr, self.cmd.Quote(opts.RemoteName)) } if opts.BranchName != "" { - cmdStr = fmt.Sprintf("%s %s", cmdStr, c.OSCommand.Quote(opts.BranchName)) + cmdStr = fmt.Sprintf("%s %s", cmdStr, self.cmd.Quote(opts.BranchName)) } - cmdObj := c.Cmd.New(cmdStr) - userInitiated := opts.PromptUserForCredential != nil - if !userInitiated { - cmdObj.DontLog() + cmdObj := self.cmd.New(cmdStr) + if opts.Background { + cmdObj.DontLog().FailOnCredentialRequest() + } else { + cmdObj.PromptOnCredentialRequest() } - return c.DetectUnamePass(cmdObj, func(question string) string { - if userInitiated { - return opts.PromptUserForCredential(question) - } - return "\n" - }) + return cmdObj.Run() } type PullOptions struct { - PromptUserForCredential func(string) string - RemoteName string - BranchName string - FastForwardOnly bool + RemoteName string + BranchName string + FastForwardOnly bool } -func (c *GitCommand) Pull(opts PullOptions) error { - if opts.PromptUserForCredential == nil { - return errors.New("PromptUserForCredential is required") - } - +func (self *SyncCommands) Pull(opts PullOptions) error { cmdStr := "git pull --no-edit" if opts.FastForwardOnly { @@ -99,26 +107,23 @@ func (c *GitCommand) Pull(opts PullOptions) error { } if opts.RemoteName != "" { - cmdStr = fmt.Sprintf("%s %s", cmdStr, c.OSCommand.Quote(opts.RemoteName)) + cmdStr = fmt.Sprintf("%s %s", cmdStr, self.cmd.Quote(opts.RemoteName)) } if opts.BranchName != "" { - cmdStr = fmt.Sprintf("%s %s", cmdStr, c.OSCommand.Quote(opts.BranchName)) + cmdStr = fmt.Sprintf("%s %s", cmdStr, self.cmd.Quote(opts.BranchName)) } // setting GIT_SEQUENCE_EDITOR to ':' as a way of skipping it, in case the user // has 'pull.rebase = interactive' configured. - cmdObj := c.Cmd.New(cmdStr).AddEnvVars("GIT_SEQUENCE_EDITOR=:") - return c.DetectUnamePass(cmdObj, opts.PromptUserForCredential) + return self.cmd.New(cmdStr).AddEnvVars("GIT_SEQUENCE_EDITOR=:").PromptOnCredentialRequest().Run() } -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.Cmd.New(cmdStr) - return c.DetectUnamePass(cmdObj, promptUserForCredential) +func (self *SyncCommands) FastForward(branchName string, remoteName string, remoteBranchName string) error { + cmdStr := fmt.Sprintf("git fetch %s %s:%s", self.cmd.Quote(remoteName), self.cmd.Quote(remoteBranchName), self.cmd.Quote(branchName)) + return self.cmd.New(cmdStr).PromptOnCredentialRequest().Run() } -func (c *GitCommand) FetchRemote(remoteName string, promptUserForCredential func(string) string) error { - cmdStr := fmt.Sprintf("git fetch %s", c.OSCommand.Quote(remoteName)) - cmdObj := c.Cmd.New(cmdStr) - return c.DetectUnamePass(cmdObj, promptUserForCredential) +func (self *SyncCommands) FetchRemote(remoteName string) error { + cmdStr := fmt.Sprintf("git fetch %s", self.cmd.Quote(remoteName)) + return self.cmd.New(cmdStr).PromptOnCredentialRequest().Run() } diff --git a/pkg/commands/sync_test.go b/pkg/commands/sync_test.go index 24b7bf6c3..13fb77523 100644 --- a/pkg/commands/sync_test.go +++ b/pkg/commands/sync_test.go @@ -87,7 +87,7 @@ func TestGitCommandPush(t *testing.T) { for _, s := range scenarios { t.Run(s.testName, func(t *testing.T) { gitCmd := NewDummyGitCommandWithRunner(oscommands.NewFakeRunner(t)) - s.test(gitCmd.PushCmdObj(s.opts)) + s.test(gitCmd.Sync.PushCmdObj(s.opts)) }) } } diff --git a/pkg/commands/tags.go b/pkg/commands/tags.go index 795a44b79..b802aef8c 100644 --- a/pkg/commands/tags.go +++ b/pkg/commands/tags.go @@ -2,22 +2,36 @@ package commands import ( "fmt" + + "github.com/jesseduffield/lazygit/pkg/commands/oscommands" + "github.com/jesseduffield/lazygit/pkg/common" ) -func (c *GitCommand) CreateLightweightTag(tagName string, commitSha string) error { - return c.Cmd.New(fmt.Sprintf("git tag -- %s %s", c.OSCommand.Quote(tagName), commitSha)).Run() +type TagCommands struct { + *common.Common + + cmd oscommands.ICmdObjBuilder } -func (c *GitCommand) CreateAnnotatedTag(tagName, commitSha, msg string) error { - return c.Cmd.New(fmt.Sprintf("git tag %s %s -m %s", tagName, commitSha, c.OSCommand.Quote(msg))).Run() +func NewTagCommands(common *common.Common, cmd oscommands.ICmdObjBuilder) *TagCommands { + return &TagCommands{ + Common: common, + cmd: cmd, + } } -func (c *GitCommand) DeleteTag(tagName string) error { - return c.Cmd.New(fmt.Sprintf("git tag -d %s", c.OSCommand.Quote(tagName))).Run() +func (self *TagCommands) CreateLightweight(tagName string, commitSha string) error { + return self.cmd.New(fmt.Sprintf("git tag -- %s %s", self.cmd.Quote(tagName), commitSha)).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.Cmd.New(cmdStr) - return c.DetectUnamePass(cmdObj, promptUserForCredential) +func (self *TagCommands) CreateAnnotated(tagName, commitSha, msg string) error { + return self.cmd.New(fmt.Sprintf("git tag %s %s -m %s", tagName, commitSha, self.cmd.Quote(msg))).Run() +} + +func (self *TagCommands) Delete(tagName string) error { + return self.cmd.New(fmt.Sprintf("git tag -d %s", self.cmd.Quote(tagName))).Run() +} + +func (self *TagCommands) Push(remoteName string, tagName string) error { + return self.cmd.New(fmt.Sprintf("git push %s %s", self.cmd.Quote(remoteName), self.cmd.Quote(tagName))).PromptOnCredentialRequest().Run() } diff --git a/pkg/commands/working_tree.go b/pkg/commands/working_tree.go new file mode 100644 index 000000000..05717cca6 --- /dev/null +++ b/pkg/commands/working_tree.go @@ -0,0 +1,347 @@ +package commands + +import ( + "fmt" + "os" + "path/filepath" + "time" + + "github.com/go-errors/errors" + "github.com/jesseduffield/lazygit/pkg/commands/loaders" + "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/filetree" + "github.com/jesseduffield/lazygit/pkg/utils" +) + +type WorkingTreeCommands struct { + *common.Common + + cmd oscommands.ICmdObjBuilder + os WorkingTreeOSCommand + submodule *SubmoduleCommands + fileLoader *loaders.FileLoader +} + +type WorkingTreeOSCommand interface { + RemoveFile(string) error + CreateFileWithContent(string, string) error + AppendLineToFile(string, string) error +} + +func NewWorkingTreeCommands( + common *common.Common, + cmd oscommands.ICmdObjBuilder, + submodulesCommands *SubmoduleCommands, + osCommand WorkingTreeOSCommand, + fileLoader *loaders.FileLoader, +) *WorkingTreeCommands { + return &WorkingTreeCommands{ + Common: common, + cmd: cmd, + os: osCommand, + submodule: submodulesCommands, + fileLoader: fileLoader, + } +} + +func (self *WorkingTreeCommands) OpenMergeToolCmdObj() oscommands.ICmdObj { + return self.cmd.New("git mergetool") +} + +func (self *WorkingTreeCommands) OpenMergeTool() error { + return self.OpenMergeToolCmdObj().Run() +} + +// StageFile stages a file +func (self *WorkingTreeCommands) StageFile(fileName string) error { + return self.cmd.New("git add -- " + self.cmd.Quote(fileName)).Run() +} + +// StageAll stages all files +func (self *WorkingTreeCommands) StageAll() error { + return self.cmd.New("git add -A").Run() +} + +// UnstageAll unstages all files +func (self *WorkingTreeCommands) UnstageAll() error { + return self.cmd.New("git reset").Run() +} + +// UnStageFile unstages a file +// we accept an array of filenames for the cases where a file has been renamed i.e. +// we accept the current name and the previous name +func (self *WorkingTreeCommands) UnStageFile(fileNames []string, reset bool) error { + command := "git rm --cached --force -- %s" + if reset { + command = "git reset HEAD -- %s" + } + + for _, name := range fileNames { + err := self.cmd.New(fmt.Sprintf(command, self.cmd.Quote(name))).Run() + if err != nil { + return err + } + } + return nil +} + +func (c *WorkingTreeCommands) BeforeAndAfterFileForRename(file *models.File) (*models.File, *models.File, error) { + if !file.IsRename() { + return nil, nil, errors.New("Expected renamed file") + } + + // we've got a file that represents a rename from one file to another. Here we will refetch + // all files, passing the --no-renames flag and then recursively call the function + // again for the before file and after file. + + filesWithoutRenames := c.fileLoader.GetStatusFiles(loaders.GetStatusFileOptions{NoRenames: true}) + + var beforeFile *models.File + var afterFile *models.File + for _, f := range filesWithoutRenames { + if f.Name == file.PreviousName { + beforeFile = f + } + + if f.Name == file.Name { + afterFile = f + } + } + + if beforeFile == nil || afterFile == nil { + return nil, nil, errors.New("Could not find deleted file or new file for file rename") + } + + if beforeFile.IsRename() || afterFile.IsRename() { + // probably won't happen but we want to ensure we don't get an infinite loop + return nil, nil, errors.New("Nested rename found") + } + + return beforeFile, afterFile, nil +} + +// DiscardAllFileChanges directly +func (c *WorkingTreeCommands) DiscardAllFileChanges(file *models.File) error { + if file.IsRename() { + beforeFile, afterFile, err := c.BeforeAndAfterFileForRename(file) + if err != nil { + return err + } + + if err := c.DiscardAllFileChanges(beforeFile); err != nil { + return err + } + + if err := c.DiscardAllFileChanges(afterFile); err != nil { + return err + } + + return nil + } + + quotedFileName := c.cmd.Quote(file.Name) + + if file.ShortStatus == "AA" { + if err := c.cmd.New("git checkout --ours -- " + quotedFileName).Run(); err != nil { + return err + } + if err := c.cmd.New("git add -- " + quotedFileName).Run(); err != nil { + return err + } + return nil + } + + if file.ShortStatus == "DU" { + 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.cmd.New("git reset -- " + quotedFileName).Run(); err != nil { + return err + } + } + + if file.ShortStatus == "DD" || file.ShortStatus == "AU" { + return nil + } + + if file.Added { + return c.os.RemoveFile(file.Name) + } + return c.DiscardUnstagedFileChanges(file) +} + +func (c *WorkingTreeCommands) DiscardAllDirChanges(node *filetree.FileNode) error { + // this could be more efficient but we would need to handle all the edge cases + return node.ForEachFile(c.DiscardAllFileChanges) +} + +func (c *WorkingTreeCommands) DiscardUnstagedDirChanges(node *filetree.FileNode) error { + if err := c.RemoveUntrackedDirFiles(node); err != nil { + return err + } + + quotedPath := c.cmd.Quote(node.GetPath()) + if err := c.cmd.New("git checkout -- " + quotedPath).Run(); err != nil { + return err + } + + return nil +} + +func (c *WorkingTreeCommands) RemoveUntrackedDirFiles(node *filetree.FileNode) error { + untrackedFilePaths := node.GetPathsMatching( + func(n *filetree.FileNode) bool { return n.File != nil && !n.File.GetIsTracked() }, + ) + + for _, path := range untrackedFilePaths { + err := os.Remove(path) + if err != nil { + return err + } + } + + return nil +} + +// DiscardUnstagedFileChanges directly +func (c *WorkingTreeCommands) DiscardUnstagedFileChanges(file *models.File) error { + quotedFileName := c.cmd.Quote(file.Name) + return c.cmd.New("git checkout -- " + quotedFileName).Run() +} + +// Ignore adds a file to the gitignore for the repo +func (c *WorkingTreeCommands) Ignore(filename string) error { + return c.os.AppendLineToFile(".gitignore", filename) +} + +// WorktreeFileDiff returns the diff of a file +func (c *WorkingTreeCommands) WorktreeFileDiff(file *models.File, plain bool, cached bool, ignoreWhitespace bool) string { + // for now we assume an error means the file was deleted + s, _ := c.WorktreeFileDiffCmdObj(file, plain, cached, ignoreWhitespace).RunWithOutput() + return s +} + +func (c *WorkingTreeCommands) WorktreeFileDiffCmdObj(node models.IFile, plain bool, cached bool, ignoreWhitespace bool) oscommands.ICmdObj { + cachedArg := "" + trackedArg := "--" + colorArg := c.UserConfig.Git.Paging.ColorArg + quotedPath := c.cmd.Quote(node.GetPath()) + ignoreWhitespaceArg := "" + contextSize := c.UserConfig.Git.DiffContextSize + if cached { + cachedArg = "--cached" + } + if !node.GetIsTracked() && !node.GetHasStagedChanges() && !cached { + trackedArg = "--no-index -- /dev/null" + } + if plain { + colorArg = "never" + } + if ignoreWhitespace { + ignoreWhitespaceArg = "--ignore-all-space" + } + + 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.cmd.New(cmdStr).DontLog() +} + +func (c *WorkingTreeCommands) ApplyPatch(patch string, flags ...string) error { + filepath := filepath.Join(oscommands.GetTempDir(), utils.GetCurrentRepoName(), time.Now().Format("Jan _2 15.04.05.000000000")+".patch") + c.Log.Infof("saving temporary patch to %s", filepath) + if err := c.os.CreateFileWithContent(filepath, patch); err != nil { + return err + } + + flagStr := "" + for _, flag := range flags { + flagStr += " --" + flag + } + + return c.cmd.New(fmt.Sprintf("git apply%s %s", flagStr, c.cmd.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 *WorkingTreeCommands) ShowFileDiff(from string, to string, reverse bool, fileName string, plain bool) (string, error) { + return c.ShowFileDiffCmdObj(from, to, reverse, fileName, plain).RunWithOutput() +} + +func (c *WorkingTreeCommands) ShowFileDiffCmdObj(from string, to string, reverse bool, fileName string, plain bool) oscommands.ICmdObj { + colorArg := c.UserConfig.Git.Paging.ColorArg + contextSize := c.UserConfig.Git.DiffContextSize + if plain { + colorArg = "never" + } + + reverseFlag := "" + if reverse { + reverseFlag = " -R " + } + + 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.cmd.Quote(fileName)), + ). + DontLog() +} + +// CheckoutFile checks out the file for the given commit +func (c *WorkingTreeCommands) CheckoutFile(commitSha, fileName string) error { + return c.cmd.New(fmt.Sprintf("git checkout %s -- %s", commitSha, c.cmd.Quote(fileName))).Run() +} + +// DiscardAnyUnstagedFileChanges discards any unstages file changes via `git checkout -- .` +func (c *WorkingTreeCommands) DiscardAnyUnstagedFileChanges() error { + return c.cmd.New("git checkout -- .").Run() +} + +// RemoveTrackedFiles will delete the given file(s) even if they are currently tracked +func (c *WorkingTreeCommands) RemoveTrackedFiles(name string) error { + return c.cmd.New("git rm -r --cached -- " + c.cmd.Quote(name)).Run() +} + +// RemoveUntrackedFiles runs `git clean -fd` +func (c *WorkingTreeCommands) RemoveUntrackedFiles() error { + return c.cmd.New("git clean -fd").Run() +} + +// ResetAndClean removes all unstaged changes and removes all untracked files +func (c *WorkingTreeCommands) ResetAndClean() error { + submoduleConfigs, err := c.submodule.GetConfigs() + if err != nil { + return err + } + + if len(submoduleConfigs) > 0 { + if err := c.submodule.ResetSubmodules(submoduleConfigs); err != nil { + return err + } + } + + if err := c.ResetHard("HEAD"); err != nil { + return err + } + + return c.RemoveUntrackedFiles() +} + +// ResetHardHead runs `git reset --hard` +func (self *WorkingTreeCommands) ResetHard(ref string) error { + return self.cmd.New("git reset --hard " + self.cmd.Quote(ref)).Run() +} + +// ResetSoft runs `git reset --soft HEAD` +func (self *WorkingTreeCommands) ResetSoft(ref string) error { + return self.cmd.New("git reset --soft " + self.cmd.Quote(ref)).Run() +} + +func (self *WorkingTreeCommands) ResetMixed(ref string) error { + return self.cmd.New("git reset --mixed " + self.cmd.Quote(ref)).Run() +} diff --git a/pkg/commands/working_tree_test.go b/pkg/commands/working_tree_test.go new file mode 100644 index 000000000..15b552cd5 --- /dev/null +++ b/pkg/commands/working_tree_test.go @@ -0,0 +1,558 @@ +package commands + +import ( + "fmt" + "io/ioutil" + "regexp" + "testing" + + "github.com/go-errors/errors" + "github.com/jesseduffield/lazygit/pkg/commands/models" + "github.com/jesseduffield/lazygit/pkg/commands/oscommands" + "github.com/stretchr/testify/assert" +) + +func TestGitCommandStageFile(t *testing.T) { + runner := oscommands.NewFakeRunner(t). + ExpectArgs([]string{"git", "add", "--", "test.txt"}, "", nil) + gitCmd := NewDummyGitCommandWithRunner(runner) + + assert.NoError(t, gitCmd.WorkingTree.StageFile("test.txt")) + runner.CheckForMissingCalls() +} + +func TestGitCommandUnstageFile(t *testing.T) { + type scenario struct { + testName string + reset bool + runner *oscommands.FakeCmdObjRunner + test func(error) + } + + scenarios := []scenario{ + { + testName: "Remove an untracked file from staging", + reset: false, + runner: oscommands.NewFakeRunner(t). + ExpectArgs([]string{"git", "rm", "--cached", "--force", "--", "test.txt"}, "", nil), + test: func(err error) { + assert.NoError(t, err) + }, + }, + { + testName: "Remove a tracked file from staging", + reset: true, + runner: oscommands.NewFakeRunner(t). + ExpectArgs([]string{"git", "reset", "HEAD", "--", "test.txt"}, "", nil), + test: func(err error) { + assert.NoError(t, err) + }, + }, + } + + for _, s := range scenarios { + t.Run(s.testName, func(t *testing.T) { + gitCmd := NewDummyGitCommandWithRunner(s.runner) + s.test(gitCmd.WorkingTree.UnStageFile([]string{"test.txt"}, s.reset)) + }) + } +} + +// these tests don't cover everything, in part because we already have an integration +// test which does cover everything. I don't want to unnecessarily assert on the 'how' +// when the 'what' is what matters +func TestGitCommandDiscardAllFileChanges(t *testing.T) { + type scenario struct { + testName string + file *models.File + removeFile func(string) error + runner *oscommands.FakeCmdObjRunner + expectedError string + } + + scenarios := []scenario{ + { + testName: "An error occurred when resetting", + file: &models.File{ + Name: "test", + HasStagedChanges: true, + }, + removeFile: func(string) error { return nil }, + runner: oscommands.NewFakeRunner(t). + ExpectArgs([]string{"git", "reset", "--", "test"}, "", errors.New("error")), + expectedError: "error", + }, + { + testName: "An error occurred when removing file", + file: &models.File{ + Name: "test", + Tracked: false, + Added: true, + }, + removeFile: func(string) error { + return fmt.Errorf("an error occurred when removing file") + }, + runner: oscommands.NewFakeRunner(t), + expectedError: "an error occurred when removing file", + }, + { + testName: "An error occurred with checkout", + file: &models.File{ + Name: "test", + Tracked: true, + HasStagedChanges: false, + }, + removeFile: func(string) error { return nil }, + runner: oscommands.NewFakeRunner(t). + ExpectArgs([]string{"git", "checkout", "--", "test"}, "", errors.New("error")), + expectedError: "error", + }, + { + testName: "Checkout only", + file: &models.File{ + Name: "test", + Tracked: true, + HasStagedChanges: false, + }, + removeFile: func(string) error { return nil }, + runner: oscommands.NewFakeRunner(t). + ExpectArgs([]string{"git", "checkout", "--", "test"}, "", nil), + expectedError: "", + }, + { + testName: "Reset and checkout staged changes", + file: &models.File{ + Name: "test", + Tracked: true, + HasStagedChanges: true, + }, + removeFile: func(string) error { return nil }, + runner: oscommands.NewFakeRunner(t). + ExpectArgs([]string{"git", "reset", "--", "test"}, "", nil). + ExpectArgs([]string{"git", "checkout", "--", "test"}, "", nil), + expectedError: "", + }, + { + testName: "Reset and checkout merge conflicts", + file: &models.File{ + Name: "test", + Tracked: true, + HasMergeConflicts: true, + }, + removeFile: func(string) error { return nil }, + runner: oscommands.NewFakeRunner(t). + ExpectArgs([]string{"git", "reset", "--", "test"}, "", nil). + ExpectArgs([]string{"git", "checkout", "--", "test"}, "", nil), + expectedError: "", + }, + { + testName: "Reset and remove", + file: &models.File{ + Name: "test", + Tracked: false, + Added: true, + HasStagedChanges: true, + }, + removeFile: func(filename string) error { + assert.Equal(t, "test", filename) + return nil + }, + runner: oscommands.NewFakeRunner(t). + ExpectArgs([]string{"git", "reset", "--", "test"}, "", nil), + expectedError: "", + }, + { + testName: "Remove only", + file: &models.File{ + Name: "test", + Tracked: false, + Added: true, + HasStagedChanges: false, + }, + removeFile: func(filename string) error { + assert.Equal(t, "test", filename) + return nil + }, + runner: oscommands.NewFakeRunner(t), + expectedError: "", + }, + } + + for _, s := range scenarios { + t.Run(s.testName, func(t *testing.T) { + gitCmd := NewDummyGitCommandWithRunner(s.runner) + gitCmd.OSCommand.SetRemoveFile(s.removeFile) + err := gitCmd.WorkingTree.DiscardAllFileChanges(s.file) + + if s.expectedError == "" { + assert.Nil(t, err) + } else { + assert.Equal(t, s.expectedError, err.Error()) + } + s.runner.CheckForMissingCalls() + }) + } +} + +func TestGitCommandDiff(t *testing.T) { + type scenario struct { + testName string + file *models.File + plain bool + cached bool + ignoreWhitespace bool + contextSize int + runner *oscommands.FakeCmdObjRunner + } + + const expectedResult = "pretend this is an actual git diff" + + scenarios := []scenario{ + { + testName: "Default case", + file: &models.File{ + Name: "test.txt", + HasStagedChanges: false, + Tracked: true, + }, + plain: false, + cached: false, + ignoreWhitespace: false, + contextSize: 3, + runner: oscommands.NewFakeRunner(t). + ExpectArgs([]string{"git", "diff", "--submodule", "--no-ext-diff", "--unified=3", "--color=always", "--", "test.txt"}, expectedResult, nil), + }, + { + testName: "cached", + file: &models.File{ + Name: "test.txt", + HasStagedChanges: false, + Tracked: true, + }, + plain: false, + cached: true, + ignoreWhitespace: false, + contextSize: 3, + runner: oscommands.NewFakeRunner(t). + ExpectArgs([]string{"git", "diff", "--submodule", "--no-ext-diff", "--unified=3", "--color=always", "--cached", "--", "test.txt"}, expectedResult, nil), + }, + { + testName: "plain", + file: &models.File{ + Name: "test.txt", + HasStagedChanges: false, + Tracked: true, + }, + plain: true, + cached: false, + ignoreWhitespace: false, + contextSize: 3, + runner: oscommands.NewFakeRunner(t). + ExpectArgs([]string{"git", "diff", "--submodule", "--no-ext-diff", "--unified=3", "--color=never", "--", "test.txt"}, expectedResult, nil), + }, + { + testName: "File not tracked and file has no staged changes", + file: &models.File{ + Name: "test.txt", + HasStagedChanges: false, + Tracked: false, + }, + plain: false, + cached: false, + ignoreWhitespace: false, + contextSize: 3, + runner: oscommands.NewFakeRunner(t). + ExpectArgs([]string{"git", "diff", "--submodule", "--no-ext-diff", "--unified=3", "--color=always", "--no-index", "--", "/dev/null", "test.txt"}, expectedResult, nil), + }, + { + testName: "Default case (ignore whitespace)", + file: &models.File{ + Name: "test.txt", + HasStagedChanges: false, + Tracked: true, + }, + plain: false, + cached: false, + ignoreWhitespace: true, + contextSize: 3, + runner: oscommands.NewFakeRunner(t). + ExpectArgs([]string{"git", "diff", "--submodule", "--no-ext-diff", "--unified=3", "--color=always", "--ignore-all-space", "--", "test.txt"}, expectedResult, nil), + }, + { + testName: "Show diff with custom context size", + file: &models.File{ + Name: "test.txt", + HasStagedChanges: false, + Tracked: true, + }, + plain: false, + cached: false, + ignoreWhitespace: false, + contextSize: 17, + runner: oscommands.NewFakeRunner(t). + ExpectArgs([]string{"git", "diff", "--submodule", "--no-ext-diff", "--unified=17", "--color=always", "--", "test.txt"}, expectedResult, nil), + }, + } + + for _, s := range scenarios { + t.Run(s.testName, func(t *testing.T) { + gitCmd := NewDummyGitCommandWithRunner(s.runner) + gitCmd.UserConfig.Git.DiffContextSize = s.contextSize + result := gitCmd.WorkingTree.WorktreeFileDiff(s.file, s.plain, s.cached, s.ignoreWhitespace) + assert.Equal(t, expectedResult, result) + s.runner.CheckForMissingCalls() + }) + } +} + +func TestGitCommandShowFileDiff(t *testing.T) { + type scenario struct { + testName string + from string + to string + reverse bool + plain bool + contextSize int + runner *oscommands.FakeCmdObjRunner + } + + const expectedResult = "pretend this is an actual git diff" + + scenarios := []scenario{ + { + testName: "Default case", + from: "1234567890", + to: "0987654321", + reverse: false, + plain: false, + contextSize: 3, + runner: oscommands.NewFakeRunner(t). + ExpectArgs([]string{"git", "diff", "--submodule", "--no-ext-diff", "--unified=3", "--no-renames", "--color=always", "1234567890", "0987654321", "--", "test.txt"}, expectedResult, nil), + }, + { + testName: "Show diff with custom context size", + from: "1234567890", + to: "0987654321", + reverse: false, + plain: false, + contextSize: 123, + runner: oscommands.NewFakeRunner(t). + ExpectArgs([]string{"git", "diff", "--submodule", "--no-ext-diff", "--unified=123", "--no-renames", "--color=always", "1234567890", "0987654321", "--", "test.txt"}, expectedResult, nil), + }, + } + + for _, s := range scenarios { + t.Run(s.testName, func(t *testing.T) { + gitCmd := NewDummyGitCommandWithRunner(s.runner) + gitCmd.UserConfig.Git.DiffContextSize = s.contextSize + result, err := gitCmd.WorkingTree.ShowFileDiff(s.from, s.to, s.reverse, "test.txt", s.plain) + assert.NoError(t, err) + assert.Equal(t, expectedResult, result) + s.runner.CheckForMissingCalls() + }) + } +} + +func TestGitCommandCheckoutFile(t *testing.T) { + type scenario struct { + testName string + commitSha string + fileName string + runner *oscommands.FakeCmdObjRunner + test func(error) + } + + scenarios := []scenario{ + { + testName: "typical case", + commitSha: "11af912", + fileName: "test999.txt", + runner: oscommands.NewFakeRunner(t). + Expect(`git checkout 11af912 -- "test999.txt"`, "", nil), + test: func(err error) { + assert.NoError(t, err) + }, + }, + { + testName: "returns error if there is one", + commitSha: "11af912", + fileName: "test999.txt", + runner: oscommands.NewFakeRunner(t). + Expect(`git checkout 11af912 -- "test999.txt"`, "", errors.New("error")), + test: func(err error) { + assert.Error(t, err) + }, + }, + } + + for _, s := range scenarios { + t.Run(s.testName, func(t *testing.T) { + gitCmd := NewDummyGitCommandWithRunner(s.runner) + s.test(gitCmd.WorkingTree.CheckoutFile(s.commitSha, s.fileName)) + s.runner.CheckForMissingCalls() + }) + } +} + +func TestGitCommandApplyPatch(t *testing.T) { + type scenario struct { + testName string + runner *oscommands.FakeCmdObjRunner + test func(error) + } + + expectFn := func(regexStr string, errToReturn error) func(cmdObj oscommands.ICmdObj) (string, error) { + return func(cmdObj oscommands.ICmdObj) (string, error) { + re := regexp.MustCompile(regexStr) + matches := re.FindStringSubmatch(cmdObj.ToString()) + assert.Equal(t, 2, len(matches)) + + filename := matches[1] + + content, err := ioutil.ReadFile(filename) + assert.NoError(t, err) + + assert.Equal(t, "test", string(content)) + + return "", errToReturn + } + } + + scenarios := []scenario{ + { + testName: "valid case", + runner: oscommands.NewFakeRunner(t). + ExpectFunc(expectFn(`git apply --cached "(.*)"`, nil)), + test: func(err error) { + assert.NoError(t, err) + }, + }, + { + testName: "command returns error", + runner: oscommands.NewFakeRunner(t). + ExpectFunc(expectFn(`git apply --cached "(.*)"`, errors.New("error"))), + test: func(err error) { + assert.Error(t, err) + }, + }, + } + + for _, s := range scenarios { + t.Run(s.testName, func(t *testing.T) { + gitCmd := NewDummyGitCommandWithRunner(s.runner) + s.test(gitCmd.WorkingTree.ApplyPatch("test", "cached")) + s.runner.CheckForMissingCalls() + }) + } +} + +func TestGitCommandDiscardUnstagedFileChanges(t *testing.T) { + type scenario struct { + testName string + file *models.File + runner *oscommands.FakeCmdObjRunner + test func(error) + } + + scenarios := []scenario{ + { + testName: "valid case", + file: &models.File{Name: "test.txt"}, + runner: oscommands.NewFakeRunner(t). + Expect(`git checkout -- "test.txt"`, "", nil), + test: func(err error) { + assert.NoError(t, err) + }, + }, + } + + for _, s := range scenarios { + t.Run(s.testName, func(t *testing.T) { + gitCmd := NewDummyGitCommandWithRunner(s.runner) + s.test(gitCmd.WorkingTree.DiscardUnstagedFileChanges(s.file)) + s.runner.CheckForMissingCalls() + }) + } +} + +func TestGitCommandDiscardAnyUnstagedFileChanges(t *testing.T) { + type scenario struct { + testName string + runner *oscommands.FakeCmdObjRunner + test func(error) + } + + scenarios := []scenario{ + { + testName: "valid case", + runner: oscommands.NewFakeRunner(t). + Expect(`git checkout -- .`, "", nil), + test: func(err error) { + assert.NoError(t, err) + }, + }, + } + + for _, s := range scenarios { + t.Run(s.testName, func(t *testing.T) { + gitCmd := NewDummyGitCommandWithRunner(s.runner) + s.test(gitCmd.WorkingTree.DiscardAnyUnstagedFileChanges()) + s.runner.CheckForMissingCalls() + }) + } +} + +func TestGitCommandRemoveUntrackedFiles(t *testing.T) { + type scenario struct { + testName string + runner *oscommands.FakeCmdObjRunner + test func(error) + } + + scenarios := []scenario{ + { + testName: "valid case", + runner: oscommands.NewFakeRunner(t). + Expect(`git clean -fd`, "", nil), + test: func(err error) { + assert.NoError(t, err) + }, + }, + } + + for _, s := range scenarios { + t.Run(s.testName, func(t *testing.T) { + gitCmd := NewDummyGitCommandWithRunner(s.runner) + s.test(gitCmd.WorkingTree.RemoveUntrackedFiles()) + s.runner.CheckForMissingCalls() + }) + } +} + +func TestGitCommandResetHard(t *testing.T) { + type scenario struct { + testName string + ref string + runner *oscommands.FakeCmdObjRunner + test func(error) + } + + scenarios := []scenario{ + { + "valid case", + "HEAD", + oscommands.NewFakeRunner(t). + Expect(`git reset --hard "HEAD"`, "", nil), + func(err error) { + assert.NoError(t, err) + }, + }, + } + + for _, s := range scenarios { + t.Run(s.testName, func(t *testing.T) { + gitCmd := NewDummyGitCommandWithRunner(s.runner) + s.test(gitCmd.WorkingTree.ResetHard(s.ref)) + }) + } +} diff --git a/pkg/gui/branches_panel.go b/pkg/gui/branches_panel.go index dbb2ae8da..474ff6c50 100644 --- a/pkg/gui/branches_panel.go +++ b/pkg/gui/branches_panel.go @@ -31,7 +31,7 @@ func (gui *Gui) branchesRenderToMain() error { if branch == nil { task = NewRenderStringTask(gui.Tr.NoBranchesThisRepo) } else { - cmdObj := gui.GitCommand.GetBranchGraphCmdObj(branch.Name) + cmdObj := gui.GitCommand.Branch.GetGraphCmdObj(branch.Name) task = NewRunPtyTask(cmdObj.GetCmd()) } @@ -103,7 +103,7 @@ func (gui *Gui) handleCopyPullRequestURLPress() error { branch := gui.getSelectedBranch() - branchExistsOnRemote := gui.GitCommand.CheckRemoteBranchExists(branch.Name) + branchExistsOnRemote := gui.GitCommand.Remote.CheckRemoteBranchExists(branch.Name) if !branchExistsOnRemote { return gui.surfaceError(errors.New(gui.Tr.NoBranchOnRemote)) @@ -146,7 +146,7 @@ func (gui *Gui) handleForceCheckout() error { prompt: message, handleConfirm: func() error { gui.logAction(gui.Tr.Actions.ForceCheckoutBranch) - if err := gui.GitCommand.Checkout(branch.Name, commands.CheckoutOptions{Force: true}); err != nil { + if err := gui.GitCommand.Branch.Checkout(branch.Name, commands.CheckoutOptions{Force: true}); err != nil { _ = gui.surfaceError(err) } return gui.refreshSidePanels(refreshOptions{mode: ASYNC}) @@ -176,7 +176,7 @@ func (gui *Gui) handleCheckoutRef(ref string, options handleCheckoutRefOptions) } return gui.WithWaitingStatus(waitingStatus, func() error { - if err := gui.GitCommand.Checkout(ref, cmdOptions); err != nil { + if err := gui.GitCommand.Branch.Checkout(ref, cmdOptions); err != nil { // note, this will only work for english-language git commands. If we force git to use english, and the error isn't this one, then the user will receive an english command they may not understand. I'm not sure what the best solution to this is. Running the command once in english and a second time in the native language is one option if options.onRefNotFound != nil && strings.Contains(err.Error(), "did not match any file(s) known to git") { @@ -190,15 +190,15 @@ func (gui *Gui) handleCheckoutRef(ref string, options handleCheckoutRefOptions) title: gui.Tr.AutoStashTitle, prompt: gui.Tr.AutoStashPrompt, handleConfirm: func() error { - if err := gui.GitCommand.StashSave(gui.Tr.StashPrefix + ref); err != nil { + if err := gui.GitCommand.Stash.Save(gui.Tr.StashPrefix + ref); err != nil { return gui.surfaceError(err) } - if err := gui.GitCommand.Checkout(ref, cmdOptions); err != nil { + if err := gui.GitCommand.Branch.Checkout(ref, cmdOptions); err != nil { return gui.surfaceError(err) } onSuccess() - if err := gui.GitCommand.StashDo(0, "pop"); err != nil { + if err := gui.GitCommand.Stash.Pop(0); err != nil { if err := gui.refreshSidePanels(refreshOptions{mode: BLOCK_UI}); err != nil { return err } @@ -254,7 +254,7 @@ func (gui *Gui) createNewBranchWithName(newBranchName string) error { return nil } - if err := gui.GitCommand.NewBranch(newBranchName, branch.Name); err != nil { + if err := gui.GitCommand.Branch.New(newBranchName, branch.Name); err != nil { return gui.surfaceError(err) } @@ -298,7 +298,7 @@ func (gui *Gui) deleteNamedBranch(selectedBranch *models.Branch, force bool) err prompt: message, handleConfirm: func() error { gui.logAction(gui.Tr.Actions.DeleteBranch) - if err := gui.GitCommand.DeleteBranch(selectedBranch.Name, force); err != nil { + if err := gui.GitCommand.Branch.Delete(selectedBranch.Name, force); err != nil { errMessage := err.Error() if !force && strings.Contains(errMessage, "git branch -D ") { return gui.deleteNamedBranch(selectedBranch, true) @@ -315,7 +315,7 @@ func (gui *Gui) mergeBranchIntoCheckedOutBranch(branchName string) error { return err } - if gui.GitCommand.IsHeadDetached() { + if gui.GitCommand.Branch.IsHeadDetached() { return gui.createErrorPanel("Cannot merge branch in detached head state. You might have checked out a commit directly or a remote branch, in which case you should checkout the local branch you want to be on") } checkedOutBranchName := gui.getCheckedOutBranch().Name @@ -335,7 +335,7 @@ func (gui *Gui) mergeBranchIntoCheckedOutBranch(branchName string) error { prompt: prompt, handleConfirm: func() error { gui.logAction(gui.Tr.Actions.Merge) - err := gui.GitCommand.Merge(branchName, commands.MergeOpts{}) + err := gui.GitCommand.Branch.Merge(branchName, commands.MergeOpts{}) return gui.handleGenericMergeCommandResult(err) }, }) @@ -377,7 +377,7 @@ func (gui *Gui) handleRebaseOntoBranch(selectedBranchName string) error { prompt: prompt, handleConfirm: func() error { gui.logAction(gui.Tr.Actions.RebaseBranch) - err := gui.GitCommand.RebaseBranch(selectedBranchName) + err := gui.GitCommand.Rebase.RebaseBranch(selectedBranchName) return gui.handleGenericMergeCommandResult(err) }, }) @@ -396,7 +396,7 @@ func (gui *Gui) handleFastForward() error { return gui.createErrorPanel(gui.Tr.FwdCommitsToPush) } - upstream, err := gui.GitCommand.GetUpstreamForBranch(branch.Name) + upstream, err := gui.GitCommand.Branch.GetUpstream(branch.Name) if err != nil { return gui.surfaceError(err) } @@ -421,7 +421,7 @@ func (gui *Gui) handleFastForward() error { _ = gui.pullWithLock(PullFilesOptions{action: action, FastForwardOnly: true}) } else { gui.logAction(action) - err := gui.GitCommand.FastForward(branch.Name, remoteName, remoteBranchName, gui.promptUserForCredential) + err := gui.GitCommand.Sync.FastForward(branch.Name, remoteName, remoteBranchName) gui.handleCredentialsPopup(err) _ = gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []RefreshableView{BRANCHES}}) } @@ -450,7 +450,7 @@ func (gui *Gui) handleRenameBranch() error { initialContent: branch.Name, handleConfirm: func(newBranchName string) error { gui.logAction(gui.Tr.Actions.RenameBranch) - if err := gui.GitCommand.RenameBranch(branch.Name, newBranchName); err != nil { + if err := gui.GitCommand.Branch.Rename(branch.Name, newBranchName); err != nil { return gui.surfaceError(err) } @@ -519,7 +519,7 @@ func (gui *Gui) handleNewBranchOffCurrentItem() error { initialContent: prefilledName, handleConfirm: func(response string) error { gui.logAction(gui.Tr.Actions.CreateBranch) - if err := gui.GitCommand.NewBranch(sanitizedBranchName(response), item.ID()); err != nil { + if err := gui.GitCommand.Branch.New(sanitizedBranchName(response), item.ID()); err != nil { return err } diff --git a/pkg/gui/cherry_picking.go b/pkg/gui/cherry_picking.go index 8faa55118..3f49d3534 100644 --- a/pkg/gui/cherry_picking.go +++ b/pkg/gui/cherry_picking.go @@ -149,7 +149,7 @@ func (gui *Gui) HandlePasteCommits() error { handleConfirm: func() error { return gui.WithWaitingStatus(gui.Tr.CherryPickingStatus, func() error { gui.logAction(gui.Tr.Actions.CherryPick) - err := gui.GitCommand.CherryPickCommits(gui.State.Modes.CherryPicking.CherryPickedCommits) + err := gui.GitCommand.Rebase.CherryPickCommits(gui.State.Modes.CherryPicking.CherryPickedCommits) return gui.handleGenericMergeCommandResult(err) }) }, diff --git a/pkg/gui/commit_files_panel.go b/pkg/gui/commit_files_panel.go index 6dd894906..dab087fc1 100644 --- a/pkg/gui/commit_files_panel.go +++ b/pkg/gui/commit_files_panel.go @@ -45,7 +45,7 @@ func (gui *Gui) commitFilesRenderToMain() error { to := gui.State.CommitFileManager.GetParent() from, reverse := gui.getFromAndReverseArgsForDiff(to) - cmdObj := gui.GitCommand.ShowFileDiffCmdObj(from, to, reverse, node.GetPath(), false) + cmdObj := gui.GitCommand.WorkingTree.ShowFileDiffCmdObj(from, to, reverse, node.GetPath(), false) task := NewRunPtyTask(cmdObj.GetCmd()) return gui.refreshMainViews(refreshMainOpts{ @@ -64,7 +64,7 @@ func (gui *Gui) handleCheckoutCommitFile() error { } gui.logAction(gui.Tr.Actions.CheckoutFile) - if err := gui.GitCommand.CheckoutFile(gui.State.CommitFileManager.GetParent(), node.GetPath()); err != nil { + if err := gui.GitCommand.WorkingTree.CheckoutFile(gui.State.CommitFileManager.GetParent(), node.GetPath()); err != nil { return gui.surfaceError(err) } @@ -84,7 +84,7 @@ func (gui *Gui) handleDiscardOldFileChange() error { handleConfirm: func() error { return gui.WithWaitingStatus(gui.Tr.RebasingStatus, func() error { gui.logAction(gui.Tr.Actions.DiscardOldFileChange) - if err := gui.GitCommand.DiscardOldFileChanges(gui.State.Commits, gui.State.Panels.Commits.SelectedLineIdx, fileName); err != nil { + if err := gui.GitCommand.Rebase.DiscardOldFileChanges(gui.State.Commits, gui.State.Panels.Commits.SelectedLineIdx, fileName); err != nil { if err := gui.handleGenericMergeCommandResult(err); err != nil { return err } @@ -145,7 +145,7 @@ func (gui *Gui) handleToggleFileForPatch() error { } toggleTheFile := func() error { - if !gui.GitCommand.PatchManager.Active() { + if !gui.GitCommand.Patch.PatchManager.Active() { if err := gui.startPatchManager(); err != nil { return err } @@ -154,14 +154,14 @@ func (gui *Gui) handleToggleFileForPatch() error { // if there is any file that hasn't been fully added we'll fully add everything, // otherwise we'll remove everything adding := node.AnyFile(func(file *models.CommitFile) bool { - return gui.GitCommand.PatchManager.GetFileStatus(file.Name, gui.State.CommitFileManager.GetParent()) != patch.WHOLE + return gui.GitCommand.Patch.PatchManager.GetFileStatus(file.Name, gui.State.CommitFileManager.GetParent()) != patch.WHOLE }) err := node.ForEachFile(func(file *models.CommitFile) error { if adding { - return gui.GitCommand.PatchManager.AddFileWhole(file.Name) + return gui.GitCommand.Patch.PatchManager.AddFileWhole(file.Name) } else { - return gui.GitCommand.PatchManager.RemoveFile(file.Name) + return gui.GitCommand.Patch.PatchManager.RemoveFile(file.Name) } }) @@ -169,19 +169,19 @@ func (gui *Gui) handleToggleFileForPatch() error { return gui.surfaceError(err) } - if gui.GitCommand.PatchManager.IsEmpty() { - gui.GitCommand.PatchManager.Reset() + if gui.GitCommand.Patch.PatchManager.IsEmpty() { + gui.GitCommand.Patch.PatchManager.Reset() } return gui.postRefreshUpdate(gui.State.Contexts.CommitFiles) } - if gui.GitCommand.PatchManager.Active() && gui.GitCommand.PatchManager.To != gui.State.CommitFileManager.GetParent() { + if gui.GitCommand.Patch.PatchManager.Active() && gui.GitCommand.Patch.PatchManager.To != gui.State.CommitFileManager.GetParent() { return gui.ask(askOpts{ title: gui.Tr.DiscardPatch, prompt: gui.Tr.DiscardPatchConfirm, handleConfirm: func() error { - gui.GitCommand.PatchManager.Reset() + gui.GitCommand.Patch.PatchManager.Reset() return toggleTheFile() }, }) @@ -196,7 +196,7 @@ func (gui *Gui) startPatchManager() error { to := gui.State.Panels.CommitFiles.refName from, reverse := gui.getFromAndReverseArgsForDiff(to) - gui.GitCommand.PatchManager.Start(from, to, reverse, canRebase) + gui.GitCommand.Patch.PatchManager.Start(from, to, reverse, canRebase) return nil } @@ -215,7 +215,7 @@ func (gui *Gui) enterCommitFile(opts OnFocusOpts) error { } enterTheFile := func() error { - if !gui.GitCommand.PatchManager.Active() { + if !gui.GitCommand.Patch.PatchManager.Active() { if err := gui.startPatchManager(); err != nil { return err } @@ -224,13 +224,13 @@ func (gui *Gui) enterCommitFile(opts OnFocusOpts) error { return gui.pushContext(gui.State.Contexts.PatchBuilding, opts) } - if gui.GitCommand.PatchManager.Active() && gui.GitCommand.PatchManager.To != gui.State.CommitFileManager.GetParent() { + if gui.GitCommand.Patch.PatchManager.Active() && gui.GitCommand.Patch.PatchManager.To != gui.State.CommitFileManager.GetParent() { return gui.ask(askOpts{ title: gui.Tr.DiscardPatch, prompt: gui.Tr.DiscardPatchConfirm, handlersManageFocus: true, handleConfirm: func() error { - gui.GitCommand.PatchManager.Reset() + gui.GitCommand.Patch.PatchManager.Reset() return enterTheFile() }, handleClose: func() error { diff --git a/pkg/gui/commit_message_panel.go b/pkg/gui/commit_message_panel.go index 5f4fa8069..32ddb5829 100644 --- a/pkg/gui/commit_message_panel.go +++ b/pkg/gui/commit_message_panel.go @@ -24,7 +24,7 @@ func (gui *Gui) handleCommitConfirm() error { flags = append(flags, "--signoff") } - cmdObj := gui.GitCommand.CommitCmdObj(message, strings.Join(flags, " ")) + cmdObj := gui.GitCommand.Commit.CommitCmdObj(message, strings.Join(flags, " ")) gui.logAction(gui.Tr.Actions.Commit) _ = gui.returnFromContext() diff --git a/pkg/gui/commits_panel.go b/pkg/gui/commits_panel.go index 3e3bf3dca..1148e0a25 100644 --- a/pkg/gui/commits_panel.go +++ b/pkg/gui/commits_panel.go @@ -45,7 +45,7 @@ func (gui *Gui) branchCommitsRenderToMain() error { if commit == nil { task = NewRenderStringTask(gui.Tr.NoCommitsThisBranch) } else { - cmdObj := gui.GitCommand.ShowCmdObj(commit.Sha, gui.State.Modes.Filtering.GetPath()) + cmdObj := gui.GitCommand.Commit.ShowCmdObj(commit.Sha, gui.State.Modes.Filtering.GetPath()) task = NewRunPtyTask(cmdObj.GetCmd()) } @@ -173,7 +173,7 @@ func (gui *Gui) handleCommitSquashDown() error { handleConfirm: func() error { return gui.WithWaitingStatus(gui.Tr.SquashingStatus, func() error { gui.logAction(gui.Tr.Actions.SquashCommitDown) - err := gui.GitCommand.InteractiveRebase(gui.State.Commits, gui.State.Panels.Commits.SelectedLineIdx, "squash") + err := gui.GitCommand.Rebase.InteractiveRebase(gui.State.Commits, gui.State.Panels.Commits.SelectedLineIdx, "squash") return gui.handleGenericMergeCommandResult(err) }) }, @@ -203,14 +203,14 @@ func (gui *Gui) handleCommitFixup() error { handleConfirm: func() error { return gui.WithWaitingStatus(gui.Tr.FixingStatus, func() error { gui.logAction(gui.Tr.Actions.FixupCommit) - err := gui.GitCommand.InteractiveRebase(gui.State.Commits, gui.State.Panels.Commits.SelectedLineIdx, "fixup") + err := gui.GitCommand.Rebase.InteractiveRebase(gui.State.Commits, gui.State.Panels.Commits.SelectedLineIdx, "fixup") return gui.handleGenericMergeCommandResult(err) }) }, }) } -func (gui *Gui) handleRenameCommit() error { +func (gui *Gui) handleRewordCommit() error { if ok, err := gui.validateNotInFilterMode(); err != nil || !ok { return err } @@ -224,7 +224,7 @@ func (gui *Gui) handleRenameCommit() error { } if gui.State.Panels.Commits.SelectedLineIdx != 0 { - return gui.createErrorPanel(gui.Tr.OnlyRenameTopCommit) + return gui.createErrorPanel(gui.Tr.OnlyRewordTopCommit) } commit := gui.getSelectedLocalCommit() @@ -232,17 +232,17 @@ func (gui *Gui) handleRenameCommit() error { return nil } - message, err := gui.GitCommand.GetCommitMessage(commit.Sha) + message, err := gui.GitCommand.Commit.GetCommitMessage(commit.Sha) if err != nil { return gui.surfaceError(err) } return gui.prompt(promptOpts{ - title: gui.Tr.LcRenameCommit, + title: gui.Tr.LcRewordCommit, initialContent: message, handleConfirm: func(response string) error { gui.logAction(gui.Tr.Actions.RewordCommit) - if err := gui.GitCommand.RenameCommit(response); err != nil { + if err := gui.GitCommand.Commit.RewordLastCommit(response); err != nil { return gui.surfaceError(err) } @@ -265,7 +265,7 @@ func (gui *Gui) handleRenameCommitEditor() error { } gui.logAction(gui.Tr.Actions.RewordCommit) - subProcess, err := gui.GitCommand.RewordCommit(gui.State.Commits, gui.State.Panels.Commits.SelectedLineIdx) + subProcess, err := gui.GitCommand.Rebase.RewordCommit(gui.State.Commits, gui.State.Panels.Commits.SelectedLineIdx) if err != nil { return gui.surfaceError(err) } @@ -299,7 +299,7 @@ func (gui *Gui) handleMidRebaseCommand(action string) (bool, error) { false, ) - if err := gui.GitCommand.EditRebaseTodo(gui.State.Panels.Commits.SelectedLineIdx, action); err != nil { + if err := gui.GitCommand.Rebase.EditRebaseTodo(gui.State.Panels.Commits.SelectedLineIdx, action); err != nil { return false, gui.surfaceError(err) } @@ -325,7 +325,7 @@ func (gui *Gui) handleCommitDelete() error { handleConfirm: func() error { return gui.WithWaitingStatus(gui.Tr.DeletingStatus, func() error { gui.logAction(gui.Tr.Actions.DropCommit) - err := gui.GitCommand.InteractiveRebase(gui.State.Commits, gui.State.Panels.Commits.SelectedLineIdx, "drop") + err := gui.GitCommand.Rebase.InteractiveRebase(gui.State.Commits, gui.State.Panels.Commits.SelectedLineIdx, "drop") return gui.handleGenericMergeCommandResult(err) }) }, @@ -349,7 +349,7 @@ func (gui *Gui) handleCommitMoveDown() error { gui.logAction(gui.Tr.Actions.MoveCommitDown) gui.logCommand(fmt.Sprintf("Moving commit %s down", selectedCommit.ShortSha()), false) - if err := gui.GitCommand.MoveTodoDown(index); err != nil { + if err := gui.GitCommand.Rebase.MoveTodoDown(index); err != nil { return gui.surfaceError(err) } gui.State.Panels.Commits.SelectedLineIdx++ @@ -358,7 +358,7 @@ func (gui *Gui) handleCommitMoveDown() error { return gui.WithWaitingStatus(gui.Tr.MovingStatus, func() error { gui.logAction(gui.Tr.Actions.MoveCommitDown) - err := gui.GitCommand.MoveCommitDown(gui.State.Commits, index) + err := gui.GitCommand.Rebase.MoveCommitDown(gui.State.Commits, index) if err == nil { gui.State.Panels.Commits.SelectedLineIdx++ } @@ -386,7 +386,7 @@ func (gui *Gui) handleCommitMoveUp() error { false, ) - if err := gui.GitCommand.MoveTodoDown(index - 1); err != nil { + if err := gui.GitCommand.Rebase.MoveTodoDown(index - 1); err != nil { return gui.surfaceError(err) } gui.State.Panels.Commits.SelectedLineIdx-- @@ -395,7 +395,7 @@ func (gui *Gui) handleCommitMoveUp() error { return gui.WithWaitingStatus(gui.Tr.MovingStatus, func() error { gui.logAction(gui.Tr.Actions.MoveCommitUp) - err := gui.GitCommand.MoveCommitDown(gui.State.Commits, index-1) + err := gui.GitCommand.Rebase.MoveCommitDown(gui.State.Commits, index-1) if err == nil { gui.State.Panels.Commits.SelectedLineIdx-- } @@ -418,7 +418,7 @@ func (gui *Gui) handleCommitEdit() error { return gui.WithWaitingStatus(gui.Tr.RebasingStatus, func() error { gui.logAction(gui.Tr.Actions.EditCommit) - err = gui.GitCommand.InteractiveRebase(gui.State.Commits, gui.State.Panels.Commits.SelectedLineIdx, "edit") + err = gui.GitCommand.Rebase.InteractiveRebase(gui.State.Commits, gui.State.Panels.Commits.SelectedLineIdx, "edit") return gui.handleGenericMergeCommandResult(err) }) } @@ -434,7 +434,7 @@ func (gui *Gui) handleCommitAmendTo() error { handleConfirm: func() error { return gui.WithWaitingStatus(gui.Tr.AmendingStatus, func() error { gui.logAction(gui.Tr.Actions.AmendCommit) - err := gui.GitCommand.AmendTo(gui.State.Commits[gui.State.Panels.Commits.SelectedLineIdx].Sha) + err := gui.GitCommand.Rebase.AmendTo(gui.State.Commits[gui.State.Panels.Commits.SelectedLineIdx].Sha) return gui.handleGenericMergeCommandResult(err) }) }, @@ -470,7 +470,7 @@ func (gui *Gui) handleCommitRevert() error { return gui.createRevertMergeCommitMenu(commit) } else { gui.logAction(gui.Tr.Actions.RevertCommit) - if err := gui.GitCommand.Revert(commit.Sha); err != nil { + if err := gui.GitCommand.Commit.Revert(commit.Sha); err != nil { return gui.surfaceError(err) } return gui.afterRevertCommit() @@ -481,7 +481,7 @@ func (gui *Gui) createRevertMergeCommitMenu(commit *models.Commit) error { menuItems := make([]*menuItem, len(commit.Parents)) for i, parentSha := range commit.Parents { i := i - message, err := gui.GitCommand.GetCommitMessageFirstLine(parentSha) + message, err := gui.GitCommand.Commit.GetCommitMessageFirstLine(parentSha) if err != nil { return gui.surfaceError(err) } @@ -491,7 +491,7 @@ func (gui *Gui) createRevertMergeCommitMenu(commit *models.Commit) error { onPress: func() error { parentNumber := i + 1 gui.logAction(gui.Tr.Actions.RevertCommit) - if err := gui.GitCommand.RevertMerge(commit.Sha, parentNumber); err != nil { + if err := gui.GitCommand.Commit.RevertMerge(commit.Sha, parentNumber); err != nil { return gui.surfaceError(err) } return gui.afterRevertCommit() @@ -538,7 +538,7 @@ func (gui *Gui) handleCreateFixupCommit() error { prompt: prompt, handleConfirm: func() error { gui.logAction(gui.Tr.Actions.CreateFixupCommit) - if err := gui.GitCommand.CreateFixupCommit(commit.Sha); err != nil { + if err := gui.GitCommand.Commit.CreateFixupCommit(commit.Sha); err != nil { return gui.surfaceError(err) } @@ -570,7 +570,7 @@ func (gui *Gui) handleSquashAllAboveFixupCommits() error { handleConfirm: func() error { return gui.WithWaitingStatus(gui.Tr.SquashingStatus, func() error { gui.logAction(gui.Tr.Actions.SquashAllAboveFixupCommits) - err := gui.GitCommand.SquashAllAboveFixupCommits(commit.Sha) + err := gui.GitCommand.Rebase.SquashAllAboveFixupCommits(commit.Sha) return gui.handleGenericMergeCommandResult(err) }) }, @@ -618,7 +618,7 @@ func (gui *Gui) handleCreateAnnotatedTag(commitSha string) error { title: gui.Tr.TagMessageTitle, handleConfirm: func(msg string) error { gui.logAction(gui.Tr.Actions.CreateAnnotatedTag) - if err := gui.GitCommand.CreateAnnotatedTag(tagName, commitSha, msg); err != nil { + if err := gui.GitCommand.Tag.CreateAnnotated(tagName, commitSha, msg); err != nil { return gui.surfaceError(err) } return gui.afterTagCreate(tagName) @@ -633,7 +633,7 @@ func (gui *Gui) handleCreateLightweightTag(commitSha string) error { title: gui.Tr.TagNameTitle, handleConfirm: func(tagName string) error { gui.logAction(gui.Tr.Actions.CreateLightweightTag) - if err := gui.GitCommand.CreateLightweightTag(tagName, commitSha); err != nil { + if err := gui.GitCommand.Tag.CreateLightweight(tagName, commitSha); err != nil { return gui.surfaceError(err) } return gui.afterTagCreate(tagName) @@ -702,7 +702,7 @@ func (gui *Gui) handleCopySelectedCommitMessageToClipboard() error { return nil } - message, err := gui.GitCommand.GetCommitMessage(commit.Sha) + message, err := gui.GitCommand.Commit.GetCommitMessage(commit.Sha) if err != nil { return gui.surfaceError(err) } diff --git a/pkg/gui/diff_context_size.go b/pkg/gui/diff_context_size.go index 5e37c18af..4a6090ee8 100644 --- a/pkg/gui/diff_context_size.go +++ b/pkg/gui/diff_context_size.go @@ -39,7 +39,7 @@ func (gui *Gui) DecreaseContextInDiffView() error { } func (gui *Gui) CheckCanChangeContext() error { - if gui.GitCommand.PatchManager.Active() { + if gui.GitCommand.Patch.PatchManager.Active() { return errors.New(gui.Tr.CantChangeContextSizeError) } diff --git a/pkg/gui/diff_context_size_test.go b/pkg/gui/diff_context_size_test.go index bab2fd553..a9b8e05bf 100644 --- a/pkg/gui/diff_context_size_test.go +++ b/pkg/gui/diff_context_size_test.go @@ -27,7 +27,7 @@ func setupGuiForTest(gui *Gui) { gui.g = &gocui.Gui{} gui.Views.Main, _ = gui.prepareView("main") gui.Views.Secondary, _ = gui.prepareView("secondary") - gui.GitCommand.PatchManager = &patch.PatchManager{} + gui.GitCommand.Patch.PatchManager = &patch.PatchManager{} _, _ = gui.refreshLineByLinePanel(diffForTest, "", false, 11) } @@ -136,7 +136,7 @@ func TestDoesntIncreaseContextInDiffViewInContextWhenInPatchBuildingMode(t *test setupGuiForTest(gui) gui.UserConfig.Git.DiffContextSize = 2 _ = gui.pushContextDirect(gui.State.Contexts.CommitFiles) - gui.GitCommand.PatchManager.Start("from", "to", false, false) + gui.GitCommand.Patch.PatchManager.Start("from", "to", false, false) errorCount := 0 gui.PopupHandler = &TestPopupHandler{ @@ -158,7 +158,7 @@ func TestDoesntDecreaseContextInDiffViewInContextWhenInPatchBuildingMode(t *test setupGuiForTest(gui) gui.UserConfig.Git.DiffContextSize = 2 _ = gui.pushContextDirect(gui.State.Contexts.CommitFiles) - gui.GitCommand.PatchManager.Start("from", "to", false, false) + gui.GitCommand.Patch.PatchManager.Start("from", "to", false, false) errorCount := 0 gui.PopupHandler = &TestPopupHandler{ diff --git a/pkg/gui/discard_changes_menu_panel.go b/pkg/gui/discard_changes_menu_panel.go index 4ed32b2f5..1eb335941 100644 --- a/pkg/gui/discard_changes_menu_panel.go +++ b/pkg/gui/discard_changes_menu_panel.go @@ -13,7 +13,7 @@ func (gui *Gui) handleCreateDiscardMenu() error { displayString: gui.Tr.LcDiscardAllChanges, onPress: func() error { gui.logAction(gui.Tr.Actions.DiscardAllChangesInDirectory) - if err := gui.GitCommand.DiscardAllDirChanges(node); err != nil { + if err := gui.GitCommand.WorkingTree.DiscardAllDirChanges(node); err != nil { return gui.surfaceError(err) } return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []RefreshableView{FILES}}) @@ -26,7 +26,7 @@ func (gui *Gui) handleCreateDiscardMenu() error { displayString: gui.Tr.LcDiscardUnstagedChanges, onPress: func() error { gui.logAction(gui.Tr.Actions.DiscardUnstagedChangesInDirectory) - if err := gui.GitCommand.DiscardUnstagedDirChanges(node); err != nil { + if err := gui.GitCommand.WorkingTree.DiscardUnstagedDirChanges(node); err != nil { return gui.surfaceError(err) } @@ -55,7 +55,7 @@ func (gui *Gui) handleCreateDiscardMenu() error { displayString: gui.Tr.LcDiscardAllChanges, onPress: func() error { gui.logAction(gui.Tr.Actions.DiscardAllChangesInFile) - if err := gui.GitCommand.DiscardAllFileChanges(file); err != nil { + if err := gui.GitCommand.WorkingTree.DiscardAllFileChanges(file); err != nil { return gui.surfaceError(err) } return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []RefreshableView{FILES}}) @@ -68,7 +68,7 @@ func (gui *Gui) handleCreateDiscardMenu() error { displayString: gui.Tr.LcDiscardUnstagedChanges, onPress: func() error { gui.logAction(gui.Tr.Actions.DiscardAllUnstagedChangesInFile) - if err := gui.GitCommand.DiscardUnstagedFileChanges(file); err != nil { + if err := gui.GitCommand.WorkingTree.DiscardUnstagedFileChanges(file); err != nil { return gui.surfaceError(err) } diff --git a/pkg/gui/dummies.go b/pkg/gui/dummies.go index b86c7b8bc..32f032e34 100644 --- a/pkg/gui/dummies.go +++ b/pkg/gui/dummies.go @@ -1,7 +1,6 @@ package gui import ( - "github.com/jesseduffield/lazygit/pkg/commands" "github.com/jesseduffield/lazygit/pkg/commands/oscommands" "github.com/jesseduffield/lazygit/pkg/config" "github.com/jesseduffield/lazygit/pkg/updates" @@ -17,6 +16,6 @@ func NewDummyUpdater() *updates.Updater { func NewDummyGui() *Gui { newAppConfig := config.NewDummyAppConfig() - dummyGui, _ := NewGui(utils.NewDummyCommon(), commands.NewDummyGitCommand(), oscommands.NewDummyOSCommand(), newAppConfig, NewDummyUpdater(), "", false) + dummyGui, _ := NewGui(utils.NewDummyCommon(), newAppConfig, utils.NewDummyGitConfig(), NewDummyUpdater(), "", false) return dummyGui } diff --git a/pkg/gui/files_panel.go b/pkg/gui/files_panel.go index 2c7cd2a3a..855a8f851 100644 --- a/pkg/gui/files_panel.go +++ b/pkg/gui/files_panel.go @@ -58,7 +58,7 @@ func (gui *Gui) filesRenderToMain() error { return gui.refreshMergePanelWithLock() } - cmdObj := gui.GitCommand.WorktreeFileDiffCmdObj(node, false, !node.GetHasUnstagedChanges() && node.GetHasStagedChanges(), gui.State.IgnoreWhitespaceInDiffView) + cmdObj := gui.GitCommand.WorkingTree.WorktreeFileDiffCmdObj(node, false, !node.GetHasUnstagedChanges() && node.GetHasStagedChanges(), gui.State.IgnoreWhitespaceInDiffView) refreshOpts := refreshMainOpts{main: &viewUpdateOpts{ title: gui.Tr.UnstagedChanges, @@ -67,7 +67,7 @@ func (gui *Gui) filesRenderToMain() error { if node.GetHasUnstagedChanges() { if node.GetHasStagedChanges() { - cmdObj := gui.GitCommand.WorktreeFileDiffCmdObj(node, false, true, gui.State.IgnoreWhitespaceInDiffView) + cmdObj := gui.GitCommand.WorkingTree.WorktreeFileDiffCmdObj(node, false, true, gui.State.IgnoreWhitespaceInDiffView) refreshOpts.secondary = &viewUpdateOpts{ title: gui.Tr.StagedChanges, @@ -157,7 +157,7 @@ func (gui *Gui) stageSelectedFile() error { return nil } - return gui.GitCommand.StageFile(file.Name) + return gui.GitCommand.WorkingTree.StageFile(file.Name) } func (gui *Gui) handleEnterFile() error { @@ -207,12 +207,12 @@ func (gui *Gui) handleFilePress() error { if file.HasUnstagedChanges { gui.logAction(gui.Tr.Actions.StageFile) - if err := gui.GitCommand.StageFile(file.Name); err != nil { + if err := gui.GitCommand.WorkingTree.StageFile(file.Name); err != nil { return gui.surfaceError(err) } } else { gui.logAction(gui.Tr.Actions.UnstageFile) - if err := gui.GitCommand.UnStageFile(file.Names(), file.Tracked); err != nil { + if err := gui.GitCommand.WorkingTree.UnStageFile(file.Names(), file.Tracked); err != nil { return gui.surfaceError(err) } } @@ -225,13 +225,13 @@ func (gui *Gui) handleFilePress() error { if node.GetHasUnstagedChanges() { gui.logAction(gui.Tr.Actions.StageFile) - if err := gui.GitCommand.StageFile(node.Path); err != nil { + if err := gui.GitCommand.WorkingTree.StageFile(node.Path); err != nil { return gui.surfaceError(err) } } else { // pretty sure it doesn't matter that we're always passing true here gui.logAction(gui.Tr.Actions.UnstageFile) - if err := gui.GitCommand.UnStageFile([]string{node.Path}, true); err != nil { + if err := gui.GitCommand.WorkingTree.UnStageFile([]string{node.Path}, true); err != nil { return gui.surfaceError(err) } } @@ -262,10 +262,10 @@ func (gui *Gui) handleStageAll() error { var err error if gui.allFilesStaged() { gui.logAction(gui.Tr.Actions.UnstageAllFiles) - err = gui.GitCommand.UnstageAll() + err = gui.GitCommand.WorkingTree.UnstageAll() } else { gui.logAction(gui.Tr.Actions.StageAllFiles) - err = gui.GitCommand.StageAll() + err = gui.GitCommand.WorkingTree.StageAll() } if err != nil { _ = gui.surfaceError(err) @@ -288,12 +288,10 @@ func (gui *Gui) handleIgnoreFile() error { return gui.createErrorPanel("Cannot ignore .gitignore") } - gui.logAction(gui.Tr.Actions.IgnoreFile) - unstageFiles := func() error { return node.ForEachFile(func(file *models.File) error { if file.HasStagedChanges { - if err := gui.GitCommand.UnStageFile(file.Names(), file.Tracked); err != nil { + if err := gui.GitCommand.WorkingTree.UnStageFile(file.Names(), file.Tracked); err != nil { return err } } @@ -307,16 +305,17 @@ func (gui *Gui) handleIgnoreFile() error { title: gui.Tr.IgnoreTracked, prompt: gui.Tr.IgnoreTrackedPrompt, handleConfirm: func() error { + gui.logAction(gui.Tr.Actions.IgnoreFile) // not 100% sure if this is necessary but I'll assume it is if err := unstageFiles(); err != nil { return err } - if err := gui.GitCommand.RemoveTrackedFiles(node.GetPath()); err != nil { + if err := gui.GitCommand.WorkingTree.RemoveTrackedFiles(node.GetPath()); err != nil { return err } - if err := gui.GitCommand.Ignore(node.GetPath()); err != nil { + if err := gui.GitCommand.WorkingTree.Ignore(node.GetPath()); err != nil { return err } return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{FILES}}) @@ -324,11 +323,13 @@ func (gui *Gui) handleIgnoreFile() error { }) } + gui.logAction(gui.Tr.Actions.IgnoreFile) + if err := unstageFiles(); err != nil { return err } - if err := gui.GitCommand.Ignore(node.GetPath()); err != nil { + if err := gui.GitCommand.WorkingTree.Ignore(node.GetPath()); err != nil { return gui.surfaceError(err) } @@ -362,7 +363,7 @@ func (gui *Gui) prepareFilesForCommit() error { noStagedFiles := len(gui.stagedFiles()) == 0 if noStagedFiles && gui.UserConfig.Gui.SkipNoStagedFilesWarning { gui.logAction(gui.Tr.Actions.StageAllFiles) - err := gui.GitCommand.StageAll() + err := gui.GitCommand.WorkingTree.StageAll() if err != nil { return err } @@ -423,7 +424,7 @@ func (gui *Gui) promptToStageAllAndRetry(retry func() error) error { prompt: gui.Tr.NoFilesStagedPrompt, handleConfirm: func() error { gui.logAction(gui.Tr.Actions.StageAllFiles) - if err := gui.GitCommand.StageAll(); err != nil { + if err := gui.GitCommand.WorkingTree.StageAll(); err != nil { return gui.surfaceError(err) } if err := gui.refreshFilesAndSubmodules(); err != nil { @@ -452,7 +453,7 @@ func (gui *Gui) handleAmendCommitPress() error { title: strings.Title(gui.Tr.AmendLastCommit), prompt: gui.Tr.SureToAmend, handleConfirm: func() error { - cmdObj := gui.GitCommand.AmendHeadCmdObj() + cmdObj := gui.GitCommand.Commit.AmendHeadCmdObj() gui.logAction(gui.Tr.Actions.AmendCommit) return gui.withGpgHandling(cmdObj, gui.Tr.AmendingStatus, nil) }, @@ -520,7 +521,7 @@ func (gui *Gui) editFile(filename string) error { } func (gui *Gui) editFileAtLine(filename string, lineNumber int) error { - cmdStr, err := gui.GitCommand.EditFileCmdStr(filename, lineNumber) + cmdStr, err := gui.GitCommand.File.GetEditCmdStr(filename, lineNumber) if err != nil { return gui.surfaceError(err) } @@ -569,8 +570,7 @@ func (gui *Gui) refreshStateFiles() error { prevNodes := gui.State.FileManager.GetAllItems() prevSelectedLineIdx := gui.State.Panels.Files.SelectedLineIdx - files := loaders. - NewFileLoader(gui.Common, gui.GitCommand.Cmd, gui.GitCommand.GitConfig). + files := gui.GitCommand.Loaders.Files. GetStatusFiles(loaders.GetStatusFileOptions{}) // for when you stage the old file of a rename and the new file is in a collapsed dir @@ -685,7 +685,7 @@ func (gui *Gui) handlePullFiles() error { initialContent: suggestedRemote + "/" + currentBranch.Name, findSuggestionsFunc: gui.getRemoteBranchesSuggestionsFunc("/"), handleConfirm: func(upstream string) error { - if err := gui.GitCommand.SetUpstreamBranch(upstream); err != nil { + if err := gui.GitCommand.Branch.SetCurrentBranchUpstream(upstream); err != nil { errorMessage := err.Error() if strings.Contains(errorMessage, "does not exist") { errorMessage = fmt.Sprintf("upstream branch %s not found.\nIf you expect it to exist, you should fetch (with 'f').\nOtherwise, you should push (with 'shift+P')", upstream) @@ -724,12 +724,11 @@ func (gui *Gui) pullWithLock(opts PullFilesOptions) error { gui.logAction(opts.action) - err := gui.GitCommand.Pull( + err := gui.GitCommand.Sync.Pull( commands.PullOptions{ - PromptUserForCredential: gui.promptUserForCredential, - RemoteName: opts.RemoteName, - BranchName: opts.BranchName, - FastForwardOnly: opts.FastForwardOnly, + RemoteName: opts.RemoteName, + BranchName: opts.BranchName, + FastForwardOnly: opts.FastForwardOnly, }, ) if err == nil { @@ -751,12 +750,12 @@ func (gui *Gui) push(opts pushOpts) error { } go utils.Safe(func() { gui.logAction(gui.Tr.Actions.Push) - err := gui.GitCommand.Push(commands.PushOpts{ + err := gui.GitCommand.Sync.Push(commands.PushOpts{ Force: opts.force, UpstreamRemote: opts.upstreamRemote, UpstreamBranch: opts.upstreamBranch, SetUpstream: opts.setUpstream, - }, gui.promptUserForCredential) + }) if err != nil && !opts.force && strings.Contains(err.Error(), "Updates were rejected") { forcePushDisabled := gui.UserConfig.Git.DisableForcePushing @@ -819,7 +818,7 @@ func (gui *Gui) pushFiles() error { suggestedRemote := getSuggestedRemote(gui.State.Remotes) - if gui.GitCommand.PushToCurrent { + if gui.GitCommand.Config.GetPushToCurrent() { return gui.push(pushOpts{setUpstream: true}) } else { return gui.prompt(promptOpts{ @@ -954,14 +953,14 @@ func (gui *Gui) handleCreateStashMenu() error { displayString: gui.Tr.LcStashAllChanges, onPress: func() error { gui.logAction(gui.Tr.Actions.StashAllChanges) - return gui.handleStashSave(gui.GitCommand.StashSave) + return gui.handleStashSave(gui.GitCommand.Stash.Save) }, }, { displayString: gui.Tr.LcStashStagedChanges, onPress: func() error { gui.logAction(gui.Tr.Actions.StashStagedChanges) - return gui.handleStashSave(gui.GitCommand.StashSaveStagedChanges) + return gui.handleStashSave(gui.GitCommand.Stash.SaveStagedChanges) }, }, } @@ -970,7 +969,7 @@ func (gui *Gui) handleCreateStashMenu() error { } func (gui *Gui) handleStashChanges() error { - return gui.handleStashSave(gui.GitCommand.StashSave) + return gui.handleStashSave(gui.GitCommand.Stash.Save) } func (gui *Gui) handleCreateResetToUpstreamMenu() error { @@ -1026,7 +1025,7 @@ func (gui *Gui) handleOpenMergeTool() error { handleConfirm: func() error { gui.logAction(gui.Tr.Actions.OpenMergeTool) return gui.runSubprocessWithSuspenseAndRefresh( - gui.GitCommand.OpenMergeToolCmdObj(), + gui.GitCommand.WorkingTree.OpenMergeToolCmdObj(), ) }, }) diff --git a/pkg/gui/global_handlers.go b/pkg/gui/global_handlers.go index 68f2b6402..b211baf27 100644 --- a/pkg/gui/global_handlers.go +++ b/pkg/gui/global_handlers.go @@ -215,8 +215,7 @@ func (gui *Gui) fetch() (err error) { defer gui.Mutexes.FetchMutex.Unlock() gui.logAction("Fetch") - - err = gui.GitCommand.Fetch(commands.FetchOptions{PromptUserForCredential: gui.promptUserForCredential}) + err = gui.GitCommand.Sync.Fetch(commands.FetchOptions{}) if err != nil && strings.Contains(err.Error(), "exit status 128") { _ = gui.createErrorPanel(gui.Tr.PassUnameWrong) @@ -231,7 +230,7 @@ func (gui *Gui) backgroundFetch() (err error) { gui.Mutexes.FetchMutex.Lock() defer gui.Mutexes.FetchMutex.Unlock() - err = gui.GitCommand.Fetch(commands.FetchOptions{}) + err = gui.GitCommand.Sync.Fetch(commands.FetchOptions{Background: true}) _ = gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{BRANCHES, COMMITS, REMOTES, TAGS}, mode: ASYNC}) diff --git a/pkg/gui/gpg.go b/pkg/gui/gpg.go index 87d0f2971..f18da9803 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 { gui.logCommand(cmdObj.ToString(), true) - useSubprocess := gui.GitCommand.UsingGpg() + useSubprocess := gui.GitCommand.Config.UsingGpg() if useSubprocess { success, err := gui.runSubprocessWithSuspense(gui.OSCommand.Cmd.NewShell(cmdObj.ToString())) if success && onSuccess != nil { diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index 18c6068f7..e6aaa6186 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -12,6 +12,7 @@ import ( "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/commands" + "github.com/jesseduffield/lazygit/pkg/commands/git_config" "github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/commands/oscommands" "github.com/jesseduffield/lazygit/pkg/common" @@ -433,12 +434,16 @@ func (gui *Gui) resetState(filterPath string, reuseState bool) { // for now the split view will always be on // NewGui builds a new gui handler -func NewGui(cmn *common.Common, gitCommand *commands.GitCommand, oSCommand *oscommands.OSCommand, config config.AppConfigurer, updater *updates.Updater, filterPath string, showRecentRepos bool) (*Gui, error) { - +func NewGui( + cmn *common.Common, + config config.AppConfigurer, + gitConfig git_config.IGitConfig, + updater *updates.Updater, + filterPath string, + showRecentRepos bool, +) (*Gui, error) { gui := &Gui{ Common: cmn, - GitCommand: gitCommand, - OSCommand: oSCommand, Config: config, Updater: updater, statusManager: &statusManager{}, @@ -455,11 +460,30 @@ func NewGui(cmn *common.Common, gitCommand *commands.GitCommand, oSCommand *osco ShowExtrasWindow: cmn.UserConfig.Gui.ShowCommandLog && !config.GetAppState().HideCommandLog, } + guiIO := oscommands.NewGuiIO( + cmn.Log, + gui.logCommand, + gui.getCmdWriter, + gui.promptUserForCredential, + ) + + osCommand := oscommands.NewOSCommand(cmn, oscommands.GetPlatform(), guiIO) + + gui.OSCommand = osCommand + var err error + gui.GitCommand, err = commands.NewGitCommand( + cmn, + osCommand, + gitConfig, + ) + if err != nil { + return nil, err + } + gui.resetState(filterPath, false) gui.watchFilesForChanges() - oSCommand.SetLogCommandFn(gui.logCommand) gui.PopupHandler = &RealPopupHandler{gui: gui} authors.SetCustomAuthors(gui.UserConfig.Gui.AuthorColors) diff --git a/pkg/gui/keybindings.go b/pkg/gui/keybindings.go index 7e39edffc..896349dae 100644 --- a/pkg/gui/keybindings.go +++ b/pkg/gui/keybindings.go @@ -743,8 +743,8 @@ func (gui *Gui) GetInitialKeybindings() []*Binding { ViewName: "commits", Contexts: []string{string(BRANCH_COMMITS_CONTEXT_KEY)}, Key: gui.getKey(config.Commits.RenameCommit), - Handler: gui.handleRenameCommit, - Description: gui.Tr.LcRenameCommit, + Handler: gui.handleRewordCommit, + Description: gui.Tr.LcRewordCommit, }, { ViewName: "commits", diff --git a/pkg/gui/layout.go b/pkg/gui/layout.go index ffcace092..106c8f2a0 100644 --- a/pkg/gui/layout.go +++ b/pkg/gui/layout.go @@ -124,8 +124,6 @@ func (gui *Gui) createAllViews() error { return err } - gui.GitCommand.GetCmdWriter = gui.getCmdWriter - return nil } diff --git a/pkg/gui/line_by_line_panel.go b/pkg/gui/line_by_line_panel.go index 721af3261..eeb24a6ea 100644 --- a/pkg/gui/line_by_line_panel.go +++ b/pkg/gui/line_by_line_panel.go @@ -144,7 +144,7 @@ func (gui *Gui) refreshMainViewForLineByLine(state *LblPanelState) error { if gui.currentContext().GetKey() == gui.State.Contexts.PatchBuilding.GetKey() { filename := gui.getSelectedCommitFileName() var err error - includedLineIndices, err = gui.GitCommand.PatchManager.GetFileIncLineIndices(filename) + includedLineIndices, err = gui.GitCommand.Patch.PatchManager.GetFileIncLineIndices(filename) if err != nil { return err } diff --git a/pkg/gui/list_context_config.go b/pkg/gui/list_context_config.go index cb6a55d53..811d976aa 100644 --- a/pkg/gui/list_context_config.go +++ b/pkg/gui/list_context_config.go @@ -312,7 +312,7 @@ func (gui *Gui) commitFilesListContext() IListContext { return [][]string{{style.FgRed.Sprint("(none)")}} } - lines := gui.State.CommitFileManager.Render(gui.State.Modes.Diffing.Ref, gui.GitCommand.PatchManager) + lines := gui.State.CommitFileManager.Render(gui.State.Modes.Diffing.Ref, gui.GitCommand.Patch.PatchManager) mappedLines := make([][]string, len(lines)) for i, line := range lines { mappedLines[i] = []string{line} diff --git a/pkg/gui/merge_panel.go b/pkg/gui/merge_panel.go index 65678797b..db5cabf28 100644 --- a/pkg/gui/merge_panel.go +++ b/pkg/gui/merge_panel.go @@ -195,7 +195,7 @@ func (gui *Gui) catSelectedFile() (string, error) { return "", errors.New(gui.Tr.NotAFile) } - cat, err := gui.GitCommand.CatFile(item.Name) + cat, err := gui.GitCommand.File.Cat(item.Name) if err != nil { gui.Log.Error(err) return "", err @@ -263,7 +263,7 @@ func (gui *Gui) handleCompleteMerge() error { } // if we got conflicts after unstashing, we don't want to call any git // commands to continue rebasing/merging here - if gui.GitCommand.WorkingTreeState() == enums.REBASE_MODE_NONE { + if gui.GitCommand.Status.WorkingTreeState() == enums.REBASE_MODE_NONE { return gui.handleEscapeMerge() } // if there are no more files with merge conflicts, we should ask whether the user wants to continue diff --git a/pkg/gui/modes.go b/pkg/gui/modes.go index c8e2749e3..b4239ece7 100644 --- a/pkg/gui/modes.go +++ b/pkg/gui/modes.go @@ -26,7 +26,7 @@ func (gui *Gui) modeStatuses() []modeStatus { reset: gui.exitDiffMode, }, { - isActive: gui.GitCommand.PatchManager.Active, + isActive: gui.GitCommand.Patch.PatchManager.Active, description: func() string { return style.FgYellow.SetBold().Sprintf( "%s %s", @@ -61,10 +61,10 @@ func (gui *Gui) modeStatuses() []modeStatus { }, { isActive: func() bool { - return gui.GitCommand.WorkingTreeState() != enums.REBASE_MODE_NONE + return gui.GitCommand.Status.WorkingTreeState() != enums.REBASE_MODE_NONE }, description: func() string { - workingTreeState := gui.GitCommand.WorkingTreeState() + workingTreeState := gui.GitCommand.Status.WorkingTreeState() return style.FgYellow.Sprintf( "%s %s", workingTreeState, diff --git a/pkg/gui/patch_building_panel.go b/pkg/gui/patch_building_panel.go index 5c1d4de5f..ff3d8a942 100644 --- a/pkg/gui/patch_building_panel.go +++ b/pkg/gui/patch_building_panel.go @@ -18,7 +18,7 @@ func (gui *Gui) getFromAndReverseArgsForDiff(to string) (string, bool) { } func (gui *Gui) refreshPatchBuildingPanel(selectedLineIdx int, state *LblPanelState) error { - if !gui.GitCommand.PatchManager.Active() { + if !gui.GitCommand.Patch.PatchManager.Active() { return gui.handleEscapePatchBuildingPanel() } @@ -33,12 +33,12 @@ func (gui *Gui) refreshPatchBuildingPanel(selectedLineIdx int, state *LblPanelSt to := gui.State.CommitFileManager.GetParent() from, reverse := gui.getFromAndReverseArgsForDiff(to) - diff, err := gui.GitCommand.ShowFileDiff(from, to, reverse, node.GetPath(), true) + diff, err := gui.GitCommand.WorkingTree.ShowFileDiff(from, to, reverse, node.GetPath(), true) if err != nil { return err } - secondaryDiff := gui.GitCommand.PatchManager.RenderPatchForFile(node.GetPath(), true, false, true) + secondaryDiff := gui.GitCommand.Patch.PatchManager.RenderPatchForFile(node.GetPath(), true, false, true) if err != nil { return err } @@ -64,15 +64,15 @@ func (gui *Gui) handleRefreshPatchBuildingPanel(selectedLineIdx int) error { func (gui *Gui) handleToggleSelectionForPatch() error { err := gui.withLBLActiveCheck(func(state *LblPanelState) error { - toggleFunc := gui.GitCommand.PatchManager.AddFileLineRange + toggleFunc := gui.GitCommand.Patch.PatchManager.AddFileLineRange filename := gui.getSelectedCommitFileName() - includedLineIndices, err := gui.GitCommand.PatchManager.GetFileIncLineIndices(filename) + includedLineIndices, err := gui.GitCommand.Patch.PatchManager.GetFileIncLineIndices(filename) if err != nil { return err } currentLineIsStaged := utils.IncludesInt(includedLineIndices, state.GetSelectedLineIdx()) if currentLineIsStaged { - toggleFunc = gui.GitCommand.PatchManager.RemoveFileLineRange + toggleFunc = gui.GitCommand.Patch.PatchManager.RemoveFileLineRange } // add range of lines to those set for the file @@ -105,8 +105,8 @@ func (gui *Gui) handleToggleSelectionForPatch() error { func (gui *Gui) handleEscapePatchBuildingPanel() error { gui.escapeLineByLinePanel() - if gui.GitCommand.PatchManager.IsEmpty() { - gui.GitCommand.PatchManager.Reset() + if gui.GitCommand.Patch.PatchManager.IsEmpty() { + gui.GitCommand.Patch.PatchManager.Reset() } if gui.currentContext().GetKey() == gui.State.Contexts.PatchBuilding.GetKey() { @@ -118,8 +118,8 @@ func (gui *Gui) handleEscapePatchBuildingPanel() error { } func (gui *Gui) secondaryPatchPanelUpdateOpts() *viewUpdateOpts { - if gui.GitCommand.PatchManager.Active() { - patch := gui.GitCommand.PatchManager.RenderAggregatedPatchColored(false) + if gui.GitCommand.Patch.PatchManager.Active() { + patch := gui.GitCommand.Patch.PatchManager.RenderAggregatedPatchColored(false) return &viewUpdateOpts{ title: "Custom Patch", diff --git a/pkg/gui/patch_options_panel.go b/pkg/gui/patch_options_panel.go index 841683c06..eb31ad13e 100644 --- a/pkg/gui/patch_options_panel.go +++ b/pkg/gui/patch_options_panel.go @@ -7,7 +7,7 @@ import ( ) func (gui *Gui) handleCreatePatchOptionsMenu() error { - if !gui.GitCommand.PatchManager.Active() { + if !gui.GitCommand.Patch.PatchManager.Active() { return gui.createErrorPanel(gui.Tr.NoPatchError) } @@ -26,10 +26,10 @@ func (gui *Gui) handleCreatePatchOptionsMenu() error { }, } - if gui.GitCommand.PatchManager.CanRebase && gui.GitCommand.WorkingTreeState() == enums.REBASE_MODE_NONE { + if gui.GitCommand.Patch.PatchManager.CanRebase && gui.GitCommand.Status.WorkingTreeState() == enums.REBASE_MODE_NONE { menuItems = append(menuItems, []*menuItem{ { - displayString: fmt.Sprintf("remove patch from original commit (%s)", gui.GitCommand.PatchManager.To), + displayString: fmt.Sprintf("remove patch from original commit (%s)", gui.GitCommand.Patch.PatchManager.To), onPress: gui.handleDeletePatchFromCommit, }, { @@ -44,7 +44,7 @@ func (gui *Gui) handleCreatePatchOptionsMenu() error { if gui.currentContext().GetKey() == gui.State.Contexts.BranchCommits.GetKey() { selectedCommit := gui.getSelectedLocalCommit() - if selectedCommit != nil && gui.GitCommand.PatchManager.To != selectedCommit.Sha { + if selectedCommit != nil && gui.GitCommand.Patch.PatchManager.To != selectedCommit.Sha { // adding this option to index 1 menuItems = append( menuItems[:1], @@ -66,7 +66,7 @@ func (gui *Gui) handleCreatePatchOptionsMenu() error { func (gui *Gui) getPatchCommitIndex() int { for index, commit := range gui.State.Commits { - if commit.Sha == gui.GitCommand.PatchManager.To { + if commit.Sha == gui.GitCommand.Patch.PatchManager.To { return index } } @@ -74,7 +74,7 @@ func (gui *Gui) getPatchCommitIndex() int { } func (gui *Gui) validateNormalWorkingTreeState() (bool, error) { - if gui.GitCommand.WorkingTreeState() != enums.REBASE_MODE_NONE { + if gui.GitCommand.Status.WorkingTreeState() != enums.REBASE_MODE_NONE { return false, gui.createErrorPanel(gui.Tr.CantPatchWhileRebasingError) } return true, nil @@ -99,7 +99,7 @@ func (gui *Gui) handleDeletePatchFromCommit() error { return gui.WithWaitingStatus(gui.Tr.RebasingStatus, func() error { commitIndex := gui.getPatchCommitIndex() gui.logAction(gui.Tr.Actions.RemovePatchFromCommit) - err := gui.GitCommand.DeletePatchesFromCommit(gui.State.Commits, commitIndex, gui.GitCommand.PatchManager) + err := gui.GitCommand.Patch.DeletePatchesFromCommit(gui.State.Commits, commitIndex) return gui.handleGenericMergeCommandResult(err) }) } @@ -116,7 +116,7 @@ func (gui *Gui) handleMovePatchToSelectedCommit() error { return gui.WithWaitingStatus(gui.Tr.RebasingStatus, func() error { commitIndex := gui.getPatchCommitIndex() gui.logAction(gui.Tr.Actions.MovePatchToSelectedCommit) - err := gui.GitCommand.MovePatchToSelectedCommit(gui.State.Commits, commitIndex, gui.State.Panels.Commits.SelectedLineIdx, gui.GitCommand.PatchManager) + err := gui.GitCommand.Patch.MovePatchToSelectedCommit(gui.State.Commits, commitIndex, gui.State.Panels.Commits.SelectedLineIdx) return gui.handleGenericMergeCommandResult(err) }) } @@ -134,7 +134,7 @@ func (gui *Gui) handleMovePatchIntoWorkingTree() error { return gui.WithWaitingStatus(gui.Tr.RebasingStatus, func() error { commitIndex := gui.getPatchCommitIndex() gui.logAction(gui.Tr.Actions.MovePatchIntoIndex) - err := gui.GitCommand.MovePatchIntoIndex(gui.State.Commits, commitIndex, gui.GitCommand.PatchManager, stash) + err := gui.GitCommand.Patch.MovePatchIntoIndex(gui.State.Commits, commitIndex, stash) return gui.handleGenericMergeCommandResult(err) }) } @@ -164,7 +164,7 @@ func (gui *Gui) handlePullPatchIntoNewCommit() error { return gui.WithWaitingStatus(gui.Tr.RebasingStatus, func() error { commitIndex := gui.getPatchCommitIndex() gui.logAction(gui.Tr.Actions.MovePatchIntoNewCommit) - err := gui.GitCommand.PullPatchIntoNewCommit(gui.State.Commits, commitIndex, gui.GitCommand.PatchManager) + err := gui.GitCommand.Patch.PullPatchIntoNewCommit(gui.State.Commits, commitIndex) return gui.handleGenericMergeCommandResult(err) }) } @@ -179,14 +179,14 @@ func (gui *Gui) handleApplyPatch(reverse bool) error { action = "Apply patch in reverse" } gui.logAction(action) - if err := gui.GitCommand.PatchManager.ApplyPatches(reverse); err != nil { + if err := gui.GitCommand.Patch.PatchManager.ApplyPatches(reverse); err != nil { return gui.surfaceError(err) } return gui.refreshSidePanels(refreshOptions{mode: ASYNC}) } func (gui *Gui) handleResetPatch() error { - gui.GitCommand.PatchManager.Reset() + gui.GitCommand.Patch.PatchManager.Reset() if gui.currentContextKeyIgnoringPopups() == MAIN_PATCH_BUILDING_CONTEXT_KEY { if err := gui.pushContext(gui.State.Contexts.CommitFiles); err != nil { return err diff --git a/pkg/gui/pty.go b/pkg/gui/pty.go index 26b9156ad..c59867469 100644 --- a/pkg/gui/pty.go +++ b/pkg/gui/pty.go @@ -41,7 +41,7 @@ func (gui *Gui) onResize() error { // command. func (gui *Gui) newPtyTask(view *gocui.View, cmd *exec.Cmd, prefix string) error { width, _ := gui.Views.Main.Size() - pager := gui.GitCommand.GetPager(width) + pager := gui.GitCommand.Config.GetPager(width) if pager == "" { // if we're not using a custom pager we don't need to use a pty diff --git a/pkg/gui/pull_request_menu_panel.go b/pkg/gui/pull_request_menu_panel.go index 3bbda6f0a..e72f781cb 100644 --- a/pkg/gui/pull_request_menu_panel.go +++ b/pkg/gui/pull_request_menu_panel.go @@ -71,7 +71,7 @@ func (gui *Gui) createPullRequest(from string, to string) error { } func (gui *Gui) getHostingServiceMgr() *hosting_service.HostingServiceMgr { - remoteUrl := gui.GitCommand.GetRemoteURL() + remoteUrl := gui.GitCommand.Config.GetRemoteURL() configServices := gui.UserConfig.Services return hosting_service.NewHostingServiceMgr(gui.Log, gui.Tr, remoteUrl, configServices) } diff --git a/pkg/gui/rebase_options_panel.go b/pkg/gui/rebase_options_panel.go index 18a50ae8c..e9476cd5d 100644 --- a/pkg/gui/rebase_options_panel.go +++ b/pkg/gui/rebase_options_panel.go @@ -18,7 +18,7 @@ const ( func (gui *Gui) handleCreateRebaseOptionsMenu() error { options := []string{REBASE_OPTION_CONTINUE, REBASE_OPTION_ABORT} - if gui.GitCommand.WorkingTreeState() == enums.REBASE_MODE_REBASING { + if gui.GitCommand.Status.WorkingTreeState() == enums.REBASE_MODE_REBASING { options = append(options, REBASE_OPTION_SKIP) } @@ -35,7 +35,7 @@ func (gui *Gui) handleCreateRebaseOptionsMenu() error { } var title string - if gui.GitCommand.WorkingTreeState() == enums.REBASE_MODE_MERGING { + if gui.GitCommand.Status.WorkingTreeState() == enums.REBASE_MODE_MERGING { title = gui.Tr.MergeOptionsTitle } else { title = gui.Tr.RebaseOptionsTitle @@ -45,7 +45,7 @@ func (gui *Gui) handleCreateRebaseOptionsMenu() error { } func (gui *Gui) genericMergeCommand(command string) error { - status := gui.GitCommand.WorkingTreeState() + status := gui.GitCommand.Status.WorkingTreeState() if status != enums.REBASE_MODE_MERGING && status != enums.REBASE_MODE_REBASING { return gui.createErrorPanel(gui.Tr.NotMergingOrRebasing) @@ -71,7 +71,7 @@ func (gui *Gui) genericMergeCommand(command string) error { } return nil } - result := gui.GitCommand.GenericMergeOrRebaseAction(commandType, command) + result := gui.GitCommand.Rebase.GenericMergeOrRebaseAction(commandType, command) if err := gui.handleGenericMergeCommandResult(result); err != nil { return err } @@ -142,7 +142,7 @@ func (gui *Gui) abortMergeOrRebaseWithConfirm() error { } func (gui *Gui) workingTreeStateNoun() string { - workingTreeState := gui.GitCommand.WorkingTreeState() + workingTreeState := gui.GitCommand.Status.WorkingTreeState() switch workingTreeState { case enums.REBASE_MODE_NONE: return "" diff --git a/pkg/gui/recent_repos_panel.go b/pkg/gui/recent_repos_panel.go index 5a7c58edb..1c1bb8286 100644 --- a/pkg/gui/recent_repos_panel.go +++ b/pkg/gui/recent_repos_panel.go @@ -99,7 +99,7 @@ func (gui *Gui) dispatchSwitchToRepo(path string, reuse bool) error { // updateRecentRepoList registers the fact that we opened lazygit in this repo, // so that we can open the same repo via the 'recent repos' menu func (gui *Gui) updateRecentRepoList() error { - if gui.GitCommand.IsBareRepo() { + if gui.GitCommand.Status.IsBareRepo() { // we could totally do this but it would require storing both the git-dir and the // worktree in our recent repos list, which is a change that would need to be // backwards compatible diff --git a/pkg/gui/reflog_panel.go b/pkg/gui/reflog_panel.go index 388bab230..3870b7977 100644 --- a/pkg/gui/reflog_panel.go +++ b/pkg/gui/reflog_panel.go @@ -1,7 +1,6 @@ package gui import ( - "github.com/jesseduffield/lazygit/pkg/commands/loaders" "github.com/jesseduffield/lazygit/pkg/commands/models" ) @@ -23,7 +22,7 @@ func (gui *Gui) reflogCommitsRenderToMain() error { if commit == nil { task = NewRenderStringTask("No reflog history") } else { - cmdObj := gui.GitCommand.ShowCmdObj(commit.Sha, gui.State.Modes.Filtering.GetPath()) + cmdObj := gui.GitCommand.Commit.ShowCmdObj(commit.Sha, gui.State.Modes.Filtering.GetPath()) task = NewRunPtyTask(cmdObj.GetCmd()) } @@ -53,8 +52,7 @@ func (gui *Gui) refreshReflogCommits() error { } refresh := func(stateCommits *[]*models.Commit, filterPath string) error { - commits, onlyObtainedNewReflogCommits, err := loaders. - NewReflogCommitLoader(gui.Common, gui.GitCommand.Cmd). + commits, onlyObtainedNewReflogCommits, err := gui.GitCommand.Loaders.ReflogCommits. GetReflogCommits(lastReflogCommit, filterPath) if err != nil { return gui.surfaceError(err) diff --git a/pkg/gui/remote_branches_panel.go b/pkg/gui/remote_branches_panel.go index d9e03b8a0..f60dd7eb1 100644 --- a/pkg/gui/remote_branches_panel.go +++ b/pkg/gui/remote_branches_panel.go @@ -24,7 +24,7 @@ func (gui *Gui) remoteBranchesRenderToMain() error { if remoteBranch == nil { task = NewRenderStringTask("No branches for this remote") } else { - cmdObj := gui.GitCommand.GetBranchGraphCmdObj(remoteBranch.FullName()) + cmdObj := gui.GitCommand.Branch.GetGraphCmdObj(remoteBranch.FullName()) task = NewRunCommandTask(cmdObj.GetCmd()) } @@ -58,7 +58,7 @@ func (gui *Gui) handleDeleteRemoteBranch() error { handleConfirm: func() error { return gui.WithWaitingStatus(gui.Tr.DeletingStatus, func() error { gui.logAction(gui.Tr.Actions.DeleteRemoteBranch) - err := gui.GitCommand.DeleteRemoteBranch(remoteBranch.RemoteName, remoteBranch.Name, gui.promptUserForCredential) + err := gui.GitCommand.Remote.DeleteRemoteBranch(remoteBranch.RemoteName, remoteBranch.Name) gui.handleCredentialsPopup(err) return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{BRANCHES, REMOTES}}) @@ -89,7 +89,7 @@ func (gui *Gui) handleSetBranchUpstream() error { prompt: message, handleConfirm: func() error { gui.logAction(gui.Tr.Actions.SetBranchUpstream) - if err := gui.GitCommand.SetBranchUpstream(selectedBranch.RemoteName, selectedBranch.Name, checkedOutBranch.Name); err != nil { + if err := gui.GitCommand.Branch.SetUpstream(selectedBranch.RemoteName, selectedBranch.Name, checkedOutBranch.Name); err != nil { return gui.surfaceError(err) } diff --git a/pkg/gui/remotes_panel.go b/pkg/gui/remotes_panel.go index c0400ec35..384a34beb 100644 --- a/pkg/gui/remotes_panel.go +++ b/pkg/gui/remotes_panel.go @@ -86,7 +86,7 @@ func (gui *Gui) handleAddRemote() error { title: gui.Tr.LcNewRemoteUrl, handleConfirm: func(remoteUrl string) error { gui.logAction(gui.Tr.Actions.AddRemote) - if err := gui.GitCommand.AddRemote(remoteName, remoteUrl); err != nil { + if err := gui.GitCommand.Remote.AddRemote(remoteName, remoteUrl); err != nil { return err } return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{REMOTES}}) @@ -108,7 +108,7 @@ func (gui *Gui) handleRemoveRemote() error { prompt: gui.Tr.LcRemoveRemotePrompt + " '" + remote.Name + "'?", handleConfirm: func() error { gui.logAction(gui.Tr.Actions.RemoveRemote) - if err := gui.GitCommand.RemoveRemote(remote.Name); err != nil { + if err := gui.GitCommand.Remote.RemoveRemote(remote.Name); err != nil { return gui.surfaceError(err) } @@ -136,7 +136,7 @@ func (gui *Gui) handleEditRemote() error { handleConfirm: func(updatedRemoteName string) error { if updatedRemoteName != remote.Name { gui.logAction(gui.Tr.Actions.UpdateRemote) - if err := gui.GitCommand.RenameRemote(remote.Name, updatedRemoteName); err != nil { + if err := gui.GitCommand.Remote.RenameRemote(remote.Name, updatedRemoteName); err != nil { return gui.surfaceError(err) } } @@ -159,7 +159,7 @@ func (gui *Gui) handleEditRemote() error { initialContent: url, handleConfirm: func(updatedRemoteUrl string) error { gui.logAction(gui.Tr.Actions.UpdateRemote) - if err := gui.GitCommand.UpdateRemoteUrl(updatedRemoteName, updatedRemoteUrl); err != nil { + if err := gui.GitCommand.Remote.UpdateRemoteUrl(updatedRemoteName, updatedRemoteUrl); err != nil { return gui.surfaceError(err) } return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{BRANCHES, REMOTES}}) @@ -179,7 +179,7 @@ func (gui *Gui) handleFetchRemote() error { gui.Mutexes.FetchMutex.Lock() defer gui.Mutexes.FetchMutex.Unlock() - err := gui.GitCommand.FetchRemote(remote.Name, gui.promptUserForCredential) + err := gui.GitCommand.Sync.FetchRemote(remote.Name) gui.handleCredentialsPopup(err) return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{BRANCHES, REMOTES}}) diff --git a/pkg/gui/reset_menu_panel.go b/pkg/gui/reset_menu_panel.go index 2fa04b98b..77e5fef2e 100644 --- a/pkg/gui/reset_menu_panel.go +++ b/pkg/gui/reset_menu_panel.go @@ -7,7 +7,7 @@ import ( ) func (gui *Gui) resetToRef(ref string, strength string, envVars []string) error { - if err := gui.GitCommand.ResetToCommit(ref, strength, envVars); err != nil { + if err := gui.GitCommand.Commit.ResetToCommit(ref, strength, envVars); err != nil { return gui.surfaceError(err) } diff --git a/pkg/gui/staging_panel.go b/pkg/gui/staging_panel.go index ed610ed55..d14f36cd2 100644 --- a/pkg/gui/staging_panel.go +++ b/pkg/gui/staging_panel.go @@ -34,8 +34,8 @@ func (gui *Gui) refreshStagingPanel(forceSecondaryFocused bool, selectedLineIdx } // note for custom diffs, we'll need to send a flag here saying not to use the custom diff - diff := gui.GitCommand.WorktreeFileDiff(file, true, secondaryFocused, false) - secondaryDiff := gui.GitCommand.WorktreeFileDiff(file, true, !secondaryFocused, false) + diff := gui.GitCommand.WorkingTree.WorktreeFileDiff(file, true, secondaryFocused, false) + secondaryDiff := gui.GitCommand.WorkingTree.WorktreeFileDiff(file, true, !secondaryFocused, false) // if we have e.g. a deleted file with nothing else to the diff will have only // 4-5 lines in which case we'll swap panels @@ -144,7 +144,7 @@ func (gui *Gui) applySelection(reverse bool, state *LblPanelState) error { applyFlags = append(applyFlags, "cached") } gui.logAction(gui.Tr.Actions.ApplyPatch) - err := gui.GitCommand.ApplyPatch(patch, applyFlags...) + err := gui.GitCommand.WorkingTree.ApplyPatch(patch, applyFlags...) if err != nil { return gui.surfaceError(err) } diff --git a/pkg/gui/stash_panel.go b/pkg/gui/stash_panel.go index b77dd95f6..97e890a39 100644 --- a/pkg/gui/stash_panel.go +++ b/pkg/gui/stash_panel.go @@ -1,9 +1,7 @@ package gui import ( - "github.com/jesseduffield/lazygit/pkg/commands/loaders" "github.com/jesseduffield/lazygit/pkg/commands/models" - "github.com/jesseduffield/lazygit/pkg/utils" ) // list panel functions @@ -23,7 +21,7 @@ func (gui *Gui) stashRenderToMain() error { if stashEntry == nil { task = NewRenderStringTask(gui.Tr.NoStashEntries) } else { - task = NewRunPtyTask(gui.GitCommand.ShowStashEntryCmdObj(stashEntry.Index).GetCmd()) + task = NewRunPtyTask(gui.GitCommand.Stash.ShowStashEntryCmdObj(stashEntry.Index).GetCmd()) } return gui.refreshMainViews(refreshMainOpts{ @@ -35,8 +33,7 @@ func (gui *Gui) stashRenderToMain() error { } func (gui *Gui) refreshStashEntries() error { - gui.State.StashEntries = loaders. - NewStashLoader(gui.Common, gui.GitCommand.Cmd). + gui.State.StashEntries = gui.GitCommand.Loaders.Stash. GetStashEntries(gui.State.Modes.Filtering.GetPath()) return gui.State.Contexts.Stash.HandleRender() @@ -45,10 +42,19 @@ func (gui *Gui) refreshStashEntries() error { // specific functions func (gui *Gui) handleStashApply() error { + stashEntry := gui.getSelectedStashEntry() + if stashEntry == nil { + return nil + } + skipStashWarning := gui.UserConfig.Gui.SkipStashWarning apply := func() error { - return gui.stashDo("apply") + gui.logAction(gui.Tr.Actions.Stash) + if err := gui.GitCommand.Stash.Apply(stashEntry.Index); err != nil { + return gui.surfaceError(err) + } + return gui.postStashRefresh() } if skipStashWarning { @@ -65,10 +71,19 @@ func (gui *Gui) handleStashApply() error { } func (gui *Gui) handleStashPop() error { + stashEntry := gui.getSelectedStashEntry() + if stashEntry == nil { + return nil + } + skipStashWarning := gui.UserConfig.Gui.SkipStashWarning pop := func() error { - return gui.stashDo("pop") + gui.logAction(gui.Tr.Actions.Stash) + if err := gui.GitCommand.Stash.Pop(stashEntry.Index); err != nil { + return gui.surfaceError(err) + } + return gui.postStashRefresh() } if skipStashWarning { @@ -85,31 +100,25 @@ func (gui *Gui) handleStashPop() error { } func (gui *Gui) handleStashDrop() error { + stashEntry := gui.getSelectedStashEntry() + if stashEntry == nil { + return nil + } + return gui.ask(askOpts{ title: gui.Tr.StashDrop, prompt: gui.Tr.SureDropStashEntry, handleConfirm: func() error { - return gui.stashDo("drop") + gui.logAction(gui.Tr.Actions.Stash) + if err := gui.GitCommand.Stash.Drop(stashEntry.Index); err != nil { + return gui.surfaceError(err) + } + return gui.postStashRefresh() }, }) } -func (gui *Gui) stashDo(method string) error { - stashEntry := gui.getSelectedStashEntry() - if stashEntry == nil { - errorMessage := utils.ResolvePlaceholderString( - gui.Tr.NoStashTo, - map[string]string{ - "method": method, - }, - ) - - return gui.createErrorPanel(errorMessage) - } - gui.logAction(gui.Tr.Actions.Stash) - if err := gui.GitCommand.StashDo(stashEntry.Index, method); err != nil { - return gui.surfaceError(err) - } +func (gui *Gui) postStashRefresh() error { return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{STASH, FILES}}) } diff --git a/pkg/gui/status_panel.go b/pkg/gui/status_panel.go index 9268e3311..6a724f983 100644 --- a/pkg/gui/status_panel.go +++ b/pkg/gui/status_panel.go @@ -28,8 +28,8 @@ func (gui *Gui) refreshStatus() { status += presentation.ColoredBranchStatus(currentBranch) + " " } - if gui.GitCommand.WorkingTreeState() != enums.REBASE_MODE_NONE { - status += style.FgYellow.Sprintf("(%s) ", gui.GitCommand.WorkingTreeState()) + if gui.GitCommand.Status.WorkingTreeState() != enums.REBASE_MODE_NONE { + status += style.FgYellow.Sprintf("(%s) ", gui.GitCommand.Status.WorkingTreeState()) } name := presentation.GetBranchTextStyle(currentBranch.Name).Sprint(currentBranch.Name) @@ -71,7 +71,7 @@ func (gui *Gui) handleStatusClick() error { cx, _ := gui.Views.Status.Cursor() upstreamStatus := presentation.BranchStatus(currentBranch) repoName := utils.GetCurrentRepoName() - workingTreeState := gui.GitCommand.WorkingTreeState() + workingTreeState := gui.GitCommand.Status.WorkingTreeState() switch workingTreeState { case enums.REBASE_MODE_REBASING, enums.REBASE_MODE_MERGING: var formattedState string diff --git a/pkg/gui/sub_commits_panel.go b/pkg/gui/sub_commits_panel.go index 549004133..7a174411a 100644 --- a/pkg/gui/sub_commits_panel.go +++ b/pkg/gui/sub_commits_panel.go @@ -23,7 +23,7 @@ func (gui *Gui) subCommitsRenderToMain() error { if commit == nil { task = NewRenderStringTask("No commits") } else { - cmdObj := gui.GitCommand.ShowCmdObj(commit.Sha, gui.State.Modes.Filtering.GetPath()) + cmdObj := gui.GitCommand.Commit.ShowCmdObj(commit.Sha, gui.State.Modes.Filtering.GetPath()) task = NewRunPtyTask(cmdObj.GetCmd()) } diff --git a/pkg/gui/submodules_panel.go b/pkg/gui/submodules_panel.go index a55c7acd0..919a4c92a 100644 --- a/pkg/gui/submodules_panel.go +++ b/pkg/gui/submodules_panel.go @@ -36,7 +36,7 @@ func (gui *Gui) submodulesRenderToMain() error { if file == nil { task = NewRenderStringTask(prefix) } else { - cmdObj := gui.GitCommand.WorktreeFileDiffCmdObj(file, false, !file.HasUnstagedChanges && file.HasStagedChanges, gui.State.IgnoreWhitespaceInDiffView) + cmdObj := gui.GitCommand.WorkingTree.WorktreeFileDiffCmdObj(file, false, !file.HasUnstagedChanges && file.HasStagedChanges, gui.State.IgnoreWhitespaceInDiffView) task = NewRunCommandTaskWithPrefix(cmdObj.GetCmd(), prefix) } } @@ -50,7 +50,7 @@ func (gui *Gui) submodulesRenderToMain() error { } func (gui *Gui) refreshStateSubmoduleConfigs() error { - configs, err := gui.GitCommand.GetSubmoduleConfigs() + configs, err := gui.GitCommand.Submodule.GetConfigs() if err != nil { return err } @@ -80,7 +80,7 @@ func (gui *Gui) removeSubmodule(submodule *models.SubmoduleConfig) error { prompt: fmt.Sprintf(gui.Tr.RemoveSubmodulePrompt, submodule.Name), handleConfirm: func() error { gui.logAction(gui.Tr.Actions.RemoveSubmodule) - if err := gui.GitCommand.SubmoduleDelete(submodule); err != nil { + if err := gui.GitCommand.Submodule.Delete(submodule); err != nil { return gui.surfaceError(err) } @@ -110,15 +110,15 @@ func (gui *Gui) resetSubmodule(submodule *models.SubmoduleConfig) error { file := gui.fileForSubmodule(submodule) if file != nil { - if err := gui.GitCommand.UnStageFile(file.Names(), file.Tracked); err != nil { + if err := gui.GitCommand.WorkingTree.UnStageFile(file.Names(), file.Tracked); err != nil { return gui.surfaceError(err) } } - if err := gui.GitCommand.SubmoduleStash(submodule); err != nil { + if err := gui.GitCommand.Submodule.Stash(submodule); err != nil { return gui.surfaceError(err) } - if err := gui.GitCommand.SubmoduleReset(submodule); err != nil { + if err := gui.GitCommand.Submodule.Reset(submodule); err != nil { return gui.surfaceError(err) } @@ -142,7 +142,7 @@ func (gui *Gui) handleAddSubmodule() error { handleConfirm: func(submodulePath string) error { return gui.WithWaitingStatus(gui.Tr.LcAddingSubmoduleStatus, func() error { gui.logAction(gui.Tr.Actions.AddSubmodule) - err := gui.GitCommand.SubmoduleAdd(submoduleName, submodulePath, submoduleUrl) + err := gui.GitCommand.Submodule.Add(submoduleName, submodulePath, submoduleUrl) gui.handleCredentialsPopup(err) return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{SUBMODULES}}) @@ -163,7 +163,7 @@ func (gui *Gui) handleEditSubmoduleUrl(submodule *models.SubmoduleConfig) error handleConfirm: func(newUrl string) error { return gui.WithWaitingStatus(gui.Tr.LcUpdatingSubmoduleUrlStatus, func() error { gui.logAction(gui.Tr.Actions.UpdateSubmoduleUrl) - err := gui.GitCommand.SubmoduleUpdateUrl(submodule.Name, submodule.Path, newUrl) + err := gui.GitCommand.Submodule.UpdateUrl(submodule.Name, submodule.Path, newUrl) gui.handleCredentialsPopup(err) return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{SUBMODULES}}) @@ -175,7 +175,7 @@ func (gui *Gui) handleEditSubmoduleUrl(submodule *models.SubmoduleConfig) error func (gui *Gui) handleSubmoduleInit(submodule *models.SubmoduleConfig) error { return gui.WithWaitingStatus(gui.Tr.LcInitializingSubmoduleStatus, func() error { gui.logAction(gui.Tr.Actions.InitialiseSubmodule) - err := gui.GitCommand.SubmoduleInit(submodule.Path) + err := gui.GitCommand.Submodule.Init(submodule.Path) gui.handleCredentialsPopup(err) return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{SUBMODULES}}) @@ -215,11 +215,11 @@ func (gui *Gui) handleResetRemoveSubmodule(submodule *models.SubmoduleConfig) er func (gui *Gui) handleBulkSubmoduleActionsMenu() error { menuItems := []*menuItem{ { - displayStrings: []string{gui.Tr.LcBulkInitSubmodules, style.FgGreen.Sprint(gui.GitCommand.SubmoduleBulkInitCmdObj().ToString())}, + displayStrings: []string{gui.Tr.LcBulkInitSubmodules, style.FgGreen.Sprint(gui.GitCommand.Submodule.BulkInitCmdObj().ToString())}, onPress: func() error { return gui.WithWaitingStatus(gui.Tr.LcRunningCommand, func() error { gui.logAction(gui.Tr.Actions.BulkInitialiseSubmodules) - err := gui.GitCommand.SubmoduleBulkInitCmdObj().Run() + err := gui.GitCommand.Submodule.BulkInitCmdObj().Run() if err != nil { return gui.surfaceError(err) } @@ -229,11 +229,11 @@ func (gui *Gui) handleBulkSubmoduleActionsMenu() error { }, }, { - displayStrings: []string{gui.Tr.LcBulkUpdateSubmodules, style.FgYellow.Sprint(gui.GitCommand.SubmoduleBulkUpdateCmdObj().ToString())}, + displayStrings: []string{gui.Tr.LcBulkUpdateSubmodules, style.FgYellow.Sprint(gui.GitCommand.Submodule.BulkUpdateCmdObj().ToString())}, onPress: func() error { return gui.WithWaitingStatus(gui.Tr.LcRunningCommand, func() error { gui.logAction(gui.Tr.Actions.BulkUpdateSubmodules) - if err := gui.GitCommand.SubmoduleBulkUpdateCmdObj().Run(); err != nil { + if err := gui.GitCommand.Submodule.BulkUpdateCmdObj().Run(); err != nil { return gui.surfaceError(err) } @@ -242,11 +242,11 @@ func (gui *Gui) handleBulkSubmoduleActionsMenu() error { }, }, { - displayStrings: []string{gui.Tr.LcSubmoduleStashAndReset, style.FgRed.Sprintf("git stash in each submodule && %s", gui.GitCommand.SubmoduleForceBulkUpdateCmdObj().ToString())}, + displayStrings: []string{gui.Tr.LcSubmoduleStashAndReset, style.FgRed.Sprintf("git stash in each submodule && %s", gui.GitCommand.Submodule.ForceBulkUpdateCmdObj().ToString())}, onPress: func() error { return gui.WithWaitingStatus(gui.Tr.LcRunningCommand, func() error { gui.logAction(gui.Tr.Actions.BulkStashAndResetSubmodules) - if err := gui.GitCommand.ResetSubmodules(gui.State.Submodules); err != nil { + if err := gui.GitCommand.Submodule.ResetSubmodules(gui.State.Submodules); err != nil { return gui.surfaceError(err) } @@ -255,11 +255,11 @@ func (gui *Gui) handleBulkSubmoduleActionsMenu() error { }, }, { - displayStrings: []string{gui.Tr.LcBulkDeinitSubmodules, style.FgRed.Sprint(gui.GitCommand.SubmoduleBulkDeinitCmdObj().ToString())}, + displayStrings: []string{gui.Tr.LcBulkDeinitSubmodules, style.FgRed.Sprint(gui.GitCommand.Submodule.BulkDeinitCmdObj().ToString())}, onPress: func() error { return gui.WithWaitingStatus(gui.Tr.LcRunningCommand, func() error { gui.logAction(gui.Tr.Actions.BulkDeinitialiseSubmodules) - if err := gui.GitCommand.SubmoduleBulkDeinitCmdObj().Run(); err != nil { + if err := gui.GitCommand.Submodule.BulkDeinitCmdObj().Run(); err != nil { return gui.surfaceError(err) } @@ -275,7 +275,7 @@ func (gui *Gui) handleBulkSubmoduleActionsMenu() error { func (gui *Gui) handleUpdateSubmodule(submodule *models.SubmoduleConfig) error { return gui.WithWaitingStatus(gui.Tr.LcUpdatingSubmoduleStatus, func() error { gui.logAction(gui.Tr.Actions.UpdateSubmodule) - err := gui.GitCommand.SubmoduleUpdate(submodule.Path) + err := gui.GitCommand.Submodule.Update(submodule.Path) gui.handleCredentialsPopup(err) return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{SUBMODULES}}) diff --git a/pkg/gui/tags_panel.go b/pkg/gui/tags_panel.go index 1c5ece0c7..97488eb3e 100644 --- a/pkg/gui/tags_panel.go +++ b/pkg/gui/tags_panel.go @@ -25,7 +25,7 @@ func (gui *Gui) tagsRenderToMain() error { if tag == nil { task = NewRenderStringTask("No tags") } else { - cmdObj := gui.GitCommand.GetBranchGraphCmdObj(tag.Name) + cmdObj := gui.GitCommand.Branch.GetGraphCmdObj(tag.Name) task = NewRunCommandTask(cmdObj.GetCmd()) } @@ -83,7 +83,7 @@ func (gui *Gui) handleDeleteTag(tag *models.Tag) error { prompt: prompt, handleConfirm: func() error { gui.logAction(gui.Tr.Actions.DeleteTag) - if err := gui.GitCommand.DeleteTag(tag.Name); err != nil { + if err := gui.GitCommand.Tag.Delete(tag.Name); err != nil { return gui.surfaceError(err) } return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []RefreshableView{COMMITS, TAGS}}) @@ -106,7 +106,7 @@ func (gui *Gui) handlePushTag(tag *models.Tag) error { handleConfirm: func(response string) error { return gui.WithWaitingStatus(gui.Tr.PushingTagStatus, func() error { gui.logAction(gui.Tr.Actions.PushTag) - err := gui.GitCommand.PushTag(response, tag.Name, gui.promptUserForCredential) + err := gui.GitCommand.Tag.Push(response, tag.Name) gui.handleCredentialsPopup(err) return nil diff --git a/pkg/gui/undoing.go b/pkg/gui/undoing.go index 3e75924cd..b738c7d05 100644 --- a/pkg/gui/undoing.go +++ b/pkg/gui/undoing.go @@ -88,7 +88,7 @@ func (gui *Gui) reflogUndo() error { undoEnvVars := []string{"GIT_REFLOG_ACTION=[lazygit undo]"} undoingStatus := gui.Tr.UndoingStatus - if gui.GitCommand.WorkingTreeState() == enums.REBASE_MODE_REBASING { + if gui.GitCommand.Status.WorkingTreeState() == enums.REBASE_MODE_REBASING { return gui.createErrorPanel(gui.Tr.LcCantUndoWhileRebasing) } @@ -121,7 +121,7 @@ func (gui *Gui) reflogRedo() error { redoEnvVars := []string{"GIT_REFLOG_ACTION=[lazygit redo]"} redoingStatus := gui.Tr.RedoingStatus - if gui.GitCommand.WorkingTreeState() == enums.REBASE_MODE_REBASING { + if gui.GitCommand.Status.WorkingTreeState() == enums.REBASE_MODE_REBASING { return gui.createErrorPanel(gui.Tr.LcCantRedoWhileRebasing) } @@ -176,14 +176,14 @@ func (gui *Gui) handleHardResetWithAutoStash(commitSha string, options handleHar prompt: gui.Tr.AutoStashPrompt, handleConfirm: func() error { return gui.WithWaitingStatus(options.WaitingStatus, func() error { - if err := gui.GitCommand.StashSave(gui.Tr.StashPrefix + commitSha); err != nil { + if err := gui.GitCommand.Stash.Save(gui.Tr.StashPrefix + commitSha); err != nil { return gui.surfaceError(err) } if err := reset(); err != nil { return err } - err := gui.GitCommand.StashDo(0, "pop") + err := gui.GitCommand.Stash.Pop(0) if err := gui.refreshSidePanels(refreshOptions{}); err != nil { return err } diff --git a/pkg/gui/workspace_reset_options_panel.go b/pkg/gui/workspace_reset_options_panel.go index a4eb0733f..e2f5f2e75 100644 --- a/pkg/gui/workspace_reset_options_panel.go +++ b/pkg/gui/workspace_reset_options_panel.go @@ -22,7 +22,7 @@ func (gui *Gui) handleCreateResetMenu() error { }, onPress: func() error { gui.logAction(gui.Tr.Actions.NukeWorkingTree) - if err := gui.GitCommand.ResetAndClean(); err != nil { + if err := gui.GitCommand.WorkingTree.ResetAndClean(); err != nil { return gui.surfaceError(err) } @@ -36,7 +36,7 @@ func (gui *Gui) handleCreateResetMenu() error { }, onPress: func() error { gui.logAction(gui.Tr.Actions.DiscardUnstagedFileChanges) - if err := gui.GitCommand.DiscardAnyUnstagedFileChanges(); err != nil { + if err := gui.GitCommand.WorkingTree.DiscardAnyUnstagedFileChanges(); err != nil { return gui.surfaceError(err) } @@ -50,7 +50,7 @@ func (gui *Gui) handleCreateResetMenu() error { }, onPress: func() error { gui.logAction(gui.Tr.Actions.RemoveUntrackedFiles) - if err := gui.GitCommand.RemoveUntrackedFiles(); err != nil { + if err := gui.GitCommand.WorkingTree.RemoveUntrackedFiles(); err != nil { return gui.surfaceError(err) } @@ -64,7 +64,7 @@ func (gui *Gui) handleCreateResetMenu() error { }, onPress: func() error { gui.logAction(gui.Tr.Actions.SoftReset) - if err := gui.GitCommand.ResetSoft("HEAD"); err != nil { + if err := gui.GitCommand.WorkingTree.ResetSoft("HEAD"); err != nil { return gui.surfaceError(err) } @@ -78,7 +78,7 @@ func (gui *Gui) handleCreateResetMenu() error { }, onPress: func() error { gui.logAction(gui.Tr.Actions.MixedReset) - if err := gui.GitCommand.ResetMixed("HEAD"); err != nil { + if err := gui.GitCommand.WorkingTree.ResetMixed("HEAD"); err != nil { return gui.surfaceError(err) } @@ -92,7 +92,7 @@ func (gui *Gui) handleCreateResetMenu() error { }, onPress: func() error { gui.logAction(gui.Tr.Actions.HardReset) - if err := gui.GitCommand.ResetHard("HEAD"); err != nil { + if err := gui.GitCommand.WorkingTree.ResetHard("HEAD"); err != nil { return gui.surfaceError(err) } diff --git a/pkg/i18n/chinese.go b/pkg/i18n/chinese.go index edf1f500b..117f1f6c8 100644 --- a/pkg/i18n/chinese.go +++ b/pkg/i18n/chinese.go @@ -109,8 +109,8 @@ func chineseTranslationSet() TranslationSet { Squash: "压缩", LcPickCommit: "选择提交(变基过程中)", LcRevertCommit: "还原提交", - OnlyRenameTopCommit: "只能从 lazygit 内部重写最高的提交。请使用 shift-R", - LcRenameCommit: "改写提交", + OnlyRewordTopCommit: "只能从 lazygit 内部重写最高的提交。请使用 shift-R", + LcRewordCommit: "改写提交", LcDeleteCommit: "删除提交", LcMoveDownCommit: "下移提交", LcMoveUpCommit: "上移提交", @@ -135,7 +135,6 @@ func chineseTranslationSet() TranslationSet { SurePopStashEntry: "您确定要应用并删除此贮藏条目吗?", StashApply: "应用贮藏", SureApplyStashEntry: "您确定要应用此贮藏条目?", - NoStashTo: "没有贮藏条目可以 {{.method}}", NoTrackedStagedFilesStash: "没有可以贮藏的已跟踪/暂存文件", StashChanges: "贮藏更改", MergeAborted: "合并中止", diff --git a/pkg/i18n/dutch.go b/pkg/i18n/dutch.go index 409ffeda9..f2f270dff 100644 --- a/pkg/i18n/dutch.go +++ b/pkg/i18n/dutch.go @@ -79,8 +79,8 @@ func dutchTranslationSet() TranslationSet { Squash: "Squash", LcPickCommit: "kies commit (wanneer midden in rebase)", LcRevertCommit: "commit ongedaan maken", - OnlyRenameTopCommit: "Je kan alleen de bovenste commit hernoemen", - LcRenameCommit: "hernoem commit", + OnlyRewordTopCommit: "Je kan alleen de bovenste commit hernoemen", + LcRewordCommit: "hernoem commit", LcDeleteCommit: "verwijder commit", LcMoveDownCommit: "verplaats commit 1 naar beneden", LcMoveUpCommit: "verplaats commit 1 naar boven", @@ -106,7 +106,6 @@ func dutchTranslationSet() TranslationSet { SurePopStashEntry: "Weet je zeker dat je deze stash entry wil poppen?", StashApply: "Stash toepassen", SureApplyStashEntry: "Weet je zeker dat je deze stash entry wil toepassen?", - NoStashTo: "Geen stash voor {{.method}}", NoTrackedStagedFilesStash: "Je hebt geen tracked/staged bestanden om te laten stashen", StashChanges: "Stash veranderingen", NoChangedFiles: "Geen veranderde bestanden", diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index 7ab71465f..7cf9d17b8 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -93,8 +93,8 @@ type TranslationSet struct { Squash string LcPickCommit string LcRevertCommit string - OnlyRenameTopCommit string - LcRenameCommit string + OnlyRewordTopCommit string + LcRewordCommit string LcDeleteCommit string LcMoveDownCommit string LcMoveUpCommit string @@ -120,7 +120,6 @@ type TranslationSet struct { SurePopStashEntry string StashApply string SureApplyStashEntry string - NoStashTo string NoTrackedStagedFilesStash string StashChanges string MergeAborted string @@ -646,8 +645,8 @@ func EnglishTranslationSet() TranslationSet { Squash: "Squash", LcPickCommit: "pick commit (when mid-rebase)", LcRevertCommit: "revert commit", - OnlyRenameTopCommit: "Can only reword topmost commit from within lazygit. Use shift+R instead", - LcRenameCommit: "reword commit", + OnlyRewordTopCommit: "Can only reword topmost commit from within lazygit. Use shift+R instead", + LcRewordCommit: "reword commit", LcDeleteCommit: "delete commit", LcMoveDownCommit: "move commit down one", LcMoveUpCommit: "move commit up one", @@ -672,7 +671,6 @@ func EnglishTranslationSet() TranslationSet { SurePopStashEntry: "Are you sure you want to pop this stash entry?", StashApply: "Stash apply", SureApplyStashEntry: "Are you sure you want to apply this stash entry?", - NoStashTo: "No stash to {{.method}}", NoTrackedStagedFilesStash: "You have no tracked/staged files to stash", StashChanges: "Stash changes", MergeAborted: "Merge aborted", diff --git a/pkg/i18n/polish.go b/pkg/i18n/polish.go index 7a63f5d60..88970215f 100644 --- a/pkg/i18n/polish.go +++ b/pkg/i18n/polish.go @@ -69,8 +69,8 @@ func polishTranslationSet() TranslationSet { YouNoCommitsToSquash: "Nie masz commitów do spłaszczenia", Fixup: "Napraw", SureFixupThisCommit: "Jesteś pewny, ze chcesz naprawić ten commit? Commit poniżej zostanie spłaszczony w górę wraz z tym", - OnlyRenameTopCommit: "Można zmienić nazwę tylko ostatniemu commitowi", - LcRenameCommit: "zmień nazwę commita", + OnlyRewordTopCommit: "Można zmienić nazwę tylko ostatniemu commitowi", + LcRewordCommit: "zmień nazwę commita", LcRenameCommitEditor: "zmień nazwę commita w edytorze", Error: "Błąd", LcSelectHunk: "wybierz kawałek", @@ -84,7 +84,6 @@ func polishTranslationSet() TranslationSet { NoStashEntries: "Brak pozycji w schowku", StashDrop: "Porzuć schowek", SureDropStashEntry: "Jesteś pewny, że chcesz porzucić tę pozycję w schowku?", - NoStashTo: "Brak schowka dla {{.method}}", NoTrackedStagedFilesStash: "Nie masz śledzonych/zatwierdzonych plików do przechowania", StashChanges: "Przechowaj zmiany", MergeAborted: "Scalanie anulowane", diff --git a/pkg/utils/dummies.go b/pkg/utils/dummies.go index a9dfcd88a..9c65a0876 100644 --- a/pkg/utils/dummies.go +++ b/pkg/utils/dummies.go @@ -3,6 +3,7 @@ package utils import ( "io/ioutil" + "github.com/jesseduffield/lazygit/pkg/commands/git_config" "github.com/jesseduffield/lazygit/pkg/common" "github.com/jesseduffield/lazygit/pkg/config" "github.com/jesseduffield/lazygit/pkg/i18n" @@ -24,3 +25,7 @@ func NewDummyCommon() *common.Common { UserConfig: config.GetDefaultConfig(), } } + +func NewDummyGitConfig() git_config.IGitConfig { + return git_config.NewFakeGitConfig(map[string]string{}) +}