1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2025-05-15 22:26:40 +02:00

start breaking up git struct

This commit is contained in:
Jesse Duffield 2022-01-02 10:34:33 +11:00
parent 4a1d23dc27
commit f503ff1ecb
76 changed files with 2234 additions and 1758 deletions

View File

@ -32,7 +32,6 @@ type App struct {
closers []io.Closer closers []io.Closer
Config config.AppConfigurer Config config.AppConfigurer
OSCommand *oscommands.OSCommand OSCommand *oscommands.OSCommand
GitCommand *commands.GitCommand
Gui *gui.Gui Gui *gui.Gui
Updater *updates.Updater // may only need this on the Gui Updater *updates.Updater // may only need this on the Gui
ClientContext string ClientContext string
@ -122,7 +121,7 @@ func NewApp(config config.AppConfigurer, filterPath string) (*App, error) {
return app, nil 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) app.Updater, err = updates.NewUpdater(app.Common, config, app.OSCommand)
if err != nil { if err != nil {
@ -134,16 +133,9 @@ func NewApp(config config.AppConfigurer, filterPath string) (*App, error) {
return app, err return app, err
} }
app.GitCommand, err = commands.NewGitCommand( gitConfig := git_config.NewStdCachedGitConfig(app.Log)
app.Common,
app.OSCommand,
git_config.NewStdCachedGitConfig(app.Log),
)
if err != nil {
return app, err
}
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 { if err != nil {
return app, err return app, err
} }

View File

@ -6,24 +6,47 @@ import (
"strings" "strings"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands" "github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/common"
"github.com/jesseduffield/lazygit/pkg/utils" "github.com/jesseduffield/lazygit/pkg/utils"
) )
// NewBranch create new branch // this takes something like:
func (c *GitCommand) NewBranch(name string, base string) error { // * (HEAD detached at 264fc6f5)
return c.Cmd.New(fmt.Sprintf("git checkout -b %s %s", c.OSCommand.Quote(name), c.OSCommand.Quote(base))).Run() // 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. // CurrentBranchName get the current branch name and displayname.
// the first returned string is the name and the second is the 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)' // e.g. name is 123asdf and displayname is '(HEAD detached at 123asdf)'
func (c *GitCommand) CurrentBranchName() (string, string, error) { func (self *BranchCommands) CurrentBranchName() (string, string, error) {
branchName, err := c.Cmd.New("git symbolic-ref --short HEAD").DontLog().RunWithOutput() branchName, err := self.cmd.New("git symbolic-ref --short HEAD").DontLog().RunWithOutput()
if err == nil && branchName != "HEAD\n" { if err == nil && branchName != "HEAD\n" {
trimmedBranchName := strings.TrimSpace(branchName) trimmedBranchName := strings.TrimSpace(branchName)
return trimmedBranchName, trimmedBranchName, nil 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 { if err != nil {
return "", "", err return "", "", err
} }
@ -39,15 +62,15 @@ func (c *GitCommand) CurrentBranchName() (string, string, error) {
return "HEAD", "HEAD", nil return "HEAD", "HEAD", nil
} }
// DeleteBranch delete branch // Delete delete branch
func (c *GitCommand) DeleteBranch(branch string, force bool) error { func (self *BranchCommands) Delete(branch string, force bool) error {
command := "git branch -d" command := "git branch -d"
if force { if force {
command = "git branch -D" 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 // 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 EnvVars []string
} }
func (c *GitCommand) Checkout(branch string, options CheckoutOptions) error { func (self *BranchCommands) Checkout(branch string, options CheckoutOptions) error {
forceArg := "" forceArg := ""
if options.Force { if options.Force {
forceArg = " --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 // prevents git from prompting us for input which would freeze the program
// TODO: see if this is actually needed here // TODO: see if this is actually needed here
AddEnvVars("GIT_TERMINAL_PROMPT=0"). AddEnvVars("GIT_TERMINAL_PROMPT=0").
@ -70,104 +93,84 @@ func (c *GitCommand) Checkout(branch string, options CheckoutOptions) error {
Run() 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 // Currently it limits the result to 100 commits, but when we get async stuff
// working we can do lazy loading // working we can do lazy loading
func (c *GitCommand) GetBranchGraph(branchName string) (string, error) { func (self *BranchCommands) GetGraph(branchName string) (string, error) {
return c.GetBranchGraphCmdObj(branchName).DontLog().RunWithOutput() return self.GetGraphCmdObj(branchName).DontLog().RunWithOutput()
} }
func (c *GitCommand) GetUpstreamForBranch(branchName string) (string, error) { func (self *BranchCommands) GetGraphCmdObj(branchName string) oscommands.ICmdObj {
output, err := c.Cmd.New(fmt.Sprintf("git rev-parse --abbrev-ref --symbolic-full-name %s@{u}", c.OSCommand.Quote(branchName))).DontLog().RunWithOutput() 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 return strings.TrimSpace(output), err
} }
func (c *GitCommand) GetBranchGraphCmdObj(branchName string) oscommands.ICmdObj { func (self *BranchCommands) SetUpstream(remoteName string, remoteBranchName string, branchName string) error {
branchLogCmdTemplate := c.UserConfig.Git.BranchLogCmd 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()
templateValues := map[string]string{
"branchName": c.OSCommand.Quote(branchName),
}
return c.Cmd.New(utils.ResolvePlaceholderString(branchLogCmdTemplate, templateValues)).DontLog()
} }
func (c *GitCommand) SetUpstreamBranch(upstream string) error { func (self *BranchCommands) GetCurrentBranchUpstreamDifferenceCount() (string, string) {
return c.Cmd.New("git branch -u " + c.OSCommand.Quote(upstream)).Run() return self.GetCommitDifferences("HEAD", "HEAD@{u}")
} }
func (c *GitCommand) SetBranchUpstream(remoteName string, remoteBranchName string, branchName string) error { func (self *BranchCommands) GetUpstreamDifferenceCount(branchName string) (string, string) {
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() return self.GetCommitDifferences(branchName, branchName+"@{u}")
}
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}")
} }
// GetCommitDifferences checks how many pushables/pullables there are for the // GetCommitDifferences checks how many pushables/pullables there are for the
// current branch // 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" 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 { if err != nil {
return "?", "?" 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 { if err != nil {
return "?", "?" return "?", "?"
} }
return strings.TrimSpace(pushableCount), strings.TrimSpace(pullableCount) 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 { type MergeOpts struct {
FastForwardOnly bool FastForwardOnly bool
} }
// Merge merge func (self *BranchCommands) Merge(branchName string, opts MergeOpts) error {
func (c *GitCommand) Merge(branchName string, opts MergeOpts) error {
mergeArg := "" mergeArg := ""
if c.UserConfig.Git.Merging.Args != "" { if self.UserConfig.Git.Merging.Args != "" {
mergeArg = " " + c.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 { if opts.FastForwardOnly {
command = fmt.Sprintf("%s --ff-only", command) command = fmt.Sprintf("%s --ff-only", command)
} }
return c.OSCommand.Cmd.New(command).Run() return self.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()
} }

View File

@ -42,7 +42,7 @@ func TestGitCommandGetCommitDifferences(t *testing.T) {
for _, s := range scenarios { for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) { t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommandWithRunner(s.runner) 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.expectedPushables, pushables)
assert.EqualValues(t, s.expectedPullables, pullables) assert.EqualValues(t, s.expectedPullables, pullables)
s.runner.CheckForMissingCalls() s.runner.CheckForMissingCalls()
@ -55,7 +55,7 @@ func TestGitCommandNewBranch(t *testing.T) {
Expect(`git checkout -b "test" "master"`, "", nil) Expect(`git checkout -b "test" "master"`, "", nil)
gitCmd := NewDummyGitCommandWithRunner(runner) gitCmd := NewDummyGitCommandWithRunner(runner)
assert.NoError(t, gitCmd.NewBranch("test", "master")) assert.NoError(t, gitCmd.Branch.New("test", "master"))
runner.CheckForMissingCalls() runner.CheckForMissingCalls()
} }
@ -90,7 +90,7 @@ func TestGitCommandDeleteBranch(t *testing.T) {
t.Run(s.testName, func(t *testing.T) { t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommandWithRunner(s.runner) gitCmd := NewDummyGitCommandWithRunner(s.runner)
s.test(gitCmd.DeleteBranch("test", s.force)) s.test(gitCmd.Branch.Delete("test", s.force))
s.runner.CheckForMissingCalls() s.runner.CheckForMissingCalls()
}) })
} }
@ -101,7 +101,7 @@ func TestGitCommandMerge(t *testing.T) {
Expect(`git merge --no-edit "test"`, "", nil) Expect(`git merge --no-edit "test"`, "", nil)
gitCmd := NewDummyGitCommandWithRunner(runner) gitCmd := NewDummyGitCommandWithRunner(runner)
assert.NoError(t, gitCmd.Merge("test", MergeOpts{})) assert.NoError(t, gitCmd.Branch.Merge("test", MergeOpts{}))
runner.CheckForMissingCalls() runner.CheckForMissingCalls()
} }
@ -135,7 +135,7 @@ func TestGitCommandCheckout(t *testing.T) {
for _, s := range scenarios { for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) { t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommandWithRunner(s.runner) 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() s.runner.CheckForMissingCalls()
}) })
} }
@ -146,7 +146,7 @@ func TestGitCommandGetBranchGraph(t *testing.T) {
"log", "--graph", "--color=always", "--abbrev-commit", "--decorate", "--date=relative", "--pretty=medium", "test", "--", "log", "--graph", "--color=always", "--abbrev-commit", "--decorate", "--date=relative", "--pretty=medium", "test", "--",
}, "", nil) }, "", nil)
gitCmd := NewDummyGitCommandWithRunner(runner) gitCmd := NewDummyGitCommandWithRunner(runner)
_, err := gitCmd.GetBranchGraph("test") _, err := gitCmd.Branch.GetGraph("test")
assert.NoError(t, err) assert.NoError(t, err)
} }
@ -215,36 +215,8 @@ func TestGitCommandCurrentBranchName(t *testing.T) {
for _, s := range scenarios { for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) { t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommandWithRunner(s.runner) gitCmd := NewDummyGitCommandWithRunner(s.runner)
s.test(gitCmd.CurrentBranchName()) s.test(gitCmd.Branch.CurrentBranchName())
s.runner.CheckForMissingCalls() 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))
})
}
}

View File

@ -4,18 +4,34 @@ import (
"fmt" "fmt"
"strings" "strings"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands" "github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/common"
) )
// RenameCommit renames the topmost commit with the given name type CommitCommands struct {
func (c *GitCommand) RenameCommit(name string) error { *common.Common
return c.Cmd.New("git commit --allow-empty --amend --only -m " + c.OSCommand.Quote(name)).Run()
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 // ResetToCommit reset to commit
func (c *GitCommand) ResetToCommit(sha string, strength string, envVars []string) error { func (self *CommitCommands) ResetToCommit(sha string, strength string, envVars []string) error {
return c.Cmd.New(fmt.Sprintf("git reset --%s %s", strength, sha)). return self.cmd.New(fmt.Sprintf("git reset --%s %s", strength, sha)).
// prevents git from prompting us for input which would freeze the program // prevents git from prompting us for input which would freeze the program
// TODO: see if this is actually needed here // TODO: see if this is actually needed here
AddEnvVars("GIT_TERMINAL_PROMPT=0"). AddEnvVars("GIT_TERMINAL_PROMPT=0").
@ -23,11 +39,11 @@ func (c *GitCommand) ResetToCommit(sha string, strength string, envVars []string
Run() 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") splitMessage := strings.Split(message, "\n")
lineArgs := "" lineArgs := ""
for _, line := range splitMessage { for _, line := range splitMessage {
lineArgs += fmt.Sprintf(" -m %s", c.OSCommand.Quote(line)) lineArgs += fmt.Sprintf(" -m %s", self.cmd.Quote(line))
} }
flagsStr := "" flagsStr := ""
@ -35,71 +51,56 @@ func (c *GitCommand) CommitCmdObj(message string, flags string) oscommands.ICmdO
flagsStr = fmt.Sprintf(" %s", flags) 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 // Get the subject of the HEAD commit
func (c *GitCommand) GetHeadCommitMessage() (string, error) { func (self *CommitCommands) GetHeadCommitMessage() (string, error) {
message, err := c.Cmd.New("git log -1 --pretty=%s").DontLog().RunWithOutput() message, err := self.cmd.New("git log -1 --pretty=%s").DontLog().RunWithOutput()
return strings.TrimSpace(message), err 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 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") message := strings.Join(strings.SplitAfter(messageWithHeader, "\n")[1:], "\n")
return strings.TrimSpace(message), err return strings.TrimSpace(message), err
} }
func (c *GitCommand) GetCommitMessageFirstLine(sha string) (string, error) { func (self *CommitCommands) GetCommitMessageFirstLine(sha string) (string, error) {
return c.Cmd.New(fmt.Sprintf("git show --no-patch --pretty=format:%%s %s", sha)).DontLog().RunWithOutput() 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 // AmendHead amends HEAD with whatever is staged in your working tree
func (c *GitCommand) AmendHead() error { func (self *CommitCommands) AmendHead() error {
return c.AmendHeadCmdObj().Run() return self.AmendHeadCmdObj().Run()
} }
func (c *GitCommand) AmendHeadCmdObj() oscommands.ICmdObj { func (self *CommitCommands) AmendHeadCmdObj() oscommands.ICmdObj {
return c.Cmd.New("git commit --amend --no-edit --allow-empty") return self.cmd.New("git commit --amend --no-edit --allow-empty")
} }
func (c *GitCommand) ShowCmdObj(sha string, filterPath string) oscommands.ICmdObj { func (self *CommitCommands) ShowCmdObj(sha string, filterPath string) oscommands.ICmdObj {
contextSize := c.UserConfig.Git.DiffContextSize contextSize := self.UserConfig.Git.DiffContextSize
filterPathArg := "" filterPathArg := ""
if filterPath != "" { 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) 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 c.Cmd.New(cmdStr).DontLog() return self.cmd.New(cmdStr).DontLog()
} }
// Revert reverts the selected commit by sha // Revert reverts the selected commit by sha
func (c *GitCommand) Revert(sha string) error { func (self *CommitCommands) Revert(sha string) error {
return c.Cmd.New(fmt.Sprintf("git revert %s", sha)).Run() return self.cmd.New(fmt.Sprintf("git revert %s", sha)).Run()
} }
func (c *GitCommand) RevertMerge(sha string, parentNumber int) error { func (self *CommitCommands) RevertMerge(sha string, parentNumber int) error {
return c.Cmd.New(fmt.Sprintf("git revert %s -m %d", sha, parentNumber)).Run() return self.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()
} }
// CreateFixupCommit creates a commit that fixes up a previous commit // CreateFixupCommit creates a commit that fixes up a previous commit
func (c *GitCommand) CreateFixupCommit(sha string) error { func (self *CommitCommands) CreateFixupCommit(sha string) error {
return c.Cmd.New(fmt.Sprintf("git commit --fixup=%s", sha)).Run() return self.cmd.New(fmt.Sprintf("git commit --fixup=%s", sha)).Run()
} }

View File

@ -7,12 +7,12 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func TestGitCommandRenameCommit(t *testing.T) { func TestGitCommandRewordCommit(t *testing.T) {
runner := oscommands.NewFakeRunner(t). runner := oscommands.NewFakeRunner(t).
ExpectGitArgs([]string{"commit", "--allow-empty", "--amend", "--only", "-m", "test"}, "", nil) ExpectGitArgs([]string{"commit", "--allow-empty", "--amend", "--only", "-m", "test"}, "", nil)
gitCmd := NewDummyGitCommandWithRunner(runner) gitCmd := NewDummyGitCommandWithRunner(runner)
assert.NoError(t, gitCmd.RenameCommit("test")) assert.NoError(t, gitCmd.Commit.RewordLastCommit("test"))
runner.CheckForMissingCalls() runner.CheckForMissingCalls()
} }
@ -21,7 +21,7 @@ func TestGitCommandResetToCommit(t *testing.T) {
ExpectGitArgs([]string{"reset", "--hard", "78976bc"}, "", nil) ExpectGitArgs([]string{"reset", "--hard", "78976bc"}, "", nil)
gitCmd := NewDummyGitCommandWithRunner(runner) gitCmd := NewDummyGitCommandWithRunner(runner)
assert.NoError(t, gitCmd.ResetToCommit("78976bc", "hard", []string{})) assert.NoError(t, gitCmd.Commit.ResetToCommit("78976bc", "hard", []string{}))
runner.CheckForMissingCalls() runner.CheckForMissingCalls()
} }
@ -57,7 +57,7 @@ func TestGitCommandCommitObj(t *testing.T) {
for _, s := range scenarios { for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) { t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommand() 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) assert.Equal(t, s.expected, cmdStr)
}) })
} }
@ -86,7 +86,7 @@ func TestGitCommandCreateFixupCommit(t *testing.T) {
for _, s := range scenarios { for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) { t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommandWithRunner(s.runner) gitCmd := NewDummyGitCommandWithRunner(s.runner)
s.test(gitCmd.CreateFixupCommit(s.sha)) s.test(gitCmd.Commit.CreateFixupCommit(s.sha))
s.runner.CheckForMissingCalls() s.runner.CheckForMissingCalls()
}) })
} }
@ -125,7 +125,7 @@ func TestGitCommandShowCmdObj(t *testing.T) {
t.Run(s.testName, func(t *testing.T) { t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommand() gitCmd := NewDummyGitCommand()
gitCmd.UserConfig.Git.DiffContextSize = s.contextSize 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) assert.Equal(t, s.expected, cmdStr)
}) })
} }

View File

@ -5,24 +5,42 @@ import (
"strconv" "strconv"
"strings" "strings"
"github.com/jesseduffield/lazygit/pkg/commands/git_config"
"github.com/jesseduffield/lazygit/pkg/common"
"github.com/jesseduffield/lazygit/pkg/utils" "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") != "" { if os.Getenv("GIT_PAGER") != "" {
return os.Getenv("GIT_PAGER") return os.Getenv("GIT_PAGER")
} }
if os.Getenv("PAGER") != "" { if os.Getenv("PAGER") != "" {
return 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] return strings.Split(output, "\n")[0]
} }
func (c *GitCommand) GetPager(width int) string { func (self *ConfigCommands) GetPager(width int) string {
useConfig := c.UserConfig.Git.Paging.UseConfig useConfig := self.UserConfig.Git.Paging.UseConfig
if useConfig { if useConfig {
pager := c.ConfiguredPager() pager := self.ConfiguredPager()
return strings.Split(pager, "| less")[0] return strings.Split(pager, "| less")[0]
} }
@ -30,21 +48,35 @@ func (c *GitCommand) GetPager(width int) string {
"columnWidth": strconv.Itoa(width/2 - 6), "columnWidth": strconv.Itoa(width/2 - 6),
} }
pagerTemplate := c.UserConfig.Git.Paging.Pager pagerTemplate := self.UserConfig.Git.Paging.Pager
return utils.ResolvePlaceholderString(pagerTemplate, templateValues) 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 // 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 // whether we need to run a subprocess to allow them to enter their password
func (c *GitCommand) UsingGpg() bool { func (self *ConfigCommands) UsingGpg() bool {
overrideGpg := c.UserConfig.Git.OverrideGpg overrideGpg := self.UserConfig.Git.OverrideGpg
if overrideGpg { if overrideGpg {
return false 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"
} }

View File

@ -1,10 +1,6 @@
package commands package commands
import ( import (
"io"
"io/ioutil"
"github.com/jesseduffield/lazygit/pkg/commands/git_config"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands" "github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/utils" "github.com/jesseduffield/lazygit/pkg/utils"
) )
@ -16,16 +12,13 @@ func NewDummyGitCommand() *GitCommand {
// NewDummyGitCommandWithOSCommand creates a new dummy GitCommand for testing // NewDummyGitCommandWithOSCommand creates a new dummy GitCommand for testing
func NewDummyGitCommandWithOSCommand(osCommand *oscommands.OSCommand) *GitCommand { func NewDummyGitCommandWithOSCommand(osCommand *oscommands.OSCommand) *GitCommand {
runner := &oscommands.FakeCmdObjRunner{} return NewGitCommandAux(
builder := oscommands.NewDummyCmdObjBuilder(runner) utils.NewDummyCommon(),
osCommand,
return &GitCommand{ utils.NewDummyGitConfig(),
Common: utils.NewDummyCommon(), ".git",
Cmd: builder, nil,
OSCommand: osCommand, )
GitConfig: git_config.NewFakeGitConfig(map[string]string{}),
GetCmdWriter: func() io.Writer { return ioutil.Discard },
}
} }
func NewDummyGitCommandWithRunner(runner oscommands.ICmdObjRunner) *GitCommand { func NewDummyGitCommandWithRunner(runner oscommands.ICmdObjRunner) *GitCommand {

View File

@ -1,23 +1,43 @@
package commands package commands
import ( import (
"fmt"
"io/ioutil" "io/ioutil"
"os"
"path/filepath"
"strconv" "strconv"
"time"
"github.com/go-errors/errors" "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/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/gui/filetree" "github.com/jesseduffield/lazygit/pkg/common"
"github.com/jesseduffield/lazygit/pkg/utils" "github.com/jesseduffield/lazygit/pkg/utils"
) )
// CatFile obtains the content of a file type FileCommands struct {
func (c *GitCommand) CatFile(fileName string) (string, error) { *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) buf, err := ioutil.ReadFile(fileName)
if err != nil { if err != nil {
return "", nil return "", nil
@ -25,335 +45,24 @@ func (c *GitCommand) CatFile(fileName string) (string, error) {
return string(buf), nil return string(buf), nil
} }
func (c *GitCommand) OpenMergeToolCmdObj() oscommands.ICmdObj { func (c *FileCommands) GetEditCmdStr(filename string, lineNumber int) (string, error) {
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) {
editor := c.UserConfig.OS.EditCommand editor := c.UserConfig.OS.EditCommand
if editor == "" { if editor == "" {
editor = c.GitConfig.Get("core.editor") editor = c.config.GetCoreEditor()
} }
if editor == "" { if editor == "" {
editor = c.OSCommand.Getenv("GIT_EDITOR") editor = c.os.Getenv("GIT_EDITOR")
} }
if editor == "" { if editor == "" {
editor = c.OSCommand.Getenv("VISUAL") editor = c.os.Getenv("VISUAL")
} }
if editor == "" { if editor == "" {
editor = c.OSCommand.Getenv("EDITOR") editor = c.os.Getenv("EDITOR")
} }
if 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" editor = "vi"
} }
} }
@ -363,7 +72,7 @@ func (c *GitCommand) EditFileCmdStr(filename string, lineNumber int) (string, er
templateValues := map[string]string{ templateValues := map[string]string{
"editor": editor, "editor": editor,
"filename": c.OSCommand.Quote(filename), "filename": c.cmd.Quote(filename),
"line": strconv.Itoa(lineNumber), "line": strconv.Itoa(lineNumber),
} }

View File

@ -1,602 +1,14 @@
package commands package commands
import ( import (
"fmt"
"io/ioutil"
"regexp"
"testing" "testing"
"github.com/go-errors/errors" "github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/commands/git_config" "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/commands/oscommands"
"github.com/stretchr/testify/assert" "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) { func TestEditFileCmdStr(t *testing.T) {
type scenario struct { type scenario struct {
filename string filename string
@ -736,9 +148,9 @@ func TestEditFileCmdStr(t *testing.T) {
gitCmd := NewDummyGitCommandWithRunner(s.runner) gitCmd := NewDummyGitCommandWithRunner(s.runner)
gitCmd.UserConfig.OS.EditCommand = s.configEditCommand gitCmd.UserConfig.OS.EditCommand = s.configEditCommand
gitCmd.UserConfig.OS.EditCommandTemplate = s.configEditCommandTemplate gitCmd.UserConfig.OS.EditCommandTemplate = s.configEditCommandTemplate
gitCmd.OSCommand.Getenv = s.getenv gitCmd.OSCommand.GetenvFn = s.getenv
gitCmd.GitConfig = git_config.NewFakeGitConfig(s.gitConfigMockResponses) gitCmd.gitConfig = git_config.NewFakeGitConfig(s.gitConfigMockResponses)
s.test(gitCmd.EditFileCmdStr(s.filename, 1)) s.test(gitCmd.File.GetEditCmdStr(s.filename, 1))
s.runner.CheckForMissingCalls() s.runner.CheckForMissingCalls()
} }
} }

View File

@ -1,7 +1,6 @@
package commands package commands
import ( import (
"io"
"io/ioutil" "io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
@ -19,11 +18,31 @@ import (
"github.com/jesseduffield/lazygit/pkg/utils" "github.com/jesseduffield/lazygit/pkg/utils"
) )
// this takes something like: // GitCommand is our main git interface
// * (HEAD detached at 264fc6f5) type GitCommand struct {
// remotes *common.Common
// and returns '264fc6f5' as the second match OSCommand *oscommands.OSCommand
const CurrentBranchNameRegex = `(?m)^\*.*?([^ ]*?)\)?$`
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 { type Loaders struct {
Commits *loaders.CommitLoader Commits *loaders.CommitLoader
@ -36,44 +55,17 @@ type Loaders struct {
Tags *loaders.TagLoader 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( func NewGitCommand(
cmn *common.Common, cmn *common.Common,
osCommand *oscommands.OSCommand, osCommand *oscommands.OSCommand,
gitConfig git_config.IGitConfig, gitConfig git_config.IGitConfig,
) (*GitCommand, error) { ) (*GitCommand, error) {
var repo *gogit.Repository
pushToCurrent := gitConfig.Get("push.default") == "current"
if err := navigateToRepoRootDirectory(os.Stat, os.Chdir); err != nil { if err := navigateToRepoRootDirectory(os.Stat, os.Chdir); err != nil {
return nil, err return nil, err
} }
var err error repo, err := setupRepository(gogit.PlainOpen, cmn.Tr.GitconfigParseErr)
if repo, err = setupRepository(gogit.PlainOpen, cmn.Tr.GitconfigParseErr); err != nil { if err != nil {
return nil, err return nil, err
} }
@ -82,33 +74,81 @@ func NewGitCommand(
return nil, err 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) cmd := NewGitCmdObjBuilder(cmn.Log, osCommand.Cmd)
gitCommand := &GitCommand{ 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, Common: cmn,
OSCommand: osCommand, OSCommand: osCommand,
Repo: repo,
DotGitDir: dotGitDir,
PushToCurrent: pushToCurrent,
GitConfig: gitConfig,
GetCmdWriter: func() io.Writer { return ioutil.Discard },
Cmd: cmd,
}
gitCommand.Loaders = Loaders{ Repo: repo,
Commits: loaders.NewCommitLoader(cmn, gitCommand),
Branches: loaders.NewBranchLoader(cmn, gitCommand), Cmd: cmd,
Files: loaders.NewFileLoader(cmn, cmd, gitConfig),
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), CommitFiles: loaders.NewCommitFileLoader(cmn, cmd),
Remotes: loaders.NewRemoteLoader(cmn, cmd, gitCommand.Repo.Remotes), Remotes: loaders.NewRemoteLoader(cmn, cmd, repo.Remotes),
ReflogCommits: loaders.NewReflogCommitLoader(cmn, cmd), ReflogCommits: loaders.NewReflogCommitLoader(cmn, cmd),
Stash: loaders.NewStashLoader(cmn, cmd), Stash: loaders.NewStashLoader(cmn, cmd),
Tags: loaders.NewTagLoader(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 { 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 { func VerifyInGitRepo(osCommand *oscommands.OSCommand) error {
return osCommand.Cmd.New("git rev-parse --git-dir").DontLog().Run() 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
}

View File

@ -27,19 +27,15 @@ type BranchLoader struct {
getCurrentBranchName func() (string, string, error) getCurrentBranchName func() (string, string, error)
} }
type BranchLoaderGitCommand interface {
GetRawBranches() (string, error)
CurrentBranchName() (string, string, error)
}
func NewBranchLoader( func NewBranchLoader(
cmn *common.Common, cmn *common.Common,
gitCommand BranchLoaderGitCommand, getRawBranches func() (string, error),
getCurrentBranchName func() (string, string, error),
) *BranchLoader { ) *BranchLoader {
return &BranchLoader{ return &BranchLoader{
Common: cmn, Common: cmn,
getRawBranches: gitCommand.GetRawBranches, getRawBranches: getRawBranches,
getCurrentBranchName: gitCommand.CurrentBranchName, getCurrentBranchName: getCurrentBranchName,
} }
} }

View File

@ -36,26 +36,22 @@ type CommitLoader struct {
dotGitDir string 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 // making our dependencies explicit for the sake of easier testing
func NewCommitLoader( func NewCommitLoader(
cmn *common.Common, cmn *common.Common,
gitCommand CommitLoaderGitCommand, cmd oscommands.ICmdObjBuilder,
dotGitDir string,
getCurrentBranchName func() (string, string, error),
getRebaseMode func() (enums.RebaseMode, error),
) *CommitLoader { ) *CommitLoader {
return &CommitLoader{ return &CommitLoader{
Common: cmn, Common: cmn,
cmd: gitCommand.GetCmd(), cmd: cmd,
getCurrentBranchName: gitCommand.CurrentBranchName, getCurrentBranchName: getCurrentBranchName,
getRebaseMode: gitCommand.RebaseMode, getRebaseMode: getRebaseMode,
readFile: ioutil.ReadFile, readFile: ioutil.ReadFile,
walkFiles: filepath.Walk, walkFiles: filepath.Walk,
dotGitDir: gitCommand.GetDotGitDir(), dotGitDir: dotGitDir,
} }
} }

View File

@ -11,19 +11,24 @@ import (
"github.com/jesseduffield/lazygit/pkg/utils" "github.com/jesseduffield/lazygit/pkg/utils"
) )
type FileLoaderConfig interface {
GetShowUntrackedFiles() string
}
type FileLoader struct { type FileLoader struct {
*common.Common *common.Common
cmd oscommands.ICmdObjBuilder cmd oscommands.ICmdObjBuilder
config FileLoaderConfig
gitConfig git_config.IGitConfig gitConfig git_config.IGitConfig
getFileType func(string) string 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{ return &FileLoader{
Common: cmn, Common: cmn,
cmd: cmd, cmd: cmd,
gitConfig: gitConfig,
getFileType: oscommands.FileType, getFileType: oscommands.FileType,
config: config,
} }
} }
@ -33,7 +38,7 @@ type GetStatusFileOptions struct {
func (self *FileLoader) GetStatusFiles(opts GetStatusFileOptions) []*models.File { func (self *FileLoader) GetStatusFiles(opts GetStatusFileOptions) []*models.File {
// check if config wants us ignoring untracked files // check if config wants us ignoring untracked files
untrackedFilesSetting := self.gitConfig.Get("status.showUntrackedFiles") untrackedFilesSetting := self.config.GetShowUntrackedFiles()
if untrackedFilesSetting == "" { if untrackedFilesSetting == "" {
untrackedFilesSetting = "all" untrackedFilesSetting = "all"

View File

@ -34,6 +34,11 @@ type ICmdObj interface {
// This returns false if DontLog() was called // This returns false if DontLog() was called
ShouldLog() bool ShouldLog() bool
PromptOnCredentialRequest() ICmdObj
FailOnCredentialRequest() ICmdObj
GetCredentialStrategy() CredentialStrategy
} }
type CmdObj struct { 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. // if set to true, we don't want to log the command to the user.
dontLog bool 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 { func (self *CmdObj) GetCmd() *exec.Cmd {
return self.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 { func (self *CmdObj) RunAndProcessLines(onLine func(line string) (bool, error)) error {
return self.runner.RunAndProcessLines(self, onLine) 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
}

View File

@ -16,17 +16,45 @@ type ICmdObjRunner interface {
type cmdObjRunner struct { type cmdObjRunner struct {
log *logrus.Entry log *logrus.Entry
logCmdObj func(ICmdObj) guiIO *guiIO
} }
var _ ICmdObjRunner = &cmdObjRunner{} 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 { func (self *cmdObjRunner) Run(cmdObj ICmdObj) error {
if cmdObj.GetCredentialStrategy() == NONE {
_, err := self.RunWithOutput(cmdObj) _, err := self.RunWithOutput(cmdObj)
return err 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) { 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() { if cmdObj.ShouldLog() {
self.logCmdObj(cmdObj) 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 { 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() { if cmdObj.ShouldLog() {
self.logCmdObj(cmdObj) self.logCmdObj(cmdObj)
} }

View File

@ -6,7 +6,7 @@ import (
// NewDummyOSCommand creates a new dummy OSCommand for testing // NewDummyOSCommand creates a new dummy OSCommand for testing
func NewDummyOSCommand() *OSCommand { func NewDummyOSCommand() *OSCommand {
osCmd := NewOSCommand(utils.NewDummyCommon(), dummyPlatform) osCmd := NewOSCommand(utils.NewDummyCommon(), dummyPlatform, NewNullGuiIO(utils.NewDummyLog()))
return osCmd return osCmd
} }
@ -27,7 +27,7 @@ var dummyPlatform = &Platform{
} }
func NewDummyOSCommandWithRunner(runner *FakeCmdObjRunner) *OSCommand { func NewDummyOSCommandWithRunner(runner *FakeCmdObjRunner) *OSCommand {
osCommand := NewOSCommand(utils.NewDummyCommon(), dummyPlatform) osCommand := NewOSCommand(utils.NewDummyCommon(), dummyPlatform, NewNullGuiIO(utils.NewDummyLog()))
osCommand.Cmd = NewDummyCmdObjBuilder(runner) osCommand.Cmd = NewDummyCmdObjBuilder(runner)
return osCommand return osCommand

View File

@ -13,12 +13,12 @@ import (
"github.com/jesseduffield/lazygit/pkg/utils" "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 // 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 // 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 := "" ttyText := ""
errMessage := c.RunCommandWithOutputLive(cmdObj, writer, func(word string) string { err := self.RunCommandWithOutputLive(cmdObj, func(word string) string {
ttyText = ttyText + " " + word ttyText = ttyText + " " + word
prompts := map[string]string{ prompts := map[string]string{
@ -37,13 +37,7 @@ func (c *OSCommand) DetectUnamePass(cmdObj ICmdObj, writer io.Writer, promptUser
return "" return ""
}) })
return errMessage return err
}
// Due to a lack of pty support on windows we have RunCommandWithOutputLiveWrapper being defined
// separate for windows and other OS's
func (c *OSCommand) RunCommandWithOutputLive(cmdObj ICmdObj, writer io.Writer, handleOutput func(string) string) error {
return RunCommandWithOutputLiveWrapper(c, cmdObj, writer, handleOutput)
} }
type cmdHandler struct { type cmdHandler struct {
@ -56,23 +50,22 @@ type cmdHandler struct {
// Output is a function that executes by every word that gets read by bufio // 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 // 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 // NOTE: If the return data is empty it won't write anything to stdin
func RunCommandWithOutputLiveAux( func (self *cmdObjRunner) RunCommandWithOutputLiveAux(
c *OSCommand,
cmdObj ICmdObj, cmdObj ICmdObj,
writer io.Writer,
// handleOutput takes a word from stdout and returns a string to be written to stdin. // 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, handleOutput func(string) string,
startCmd func(cmd *exec.Cmd) (*cmdHandler, error), startCmd func(cmd *exec.Cmd) (*cmdHandler, error),
) 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() { 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() cmd := cmdObj.AddEnvVars("LANG=en_US.UTF-8", "LC_ALL=en_US.UTF-8").GetCmd()
var stderr bytes.Buffer var stderr bytes.Buffer
cmd.Stderr = io.MultiWriter(writer, &stderr) cmd.Stderr = io.MultiWriter(cmdWriter, &stderr)
handler, err := startCmd(cmd) handler, err := startCmd(cmd)
if err != nil { if err != nil {
@ -81,11 +74,11 @@ func RunCommandWithOutputLiveAux(
defer func() { defer func() {
if closeErr := handler.close(); closeErr != nil { 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() { go utils.Safe(func() {
scanner := bufio.NewScanner(tr) scanner := bufio.NewScanner(tr)

View File

@ -4,22 +4,19 @@
package oscommands package oscommands
import ( import (
"io"
"os/exec" "os/exec"
"github.com/creack/pty" "github.com/creack/pty"
) )
func RunCommandWithOutputLiveWrapper( // we define this separately for windows and non-windows given that windows does
c *OSCommand, // not have great PTY support and we need a PTY to handle a credential request
func (self *cmdObjRunner) RunCommandWithOutputLive(
cmdObj ICmdObj, cmdObj ICmdObj,
writer io.Writer,
output func(string) string, output func(string) string,
) error { ) error {
return RunCommandWithOutputLiveAux( return self.RunCommandWithOutputLiveAux(
c,
cmdObj, cmdObj,
writer,
output, output,
func(cmd *exec.Cmd) (*cmdHandler, error) { func(cmd *exec.Cmd) (*cmdHandler, error) {
ptmx, err := pty.Start(cmd) ptmx, err := pty.Start(cmd)

View File

@ -26,18 +26,14 @@ func (b *Buffer) Write(p []byte) (n int, err error) {
return b.b.Write(p) 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 // 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( func (self *cmdObjRunner) RunCommandWithOutputLive(
c *OSCommand,
cmdObj ICmdObj, cmdObj ICmdObj,
writer io.Writer,
output func(string) string, output func(string) string,
) error { ) error {
return RunCommandWithOutputLiveAux( return self.RunCommandWithOutputLiveAux(
c,
cmdObj, cmdObj,
writer,
output, output,
func(cmd *exec.Cmd) (*cmdHandler, error) { func(cmd *exec.Cmd) (*cmdHandler, error) {
stdoutReader, stdoutWriter := io.Pipe() stdoutReader, stdoutWriter := io.Pipe()

View File

@ -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 "" },
}
}

View File

@ -20,7 +20,7 @@ import (
type OSCommand struct { type OSCommand struct {
*common.Common *common.Common
Platform *Platform Platform *Platform
Getenv func(string) string GetenvFn func(string) string
// callback to run before running a command, i.e. for the purposes of logging. // 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 // 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 // NewOSCommand os command runner
func NewOSCommand(common *common.Common, platform *Platform) *OSCommand { func NewOSCommand(common *common.Common, platform *Platform, guiIO *guiIO) *OSCommand {
c := &OSCommand{ c := &OSCommand{
Common: common, Common: common,
Platform: platform, Platform: platform,
Getenv: os.Getenv, GetenvFn: os.Getenv,
removeFile: os.RemoveAll, 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} c.Cmd = &CmdObjBuilder{runner: runner, platform: platform}
return c return c
} }
func (c *OSCommand) LogCmdObj(cmdObj ICmdObj) {
c.LogCommand(cmdObj.ToString(), true)
}
func (c *OSCommand) LogCommand(cmdStr string, commandLine bool) { func (c *OSCommand) LogCommand(cmdStr string, commandLine bool) {
c.Log.WithField("command", cmdStr).Info("RunCommand") c.Log.WithField("command", cmdStr).Info("RunCommand")
@ -270,6 +266,10 @@ func (c *OSCommand) RemoveFile(path string) error {
return c.removeFile(path) return c.removeFile(path)
} }
func (c *OSCommand) Getenv(key string) string {
return c.GetenvFn(key)
}
func GetTempDir() string { func GetTempDir() string {
return filepath.Join(os.TempDir(), "lazygit") return filepath.Join(os.TempDir(), "lazygit")
} }

View File

@ -5,64 +5,98 @@ import (
"github.com/go-errors/errors" "github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/commands/models" "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/patch"
"github.com/jesseduffield/lazygit/pkg/commands/types/enums" "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 // DeletePatchesFromCommit applies a patch in reverse for a commit
func (c *GitCommand) DeletePatchesFromCommit(commits []*models.Commit, commitIndex int, p *patch.PatchManager) error { func (self *PatchCommands) DeletePatchesFromCommit(commits []*models.Commit, commitIndex int) error {
if err := c.BeginInteractiveRebaseForCommit(commits, commitIndex); err != nil { if err := self.rebase.BeginInteractiveRebaseForCommit(commits, commitIndex); err != nil {
return err return err
} }
// apply each patch in reverse // apply each patch in reverse
if err := p.ApplyPatches(true); err != nil { if err := self.PatchManager.ApplyPatches(true); err != nil {
if err := c.GenericMergeOrRebaseAction("rebase", "abort"); err != nil { if err := self.rebase.GenericMergeOrRebaseAction("rebase", "abort"); err != nil {
return err return err
} }
return err return err
} }
// time to amend the selected commit // time to amend the selected commit
if err := c.AmendHead(); err != nil { if err := self.commit.AmendHead(); err != nil {
return err return err
} }
c.onSuccessfulContinue = func() error { self.rebase.onSuccessfulContinue = func() error {
c.PatchManager.Reset() self.PatchManager.Reset()
return nil return nil
} }
// continue // 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 sourceCommitIdx < destinationCommitIdx {
if err := c.BeginInteractiveRebaseForCommit(commits, destinationCommitIdx); err != nil { if err := self.rebase.BeginInteractiveRebaseForCommit(commits, destinationCommitIdx); err != nil {
return err return err
} }
// apply each patch forward // apply each patch forward
if err := p.ApplyPatches(false); err != nil { if err := self.PatchManager.ApplyPatches(false); err != nil {
if err := c.GenericMergeOrRebaseAction("rebase", "abort"); err != nil { if err := self.rebase.GenericMergeOrRebaseAction("rebase", "abort"); err != nil {
return err return err
} }
return err return err
} }
// amend the destination commit // amend the destination commit
if err := c.AmendHead(); err != nil { if err := self.commit.AmendHead(); err != nil {
return err return err
} }
c.onSuccessfulContinue = func() error { self.rebase.onSuccessfulContinue = func() error {
c.PatchManager.Reset() self.PatchManager.Reset()
return nil return nil
} }
// continue // continue
return c.GenericMergeOrRebaseAction("rebase", "continue") return self.rebase.GenericMergeOrRebaseAction("rebase", "continue")
} }
if len(commits)-1 < sourceCommitIdx { 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: // 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 // one where we handle the possibility of a credential request, and the other
// where we continue the rebase // where we continue the rebase
if c.UsingGpg() { if self.config.UsingGpg() {
return errors.New(c.Tr.DisabledForGPG) return errors.New(self.Tr.DisabledForGPG)
} }
baseIndex := sourceCommitIdx + 1 baseIndex := sourceCommitIdx + 1
@ -86,7 +120,7 @@ func (c *GitCommand) MovePatchToSelectedCommit(commits []*models.Commit, sourceC
todo = a + " " + commit.Sha + " " + commit.Name + "\n" + todo 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 { if err != nil {
return err return err
} }
@ -96,62 +130,62 @@ func (c *GitCommand) MovePatchToSelectedCommit(commits []*models.Commit, sourceC
} }
// apply each patch in reverse // apply each patch in reverse
if err := p.ApplyPatches(true); err != nil { if err := self.PatchManager.ApplyPatches(true); err != nil {
if err := c.GenericMergeOrRebaseAction("rebase", "abort"); err != nil { if err := self.rebase.GenericMergeOrRebaseAction("rebase", "abort"); err != nil {
return err return err
} }
return err return err
} }
// amend the source commit // amend the source commit
if err := c.AmendHead(); err != nil { if err := self.commit.AmendHead(); err != nil {
return err 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") 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. // 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 // 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 := self.PatchManager.ApplyPatches(false); err != nil {
if err := c.GenericMergeOrRebaseAction("rebase", "abort"); err != nil { if err := self.rebase.GenericMergeOrRebaseAction("rebase", "abort"); err != nil {
return err return err
} }
return err return err
} }
// amend the destination commit // amend the destination commit
if err := c.AmendHead(); err != nil { if err := self.commit.AmendHead(); err != nil {
return err return err
} }
c.onSuccessfulContinue = func() error { self.rebase.onSuccessfulContinue = func() error {
c.PatchManager.Reset() self.PatchManager.Reset()
return nil 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 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 return err
} }
} }
if err := c.BeginInteractiveRebaseForCommit(commits, commitIdx); err != nil { if err := self.rebase.BeginInteractiveRebaseForCommit(commits, commitIdx); err != nil {
return err return err
} }
if err := p.ApplyPatches(true); err != nil { if err := self.PatchManager.ApplyPatches(true); err != nil {
if c.WorkingTreeState() == enums.REBASE_MODE_REBASING { if self.status.WorkingTreeState() == enums.REBASE_MODE_REBASING {
if err := c.GenericMergeOrRebaseAction("rebase", "abort"); err != nil { if err := self.rebase.GenericMergeOrRebaseAction("rebase", "abort"); err != nil {
return err return err
} }
} }
@ -159,19 +193,19 @@ func (c *GitCommand) MovePatchIntoIndex(commits []*models.Commit, commitIdx int,
} }
// amend the commit // amend the commit
if err := c.AmendHead(); err != nil { if err := self.commit.AmendHead(); err != nil {
return err 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") 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 // add patches to index
if err := p.ApplyPatches(false); err != nil { if err := self.PatchManager.ApplyPatches(false); err != nil {
if c.WorkingTreeState() == enums.REBASE_MODE_REBASING { if self.status.WorkingTreeState() == enums.REBASE_MODE_REBASING {
if err := c.GenericMergeOrRebaseAction("rebase", "abort"); err != nil { if err := self.rebase.GenericMergeOrRebaseAction("rebase", "abort"); err != nil {
return err return err
} }
} }
@ -179,54 +213,54 @@ func (c *GitCommand) MovePatchIntoIndex(commits []*models.Commit, commitIdx int,
} }
if stash { if stash {
if err := c.StashDo(0, "apply"); err != nil { if err := self.stash.Apply(0); err != nil {
return err return err
} }
} }
c.PatchManager.Reset() self.PatchManager.Reset()
return nil 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 { func (self *PatchCommands) PullPatchIntoNewCommit(commits []*models.Commit, commitIdx int) error {
if err := c.BeginInteractiveRebaseForCommit(commits, commitIdx); err != nil { if err := self.rebase.BeginInteractiveRebaseForCommit(commits, commitIdx); err != nil {
return err return err
} }
if err := p.ApplyPatches(true); err != nil { if err := self.PatchManager.ApplyPatches(true); err != nil {
if err := c.GenericMergeOrRebaseAction("rebase", "abort"); err != nil { if err := self.rebase.GenericMergeOrRebaseAction("rebase", "abort"); err != nil {
return err return err
} }
return err return err
} }
// amend the commit // amend the commit
if err := c.AmendHead(); err != nil { if err := self.commit.AmendHead(); err != nil {
return err return err
} }
// add patches to index // add patches to index
if err := p.ApplyPatches(false); err != nil { if err := self.PatchManager.ApplyPatches(false); err != nil {
if err := c.GenericMergeOrRebaseAction("rebase", "abort"); err != nil { if err := self.rebase.GenericMergeOrRebaseAction("rebase", "abort"); err != nil {
return err return err
} }
return err return err
} }
head_message, _ := c.GetHeadCommitMessage() head_message, _ := self.commit.GetHeadCommitMessage()
new_message := fmt.Sprintf("Split from \"%s\"", head_message) 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 { if err != nil {
return err 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") return errors.New("You are midway through another rebase operation. Please abort to start again")
} }
c.PatchManager.Reset() self.PatchManager.Reset()
return c.GenericMergeOrRebaseAction("rebase", "continue") return self.rebase.GenericMergeOrRebaseAction("rebase", "continue")
} }

View File

@ -9,22 +9,56 @@ import (
"github.com/go-errors/errors" "github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands" "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) { type RebaseCommands struct {
todo, sha, err := c.GenerateGenericRebaseTodo(commits, index, "reword") *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 { if err != nil {
return nil, err 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 // we must ensure that we have at least two commits after the selected one
if len(commits) <= index+2 { if len(commits) <= index+2 {
// assuming they aren't picking the bottom commit // assuming they aren't picking the bottom commit
return errors.New(c.Tr.NoRoom) return errors.New(self.Tr.NoRoom)
} }
todo := "" todo := ""
@ -33,7 +67,7 @@ func (c *GitCommand) MoveCommitDown(commits []*models.Commit, index int) error {
todo = "pick " + commit.Sha + " " + commit.Name + "\n" + todo 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 { if err != nil {
return err return err
} }
@ -41,13 +75,13 @@ func (c *GitCommand) MoveCommitDown(commits []*models.Commit, index int) error {
return cmdObj.Run() return cmdObj.Run()
} }
func (c *GitCommand) InteractiveRebase(commits []*models.Commit, index int, action string) error { func (self *RebaseCommands) InteractiveRebase(commits []*models.Commit, index int, action string) error {
todo, sha, err := c.GenerateGenericRebaseTodo(commits, index, action) todo, sha, err := self.GenerateGenericRebaseTodo(commits, index, action)
if err != nil { if err != nil {
return err return err
} }
cmdObj, err := c.PrepareInteractiveRebaseCommand(sha, todo, true) cmdObj, err := self.PrepareInteractiveRebaseCommand(sha, todo, true)
if err != nil { if err != nil {
return err return err
} }
@ -58,24 +92,24 @@ func (c *GitCommand) InteractiveRebase(commits []*models.Commit, index int, acti
// PrepareInteractiveRebaseCommand returns the cmd for an interactive rebase // PrepareInteractiveRebaseCommand returns the cmd for an interactive rebase
// we tell git to run lazygit to edit the todo list, and we pass the client // 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 // 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() ex := oscommands.GetLazygitPath()
debug := "FALSE" debug := "FALSE"
if c.Debug { if self.Debug {
debug = "TRUE" debug = "TRUE"
} }
cmdStr := fmt.Sprintf("git rebase --interactive --autostash --keep-empty %s", baseSha) 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 gitSequenceEditor := ex
if todo == "" { if todo == "" {
gitSequenceEditor = "true" gitSequenceEditor = "true"
} else { } 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( cmdObj.AddEnvVars(
@ -94,18 +128,18 @@ func (c *GitCommand) PrepareInteractiveRebaseCommand(baseSha string, todo string
return cmdObj, nil 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 baseIndex := actionIndex + 1
if len(commits) <= baseIndex { if len(commits) <= baseIndex {
return "", "", errors.New(c.Tr.CannotRebaseOntoFirstCommit) return "", "", errors.New(self.Tr.CannotRebaseOntoFirstCommit)
} }
if action == "squash" || action == "fixup" { if action == "squash" || action == "fixup" {
baseIndex++ baseIndex++
if len(commits) <= 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 // AmendTo amends the given commit with whatever files are staged
func (c *GitCommand) AmendTo(sha string) error { func (self *RebaseCommands) AmendTo(sha string) error {
if err := c.CreateFixupCommit(sha); err != nil { if err := self.commit.CreateFixupCommit(sha); err != nil {
return err return err
} }
return c.SquashAllAboveFixupCommits(sha) return self.SquashAllAboveFixupCommits(sha)
} }
// EditRebaseTodo sets the action at a given index in the git-rebase-todo file // EditRebaseTodo sets the action at a given index in the git-rebase-todo file
func (c *GitCommand) EditRebaseTodo(index int, action string) error { func (self *RebaseCommands) EditRebaseTodo(index int, action string) error {
fileName := filepath.Join(c.DotGitDir, "rebase-merge/git-rebase-todo") fileName := filepath.Join(self.dotGitDir, "rebase-merge/git-rebase-todo")
bytes, err := ioutil.ReadFile(fileName) bytes, err := ioutil.ReadFile(fileName)
if err != nil { if err != nil {
return err return err
} }
content := strings.Split(string(bytes), "\n") 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 // 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 // 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) 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 // count lines that are not blank and are not comments
commitCount := 0 commitCount := 0
for _, line := range content { 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 // MoveTodoDown moves a rebase todo item down by one position
func (c *GitCommand) MoveTodoDown(index int) error { func (self *RebaseCommands) MoveTodoDown(index int) error {
fileName := filepath.Join(c.DotGitDir, "rebase-merge/git-rebase-todo") fileName := filepath.Join(self.dotGitDir, "rebase-merge/git-rebase-todo")
bytes, err := ioutil.ReadFile(fileName) bytes, err := ioutil.ReadFile(fileName)
if err != nil { if err != nil {
return err return err
} }
content := strings.Split(string(bytes), "\n") content := strings.Split(string(bytes), "\n")
commitCount := c.getTodoCommitCount(content) commitCount := self.getTodoCommitCount(content)
contentIndex := commitCount - 1 - index contentIndex := commitCount - 1 - index
rearrangedContent := append(content[0:contentIndex-1], content[contentIndex], content[contentIndex-1]) 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 // SquashAllAboveFixupCommits squashes all fixup! commits above the given one
func (c *GitCommand) SquashAllAboveFixupCommits(sha string) error { func (self *RebaseCommands) SquashAllAboveFixupCommits(sha string) error {
return c.runSkipEditorCommand( return self.runSkipEditorCommand(
fmt.Sprintf( fmt.Sprintf(
"git rebase --interactive --autostash --autosquash %s^", "git rebase --interactive --autostash --autosquash %s^",
sha, sha,
@ -199,8 +233,8 @@ func (c *GitCommand) SquashAllAboveFixupCommits(sha string) error {
} }
// BeginInteractiveRebaseForCommit starts an interactive rebase to edit the current // 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")` // commit and pick all others. After this you'll want to call `self.GenericMergeOrRebaseAction("rebase", "continue")`
func (c *GitCommand) BeginInteractiveRebaseForCommit(commits []*models.Commit, commitIndex int) error { func (self *RebaseCommands) BeginInteractiveRebaseForCommit(commits []*models.Commit, commitIndex int) error {
if len(commits)-1 < commitIndex { if len(commits)-1 < commitIndex {
return errors.New("index outside of range of commits") 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: // 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 // one where we handle the possibility of a credential request, and the other
// where we continue the rebase // where we continue the rebase
if c.UsingGpg() { if self.config.UsingGpg() {
return errors.New(c.Tr.DisabledForGPG) 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 { if err != nil {
return err return err
} }
cmdObj, err := c.PrepareInteractiveRebaseCommand(sha, todo, true) cmdObj, err := self.PrepareInteractiveRebaseCommand(sha, todo, true)
if err != nil { if err != nil {
return err return err
} }
@ -226,8 +260,8 @@ func (c *GitCommand) BeginInteractiveRebaseForCommit(commits []*models.Commit, c
} }
// RebaseBranch interactive rebases onto a branch // RebaseBranch interactive rebases onto a branch
func (c *GitCommand) RebaseBranch(branchName string) error { func (self *RebaseCommands) RebaseBranch(branchName string) error {
cmdObj, err := c.PrepareInteractiveRebaseCommand(branchName, "", false) cmdObj, err := self.PrepareInteractiveRebaseCommand(branchName, "", false)
if err != nil { if err != nil {
return err 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" // 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 // By default we skip the editor in the case where a commit will be made
func (c *GitCommand) GenericMergeOrRebaseAction(commandType string, command string) error { func (self *RebaseCommands) GenericMergeOrRebaseAction(commandType string, command string) error {
err := c.runSkipEditorCommand( err := self.runSkipEditorCommand(
fmt.Sprintf( fmt.Sprintf(
"git %s --%s", "git %s --%s",
commandType, commandType,
@ -249,25 +283,25 @@ func (c *GitCommand) GenericMergeOrRebaseAction(commandType string, command stri
if !strings.Contains(err.Error(), "no rebase in progress") { if !strings.Contains(err.Error(), "no rebase in progress") {
return err 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 // 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 // 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 // so that after the next successful rebase continue we can continue from where we left off
if commandType == "rebase" && command == "continue" && c.onSuccessfulContinue != nil { if commandType == "rebase" && command == "continue" && self.onSuccessfulContinue != nil {
f := c.onSuccessfulContinue f := self.onSuccessfulContinue
c.onSuccessfulContinue = nil self.onSuccessfulContinue = nil
return f() return f()
} }
if command == "abort" { if command == "abort" {
c.onSuccessfulContinue = nil self.onSuccessfulContinue = nil
} }
return nil return nil
} }
func (c *GitCommand) runSkipEditorCommand(command string) error { func (self *RebaseCommands) runSkipEditorCommand(command string) error {
cmdObj := c.Cmd.New(command) cmdObj := self.cmd.New(command)
lazyGitPath := oscommands.GetLazygitPath() lazyGitPath := oscommands.GetLazygitPath()
return cmdObj. return cmdObj.
AddEnvVars( AddEnvVars(
@ -278,3 +312,46 @@ func (c *GitCommand) runSkipEditorCommand(command string) error {
). ).
Run() 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()
}

View File

@ -5,6 +5,8 @@ import (
"testing" "testing"
"github.com/go-errors/errors" "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/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/utils" "github.com/jesseduffield/lazygit/pkg/utils"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -42,7 +44,7 @@ func TestGitCommandRebaseBranch(t *testing.T) {
for _, s := range scenarios { for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) { t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommandWithRunner(s.runner) 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 return "", nil
}) })
gitCmd := NewDummyGitCommandWithRunner(runner) gitCmd := NewDummyGitCommandWithRunner(runner)
err := gitCmd.runSkipEditorCommand(commandStr) err := gitCmd.Rebase.runSkipEditorCommand(commandStr)
assert.NoError(t, err) assert.NoError(t, err)
runner.CheckForMissingCalls() 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()
})
}
}

View File

@ -4,49 +4,60 @@ import (
"fmt" "fmt"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands" "github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/common"
) )
func (c *GitCommand) AddRemote(name string, url string) error { type RemoteCommands struct {
return c.Cmd. *common.Common
New(fmt.Sprintf("git remote add %s %s", c.Cmd.Quote(name), c.Cmd.Quote(url))).
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() Run()
} }
func (c *GitCommand) RemoveRemote(name string) error { func (self *RemoteCommands) RemoveRemote(name string) error {
return c.Cmd. return self.cmd.
New(fmt.Sprintf("git remote remove %s", c.Cmd.Quote(name))). New(fmt.Sprintf("git remote remove %s", self.cmd.Quote(name))).
Run() Run()
} }
func (c *GitCommand) RenameRemote(oldRemoteName string, newRemoteName string) error { func (self *RemoteCommands) RenameRemote(oldRemoteName string, newRemoteName string) error {
return c.Cmd. return self.cmd.
New(fmt.Sprintf("git remote rename %s %s", c.Cmd.Quote(oldRemoteName), c.Cmd.Quote(newRemoteName))). New(fmt.Sprintf("git remote rename %s %s", self.cmd.Quote(oldRemoteName), self.cmd.Quote(newRemoteName))).
Run() Run()
} }
func (c *GitCommand) UpdateRemoteUrl(remoteName string, updatedUrl string) error { func (self *RemoteCommands) UpdateRemoteUrl(remoteName string, updatedUrl string) error {
return c.Cmd. return self.cmd.
New(fmt.Sprintf("git remote set-url %s %s", c.Cmd.Quote(remoteName), c.Cmd.Quote(updatedUrl))). New(fmt.Sprintf("git remote set-url %s %s", self.cmd.Quote(remoteName), self.cmd.Quote(updatedUrl))).
Run() Run()
} }
func (c *GitCommand) DeleteRemoteBranch(remoteName string, branchName string, promptUserForCredential func(string) string) error { func (self *RemoteCommands) DeleteRemoteBranch(remoteName string, branchName string) error {
command := fmt.Sprintf("git push %s --delete %s", c.Cmd.Quote(remoteName), c.Cmd.Quote(branchName)) command := fmt.Sprintf("git push %s --delete %s", self.cmd.Quote(remoteName), self.cmd.Quote(branchName))
cmdObj := c.Cmd. return self.cmd.New(command).PromptOnCredentialRequest().Run()
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)
} }
// CheckRemoteBranchExists Returns remote branch // CheckRemoteBranchExists Returns remote branch
func (c *GitCommand) CheckRemoteBranchExists(branchName string) bool { func (self *RemoteCommands) CheckRemoteBranchExists(branchName string) bool {
_, err := c.Cmd. _, err := self.cmd.
New( New(
fmt.Sprintf("git show-ref --verify -- refs/remotes/origin/%s", fmt.Sprintf("git show-ref --verify -- refs/remotes/origin/%s",
c.Cmd.Quote(branchName), self.cmd.Quote(branchName),
), ),
). ).
DontLog(). DontLog().
@ -54,8 +65,3 @@ func (c *GitCommand) CheckRemoteBranchExists(branchName string) bool {
return err == nil return err == nil
} }
// GetRemoteURL returns current repo remote url
func (c *GitCommand) GetRemoteURL() string {
return c.GitConfig.Get("remote.origin.url")
}

View File

@ -5,59 +5,91 @@ import (
"github.com/jesseduffield/lazygit/pkg/commands/loaders" "github.com/jesseduffield/lazygit/pkg/commands/loaders"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands" "github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/common"
) )
// StashDo modify stash type StashCommands struct {
func (c *GitCommand) StashDo(index int, method string) error { *common.Common
return c.Cmd.New(fmt.Sprintf("git stash %s stash@{%d}", method, index)).Run()
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 // TODO: before calling this, check if there is anything to save
func (c *GitCommand) StashSave(message string) error { func (self *StashCommands) Save(message string) error {
return c.Cmd.New("git stash save " + c.OSCommand.Quote(message)).Run() return self.cmd.New("git stash save " + self.cmd.Quote(message)).Run()
} }
func (c *GitCommand) ShowStashEntryCmdObj(index int) oscommands.ICmdObj { func (self *StashCommands) 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) 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 // 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 // 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 return err
} }
if err := c.StashSave(message); err != nil { if err := self.Save(message); err != nil {
return err 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 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 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 return err
} }
// if you had staged an untracked file, that will now appear as 'AD' in git status // 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 // 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. // now safely stashed, we need to remove it.
files := loaders. files := self.fileLoader.
NewFileLoader(c.Common, c.Cmd, c.GitConfig).
GetStatusFiles(loaders.GetStatusFileOptions{}) GetStatusFiles(loaders.GetStatusFileOptions{})
for _, file := range files { for _, file := range files {
if file.ShortStatus == "AD" { 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 return err
} }
} }

View File

@ -7,12 +7,30 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func TestGitCommandStashDo(t *testing.T) { func TestGitCommandStashDrop(t *testing.T) {
runner := oscommands.NewFakeRunner(t). runner := oscommands.NewFakeRunner(t).
ExpectGitArgs([]string{"stash", "drop", "stash@{1}"}, "", nil) ExpectGitArgs([]string{"stash", "drop", "stash@{1}"}, "", nil)
gitCmd := NewDummyGitCommandWithRunner(runner) 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() runner.CheckForMissingCalls()
} }
@ -21,7 +39,7 @@ func TestGitCommandStashSave(t *testing.T) {
ExpectGitArgs([]string{"stash", "save", "A stash message"}, "", nil) ExpectGitArgs([]string{"stash", "save", "A stash message"}, "", nil)
gitCmd := NewDummyGitCommandWithRunner(runner) gitCmd := NewDummyGitCommandWithRunner(runner)
assert.NoError(t, gitCmd.StashSave("A stash message")) assert.NoError(t, gitCmd.Stash.Save("A stash message"))
runner.CheckForMissingCalls() runner.CheckForMissingCalls()
} }
@ -52,7 +70,7 @@ func TestGitCommandShowStashEntryCmdObj(t *testing.T) {
t.Run(s.testName, func(t *testing.T) { t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommand() gitCmd := NewDummyGitCommand()
gitCmd.UserConfig.Git.DiffContextSize = s.contextSize 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) assert.Equal(t, s.expected, cmdStr)
}) })
} }

View File

@ -4,20 +4,43 @@ import (
"path/filepath" "path/filepath"
gogit "github.com/jesseduffield/go-git/v5" 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/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 // RebaseMode returns "" for non-rebase mode, "normal" for normal rebase
// and "interactive" for interactive rebase // and "interactive" for interactive rebase
func (c *GitCommand) RebaseMode() (enums.RebaseMode, error) { func (self *StatusCommands) RebaseMode() (enums.RebaseMode, error) {
exists, err := c.OSCommand.FileExists(filepath.Join(c.DotGitDir, "rebase-apply")) exists, err := self.osCommand.FileExists(filepath.Join(self.dotGitDir, "rebase-apply"))
if err != nil { if err != nil {
return enums.REBASE_MODE_NONE, err return enums.REBASE_MODE_NONE, err
} }
if exists { if exists {
return enums.REBASE_MODE_NORMAL, nil 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 { if exists {
return enums.REBASE_MODE_INTERACTIVE, err return enums.REBASE_MODE_INTERACTIVE, err
} else { } else {
@ -25,12 +48,12 @@ func (c *GitCommand) RebaseMode() (enums.RebaseMode, error) {
} }
} }
func (c *GitCommand) WorkingTreeState() enums.RebaseMode { func (self *StatusCommands) WorkingTreeState() enums.RebaseMode {
rebaseMode, _ := c.RebaseMode() rebaseMode, _ := self.RebaseMode()
if rebaseMode != enums.REBASE_MODE_NONE { if rebaseMode != enums.REBASE_MODE_NONE {
return enums.REBASE_MODE_REBASING return enums.REBASE_MODE_REBASING
} }
merging, _ := c.IsInMergeState() merging, _ := self.IsInMergeState()
if merging { if merging {
return enums.REBASE_MODE_MERGING return enums.REBASE_MODE_MERGING
} }
@ -38,12 +61,12 @@ func (c *GitCommand) WorkingTreeState() enums.RebaseMode {
} }
// IsInMergeState states whether we are still mid-merge // IsInMergeState states whether we are still mid-merge
func (c *GitCommand) IsInMergeState() (bool, error) { func (self *StatusCommands) IsInMergeState() (bool, error) {
return c.OSCommand.FileExists(filepath.Join(c.DotGitDir, "MERGE_HEAD")) 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 // 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 return err == gogit.ErrIsBareRepository
} }

View File

@ -10,6 +10,7 @@ import (
"github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands" "github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/common"
) )
// .gitmodules looks like this: // .gitmodules looks like this:
@ -17,7 +18,22 @@ import (
// path = blah/mysubmodule // path = blah/mysubmodule
// url = git@github.com:subbo.git // 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") file, err := os.Open(".gitmodules")
if err != nil { if err != nil {
if os.IsNotExist(err) { if os.IsNotExist(err) {
@ -63,36 +79,36 @@ func (c *GitCommand) GetSubmoduleConfigs() ([]*models.SubmoduleConfig, error) {
return configs, nil 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 // 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 // because the intention here is to have no dirty worktree state
if _, err := os.Stat(submodule.Path); os.IsNotExist(err) { 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 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 { func (self *SubmoduleCommands) Reset(submodule *models.SubmoduleConfig) error {
return c.Cmd.New("git submodule update --init --force -- " + c.Cmd.Quote(submodule.Path)).Run() 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 // 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 // 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 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 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 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 // 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 { func (self *SubmoduleCommands) Add(name string, path string, url string) error {
return c.Cmd. return self.cmd.
New( New(
fmt.Sprintf( fmt.Sprintf(
"git submodule add --force --name %s -- %s %s ", "git submodule add --force --name %s -- %s %s ",
c.Cmd.Quote(name), self.cmd.Quote(name),
c.Cmd.Quote(url), self.cmd.Quote(url),
c.Cmd.Quote(path), self.cmd.Quote(path),
)). )).
Run() 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 // 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 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 err
} }
return nil return nil
} }
func (c *GitCommand) SubmoduleInit(path string) error { func (self *SubmoduleCommands) Init(path string) error {
return c.Cmd.New("git submodule init -- " + c.Cmd.Quote(path)).Run() return self.cmd.New("git submodule init -- " + self.cmd.Quote(path)).Run()
} }
func (c *GitCommand) SubmoduleUpdate(path string) error { func (self *SubmoduleCommands) Update(path string) error {
return c.Cmd.New("git submodule update --init -- " + c.Cmd.Quote(path)).Run() return self.cmd.New("git submodule update --init -- " + self.cmd.Quote(path)).Run()
} }
func (c *GitCommand) SubmoduleBulkInitCmdObj() oscommands.ICmdObj { func (self *SubmoduleCommands) BulkInitCmdObj() oscommands.ICmdObj {
return c.Cmd.New("git submodule init") return self.cmd.New("git submodule init")
} }
func (c *GitCommand) SubmoduleBulkUpdateCmdObj() oscommands.ICmdObj { func (self *SubmoduleCommands) BulkUpdateCmdObj() oscommands.ICmdObj {
return c.Cmd.New("git submodule update") return self.cmd.New("git submodule update")
} }
func (c *GitCommand) SubmoduleForceBulkUpdateCmdObj() oscommands.ICmdObj { func (self *SubmoduleCommands) ForceBulkUpdateCmdObj() oscommands.ICmdObj {
return c.Cmd.New("git submodule update --force") return self.cmd.New("git submodule update --force")
} }
func (c *GitCommand) SubmoduleBulkDeinitCmdObj() oscommands.ICmdObj { func (self *SubmoduleCommands) BulkDeinitCmdObj() oscommands.ICmdObj {
return c.Cmd.New("git submodule deinit --all --force") 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 { for _, submodule := range submodules {
if err := c.SubmoduleStash(submodule); err != nil { if err := self.Stash(submodule); err != nil {
return err return err
} }
} }
return c.SubmoduleUpdateAll() return self.UpdateAll()
} }

View File

@ -5,8 +5,25 @@ import (
"github.com/go-errors/errors" "github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands" "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 // Push pushes to a branch
type PushOpts struct { type PushOpts struct {
Force bool Force bool
@ -15,7 +32,7 @@ type PushOpts struct {
SetUpstream bool SetUpstream bool
} }
func (c *GitCommand) PushCmdObj(opts PushOpts) (oscommands.ICmdObj, error) { func (self *SyncCommands) PushCmdObj(opts PushOpts) (oscommands.ICmdObj, error) {
cmdStr := "git push" cmdStr := "git push"
if opts.Force { if opts.Force {
@ -27,71 +44,62 @@ func (c *GitCommand) PushCmdObj(opts PushOpts) (oscommands.ICmdObj, error) {
} }
if opts.UpstreamRemote != "" { if opts.UpstreamRemote != "" {
cmdStr += " " + c.OSCommand.Quote(opts.UpstreamRemote) cmdStr += " " + self.cmd.Quote(opts.UpstreamRemote)
} }
if opts.UpstreamBranch != "" { if opts.UpstreamBranch != "" {
if opts.UpstreamRemote == "" { 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 return cmdObj, nil
} }
func (c *GitCommand) Push(opts PushOpts, promptUserForCredential func(string) string) error { func (self *SyncCommands) Push(opts PushOpts) error {
cmdObj, err := c.PushCmdObj(opts) cmdObj, err := self.PushCmdObj(opts)
if err != nil { if err != nil {
return err return err
} }
return c.DetectUnamePass(cmdObj, promptUserForCredential) return cmdObj.Run()
} }
type FetchOptions struct { type FetchOptions struct {
PromptUserForCredential func(string) string Background bool
RemoteName string RemoteName string
BranchName string BranchName string
} }
// Fetch fetch git repo // Fetch fetch git repo
func (c *GitCommand) Fetch(opts FetchOptions) error { func (self *SyncCommands) Fetch(opts FetchOptions) error {
cmdStr := "git fetch" cmdStr := "git fetch"
if opts.RemoteName != "" { 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 != "" { 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) cmdObj := self.cmd.New(cmdStr)
userInitiated := opts.PromptUserForCredential != nil if opts.Background {
if !userInitiated { cmdObj.DontLog().FailOnCredentialRequest()
cmdObj.DontLog() } else {
cmdObj.PromptOnCredentialRequest()
} }
return c.DetectUnamePass(cmdObj, func(question string) string { return cmdObj.Run()
if userInitiated {
return opts.PromptUserForCredential(question)
}
return "\n"
})
} }
type PullOptions struct { type PullOptions struct {
PromptUserForCredential func(string) string
RemoteName string RemoteName string
BranchName string BranchName string
FastForwardOnly bool FastForwardOnly bool
} }
func (c *GitCommand) Pull(opts PullOptions) error { func (self *SyncCommands) Pull(opts PullOptions) error {
if opts.PromptUserForCredential == nil {
return errors.New("PromptUserForCredential is required")
}
cmdStr := "git pull --no-edit" cmdStr := "git pull --no-edit"
if opts.FastForwardOnly { if opts.FastForwardOnly {
@ -99,26 +107,23 @@ func (c *GitCommand) Pull(opts PullOptions) error {
} }
if opts.RemoteName != "" { 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 != "" { 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 // setting GIT_SEQUENCE_EDITOR to ':' as a way of skipping it, in case the user
// has 'pull.rebase = interactive' configured. // has 'pull.rebase = interactive' configured.
cmdObj := c.Cmd.New(cmdStr).AddEnvVars("GIT_SEQUENCE_EDITOR=:") return self.cmd.New(cmdStr).AddEnvVars("GIT_SEQUENCE_EDITOR=:").PromptOnCredentialRequest().Run()
return c.DetectUnamePass(cmdObj, opts.PromptUserForCredential)
} }
func (c *GitCommand) FastForward(branchName string, remoteName string, remoteBranchName string, promptUserForCredential func(string) string) error { func (self *SyncCommands) FastForward(branchName string, remoteName string, remoteBranchName string) error {
cmdStr := fmt.Sprintf("git fetch %s %s:%s", c.OSCommand.Quote(remoteName), c.OSCommand.Quote(remoteBranchName), c.OSCommand.Quote(branchName)) cmdStr := fmt.Sprintf("git fetch %s %s:%s", self.cmd.Quote(remoteName), self.cmd.Quote(remoteBranchName), self.cmd.Quote(branchName))
cmdObj := c.Cmd.New(cmdStr) return self.cmd.New(cmdStr).PromptOnCredentialRequest().Run()
return c.DetectUnamePass(cmdObj, promptUserForCredential)
} }
func (c *GitCommand) FetchRemote(remoteName string, promptUserForCredential func(string) string) error { func (self *SyncCommands) FetchRemote(remoteName string) error {
cmdStr := fmt.Sprintf("git fetch %s", c.OSCommand.Quote(remoteName)) cmdStr := fmt.Sprintf("git fetch %s", self.cmd.Quote(remoteName))
cmdObj := c.Cmd.New(cmdStr) return self.cmd.New(cmdStr).PromptOnCredentialRequest().Run()
return c.DetectUnamePass(cmdObj, promptUserForCredential)
} }

View File

@ -87,7 +87,7 @@ func TestGitCommandPush(t *testing.T) {
for _, s := range scenarios { for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) { t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommandWithRunner(oscommands.NewFakeRunner(t)) gitCmd := NewDummyGitCommandWithRunner(oscommands.NewFakeRunner(t))
s.test(gitCmd.PushCmdObj(s.opts)) s.test(gitCmd.Sync.PushCmdObj(s.opts))
}) })
} }
} }

View File

@ -2,22 +2,36 @@ package commands
import ( import (
"fmt" "fmt"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/common"
) )
func (c *GitCommand) CreateLightweightTag(tagName string, commitSha string) error { type TagCommands struct {
return c.Cmd.New(fmt.Sprintf("git tag -- %s %s", c.OSCommand.Quote(tagName), commitSha)).Run() *common.Common
cmd oscommands.ICmdObjBuilder
} }
func (c *GitCommand) CreateAnnotatedTag(tagName, commitSha, msg string) error { func NewTagCommands(common *common.Common, cmd oscommands.ICmdObjBuilder) *TagCommands {
return c.Cmd.New(fmt.Sprintf("git tag %s %s -m %s", tagName, commitSha, c.OSCommand.Quote(msg))).Run() return &TagCommands{
Common: common,
cmd: cmd,
}
} }
func (c *GitCommand) DeleteTag(tagName string) error { func (self *TagCommands) CreateLightweight(tagName string, commitSha string) error {
return c.Cmd.New(fmt.Sprintf("git tag -d %s", c.OSCommand.Quote(tagName))).Run() 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 { func (self *TagCommands) CreateAnnotated(tagName, commitSha, msg string) error {
cmdStr := fmt.Sprintf("git push %s %s", c.OSCommand.Quote(remoteName), c.OSCommand.Quote(tagName)) return self.cmd.New(fmt.Sprintf("git tag %s %s -m %s", tagName, commitSha, self.cmd.Quote(msg))).Run()
cmdObj := c.Cmd.New(cmdStr) }
return c.DetectUnamePass(cmdObj, promptUserForCredential)
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()
} }

View File

@ -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()
}

View File

@ -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))
})
}
}

View File

@ -31,7 +31,7 @@ func (gui *Gui) branchesRenderToMain() error {
if branch == nil { if branch == nil {
task = NewRenderStringTask(gui.Tr.NoBranchesThisRepo) task = NewRenderStringTask(gui.Tr.NoBranchesThisRepo)
} else { } else {
cmdObj := gui.GitCommand.GetBranchGraphCmdObj(branch.Name) cmdObj := gui.GitCommand.Branch.GetGraphCmdObj(branch.Name)
task = NewRunPtyTask(cmdObj.GetCmd()) task = NewRunPtyTask(cmdObj.GetCmd())
} }
@ -103,7 +103,7 @@ func (gui *Gui) handleCopyPullRequestURLPress() error {
branch := gui.getSelectedBranch() branch := gui.getSelectedBranch()
branchExistsOnRemote := gui.GitCommand.CheckRemoteBranchExists(branch.Name) branchExistsOnRemote := gui.GitCommand.Remote.CheckRemoteBranchExists(branch.Name)
if !branchExistsOnRemote { if !branchExistsOnRemote {
return gui.surfaceError(errors.New(gui.Tr.NoBranchOnRemote)) return gui.surfaceError(errors.New(gui.Tr.NoBranchOnRemote))
@ -146,7 +146,7 @@ func (gui *Gui) handleForceCheckout() error {
prompt: message, prompt: message,
handleConfirm: func() error { handleConfirm: func() error {
gui.logAction(gui.Tr.Actions.ForceCheckoutBranch) 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) _ = gui.surfaceError(err)
} }
return gui.refreshSidePanels(refreshOptions{mode: ASYNC}) return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
@ -176,7 +176,7 @@ func (gui *Gui) handleCheckoutRef(ref string, options handleCheckoutRefOptions)
} }
return gui.WithWaitingStatus(waitingStatus, func() error { 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 // 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") { 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, title: gui.Tr.AutoStashTitle,
prompt: gui.Tr.AutoStashPrompt, prompt: gui.Tr.AutoStashPrompt,
handleConfirm: func() error { 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) 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) return gui.surfaceError(err)
} }
onSuccess() 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 { if err := gui.refreshSidePanels(refreshOptions{mode: BLOCK_UI}); err != nil {
return err return err
} }
@ -254,7 +254,7 @@ func (gui *Gui) createNewBranchWithName(newBranchName string) error {
return nil 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) return gui.surfaceError(err)
} }
@ -298,7 +298,7 @@ func (gui *Gui) deleteNamedBranch(selectedBranch *models.Branch, force bool) err
prompt: message, prompt: message,
handleConfirm: func() error { handleConfirm: func() error {
gui.logAction(gui.Tr.Actions.DeleteBranch) 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() errMessage := err.Error()
if !force && strings.Contains(errMessage, "git branch -D ") { if !force && strings.Contains(errMessage, "git branch -D ") {
return gui.deleteNamedBranch(selectedBranch, true) return gui.deleteNamedBranch(selectedBranch, true)
@ -315,7 +315,7 @@ func (gui *Gui) mergeBranchIntoCheckedOutBranch(branchName string) error {
return err 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") 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 checkedOutBranchName := gui.getCheckedOutBranch().Name
@ -335,7 +335,7 @@ func (gui *Gui) mergeBranchIntoCheckedOutBranch(branchName string) error {
prompt: prompt, prompt: prompt,
handleConfirm: func() error { handleConfirm: func() error {
gui.logAction(gui.Tr.Actions.Merge) 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) return gui.handleGenericMergeCommandResult(err)
}, },
}) })
@ -377,7 +377,7 @@ func (gui *Gui) handleRebaseOntoBranch(selectedBranchName string) error {
prompt: prompt, prompt: prompt,
handleConfirm: func() error { handleConfirm: func() error {
gui.logAction(gui.Tr.Actions.RebaseBranch) gui.logAction(gui.Tr.Actions.RebaseBranch)
err := gui.GitCommand.RebaseBranch(selectedBranchName) err := gui.GitCommand.Rebase.RebaseBranch(selectedBranchName)
return gui.handleGenericMergeCommandResult(err) return gui.handleGenericMergeCommandResult(err)
}, },
}) })
@ -396,7 +396,7 @@ func (gui *Gui) handleFastForward() error {
return gui.createErrorPanel(gui.Tr.FwdCommitsToPush) return gui.createErrorPanel(gui.Tr.FwdCommitsToPush)
} }
upstream, err := gui.GitCommand.GetUpstreamForBranch(branch.Name) upstream, err := gui.GitCommand.Branch.GetUpstream(branch.Name)
if err != nil { if err != nil {
return gui.surfaceError(err) return gui.surfaceError(err)
} }
@ -421,7 +421,7 @@ func (gui *Gui) handleFastForward() error {
_ = gui.pullWithLock(PullFilesOptions{action: action, FastForwardOnly: true}) _ = gui.pullWithLock(PullFilesOptions{action: action, FastForwardOnly: true})
} else { } else {
gui.logAction(action) 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.handleCredentialsPopup(err)
_ = gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []RefreshableView{BRANCHES}}) _ = gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []RefreshableView{BRANCHES}})
} }
@ -450,7 +450,7 @@ func (gui *Gui) handleRenameBranch() error {
initialContent: branch.Name, initialContent: branch.Name,
handleConfirm: func(newBranchName string) error { handleConfirm: func(newBranchName string) error {
gui.logAction(gui.Tr.Actions.RenameBranch) 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) return gui.surfaceError(err)
} }
@ -519,7 +519,7 @@ func (gui *Gui) handleNewBranchOffCurrentItem() error {
initialContent: prefilledName, initialContent: prefilledName,
handleConfirm: func(response string) error { handleConfirm: func(response string) error {
gui.logAction(gui.Tr.Actions.CreateBranch) 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 return err
} }

View File

@ -149,7 +149,7 @@ func (gui *Gui) HandlePasteCommits() error {
handleConfirm: func() error { handleConfirm: func() error {
return gui.WithWaitingStatus(gui.Tr.CherryPickingStatus, func() error { return gui.WithWaitingStatus(gui.Tr.CherryPickingStatus, func() error {
gui.logAction(gui.Tr.Actions.CherryPick) 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) return gui.handleGenericMergeCommandResult(err)
}) })
}, },

View File

@ -45,7 +45,7 @@ func (gui *Gui) commitFilesRenderToMain() error {
to := gui.State.CommitFileManager.GetParent() to := gui.State.CommitFileManager.GetParent()
from, reverse := gui.getFromAndReverseArgsForDiff(to) 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()) task := NewRunPtyTask(cmdObj.GetCmd())
return gui.refreshMainViews(refreshMainOpts{ return gui.refreshMainViews(refreshMainOpts{
@ -64,7 +64,7 @@ func (gui *Gui) handleCheckoutCommitFile() error {
} }
gui.logAction(gui.Tr.Actions.CheckoutFile) 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) return gui.surfaceError(err)
} }
@ -84,7 +84,7 @@ func (gui *Gui) handleDiscardOldFileChange() error {
handleConfirm: func() error { handleConfirm: func() error {
return gui.WithWaitingStatus(gui.Tr.RebasingStatus, func() error { return gui.WithWaitingStatus(gui.Tr.RebasingStatus, func() error {
gui.logAction(gui.Tr.Actions.DiscardOldFileChange) 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 { if err := gui.handleGenericMergeCommandResult(err); err != nil {
return err return err
} }
@ -145,7 +145,7 @@ func (gui *Gui) handleToggleFileForPatch() error {
} }
toggleTheFile := func() error { toggleTheFile := func() error {
if !gui.GitCommand.PatchManager.Active() { if !gui.GitCommand.Patch.PatchManager.Active() {
if err := gui.startPatchManager(); err != nil { if err := gui.startPatchManager(); err != nil {
return err 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, // if there is any file that hasn't been fully added we'll fully add everything,
// otherwise we'll remove everything // otherwise we'll remove everything
adding := node.AnyFile(func(file *models.CommitFile) bool { 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 { err := node.ForEachFile(func(file *models.CommitFile) error {
if adding { if adding {
return gui.GitCommand.PatchManager.AddFileWhole(file.Name) return gui.GitCommand.Patch.PatchManager.AddFileWhole(file.Name)
} else { } 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) return gui.surfaceError(err)
} }
if gui.GitCommand.PatchManager.IsEmpty() { if gui.GitCommand.Patch.PatchManager.IsEmpty() {
gui.GitCommand.PatchManager.Reset() gui.GitCommand.Patch.PatchManager.Reset()
} }
return gui.postRefreshUpdate(gui.State.Contexts.CommitFiles) 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{ return gui.ask(askOpts{
title: gui.Tr.DiscardPatch, title: gui.Tr.DiscardPatch,
prompt: gui.Tr.DiscardPatchConfirm, prompt: gui.Tr.DiscardPatchConfirm,
handleConfirm: func() error { handleConfirm: func() error {
gui.GitCommand.PatchManager.Reset() gui.GitCommand.Patch.PatchManager.Reset()
return toggleTheFile() return toggleTheFile()
}, },
}) })
@ -196,7 +196,7 @@ func (gui *Gui) startPatchManager() error {
to := gui.State.Panels.CommitFiles.refName to := gui.State.Panels.CommitFiles.refName
from, reverse := gui.getFromAndReverseArgsForDiff(to) from, reverse := gui.getFromAndReverseArgsForDiff(to)
gui.GitCommand.PatchManager.Start(from, to, reverse, canRebase) gui.GitCommand.Patch.PatchManager.Start(from, to, reverse, canRebase)
return nil return nil
} }
@ -215,7 +215,7 @@ func (gui *Gui) enterCommitFile(opts OnFocusOpts) error {
} }
enterTheFile := func() error { enterTheFile := func() error {
if !gui.GitCommand.PatchManager.Active() { if !gui.GitCommand.Patch.PatchManager.Active() {
if err := gui.startPatchManager(); err != nil { if err := gui.startPatchManager(); err != nil {
return err return err
} }
@ -224,13 +224,13 @@ func (gui *Gui) enterCommitFile(opts OnFocusOpts) error {
return gui.pushContext(gui.State.Contexts.PatchBuilding, opts) 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{ return gui.ask(askOpts{
title: gui.Tr.DiscardPatch, title: gui.Tr.DiscardPatch,
prompt: gui.Tr.DiscardPatchConfirm, prompt: gui.Tr.DiscardPatchConfirm,
handlersManageFocus: true, handlersManageFocus: true,
handleConfirm: func() error { handleConfirm: func() error {
gui.GitCommand.PatchManager.Reset() gui.GitCommand.Patch.PatchManager.Reset()
return enterTheFile() return enterTheFile()
}, },
handleClose: func() error { handleClose: func() error {

View File

@ -24,7 +24,7 @@ func (gui *Gui) handleCommitConfirm() error {
flags = append(flags, "--signoff") 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.logAction(gui.Tr.Actions.Commit)
_ = gui.returnFromContext() _ = gui.returnFromContext()

View File

@ -45,7 +45,7 @@ func (gui *Gui) branchCommitsRenderToMain() error {
if commit == nil { if commit == nil {
task = NewRenderStringTask(gui.Tr.NoCommitsThisBranch) task = NewRenderStringTask(gui.Tr.NoCommitsThisBranch)
} else { } 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()) task = NewRunPtyTask(cmdObj.GetCmd())
} }
@ -173,7 +173,7 @@ func (gui *Gui) handleCommitSquashDown() error {
handleConfirm: func() error { handleConfirm: func() error {
return gui.WithWaitingStatus(gui.Tr.SquashingStatus, func() error { return gui.WithWaitingStatus(gui.Tr.SquashingStatus, func() error {
gui.logAction(gui.Tr.Actions.SquashCommitDown) 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) return gui.handleGenericMergeCommandResult(err)
}) })
}, },
@ -203,14 +203,14 @@ func (gui *Gui) handleCommitFixup() error {
handleConfirm: func() error { handleConfirm: func() error {
return gui.WithWaitingStatus(gui.Tr.FixingStatus, func() error { return gui.WithWaitingStatus(gui.Tr.FixingStatus, func() error {
gui.logAction(gui.Tr.Actions.FixupCommit) 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) return gui.handleGenericMergeCommandResult(err)
}) })
}, },
}) })
} }
func (gui *Gui) handleRenameCommit() error { func (gui *Gui) handleRewordCommit() error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok { if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err return err
} }
@ -224,7 +224,7 @@ func (gui *Gui) handleRenameCommit() error {
} }
if gui.State.Panels.Commits.SelectedLineIdx != 0 { if gui.State.Panels.Commits.SelectedLineIdx != 0 {
return gui.createErrorPanel(gui.Tr.OnlyRenameTopCommit) return gui.createErrorPanel(gui.Tr.OnlyRewordTopCommit)
} }
commit := gui.getSelectedLocalCommit() commit := gui.getSelectedLocalCommit()
@ -232,17 +232,17 @@ func (gui *Gui) handleRenameCommit() error {
return nil return nil
} }
message, err := gui.GitCommand.GetCommitMessage(commit.Sha) message, err := gui.GitCommand.Commit.GetCommitMessage(commit.Sha)
if err != nil { if err != nil {
return gui.surfaceError(err) return gui.surfaceError(err)
} }
return gui.prompt(promptOpts{ return gui.prompt(promptOpts{
title: gui.Tr.LcRenameCommit, title: gui.Tr.LcRewordCommit,
initialContent: message, initialContent: message,
handleConfirm: func(response string) error { handleConfirm: func(response string) error {
gui.logAction(gui.Tr.Actions.RewordCommit) 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) return gui.surfaceError(err)
} }
@ -265,7 +265,7 @@ func (gui *Gui) handleRenameCommitEditor() error {
} }
gui.logAction(gui.Tr.Actions.RewordCommit) 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 { if err != nil {
return gui.surfaceError(err) return gui.surfaceError(err)
} }
@ -299,7 +299,7 @@ func (gui *Gui) handleMidRebaseCommand(action string) (bool, error) {
false, 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) return false, gui.surfaceError(err)
} }
@ -325,7 +325,7 @@ func (gui *Gui) handleCommitDelete() error {
handleConfirm: func() error { handleConfirm: func() error {
return gui.WithWaitingStatus(gui.Tr.DeletingStatus, func() error { return gui.WithWaitingStatus(gui.Tr.DeletingStatus, func() error {
gui.logAction(gui.Tr.Actions.DropCommit) 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) return gui.handleGenericMergeCommandResult(err)
}) })
}, },
@ -349,7 +349,7 @@ func (gui *Gui) handleCommitMoveDown() error {
gui.logAction(gui.Tr.Actions.MoveCommitDown) gui.logAction(gui.Tr.Actions.MoveCommitDown)
gui.logCommand(fmt.Sprintf("Moving commit %s down", selectedCommit.ShortSha()), false) 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) return gui.surfaceError(err)
} }
gui.State.Panels.Commits.SelectedLineIdx++ gui.State.Panels.Commits.SelectedLineIdx++
@ -358,7 +358,7 @@ func (gui *Gui) handleCommitMoveDown() error {
return gui.WithWaitingStatus(gui.Tr.MovingStatus, func() error { return gui.WithWaitingStatus(gui.Tr.MovingStatus, func() error {
gui.logAction(gui.Tr.Actions.MoveCommitDown) 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 { if err == nil {
gui.State.Panels.Commits.SelectedLineIdx++ gui.State.Panels.Commits.SelectedLineIdx++
} }
@ -386,7 +386,7 @@ func (gui *Gui) handleCommitMoveUp() error {
false, false,
) )
if err := gui.GitCommand.MoveTodoDown(index - 1); err != nil { if err := gui.GitCommand.Rebase.MoveTodoDown(index - 1); err != nil {
return gui.surfaceError(err) return gui.surfaceError(err)
} }
gui.State.Panels.Commits.SelectedLineIdx-- gui.State.Panels.Commits.SelectedLineIdx--
@ -395,7 +395,7 @@ func (gui *Gui) handleCommitMoveUp() error {
return gui.WithWaitingStatus(gui.Tr.MovingStatus, func() error { return gui.WithWaitingStatus(gui.Tr.MovingStatus, func() error {
gui.logAction(gui.Tr.Actions.MoveCommitUp) 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 { if err == nil {
gui.State.Panels.Commits.SelectedLineIdx-- gui.State.Panels.Commits.SelectedLineIdx--
} }
@ -418,7 +418,7 @@ func (gui *Gui) handleCommitEdit() error {
return gui.WithWaitingStatus(gui.Tr.RebasingStatus, func() error { return gui.WithWaitingStatus(gui.Tr.RebasingStatus, func() error {
gui.logAction(gui.Tr.Actions.EditCommit) 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) return gui.handleGenericMergeCommandResult(err)
}) })
} }
@ -434,7 +434,7 @@ func (gui *Gui) handleCommitAmendTo() error {
handleConfirm: func() error { handleConfirm: func() error {
return gui.WithWaitingStatus(gui.Tr.AmendingStatus, func() error { return gui.WithWaitingStatus(gui.Tr.AmendingStatus, func() error {
gui.logAction(gui.Tr.Actions.AmendCommit) 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) return gui.handleGenericMergeCommandResult(err)
}) })
}, },
@ -470,7 +470,7 @@ func (gui *Gui) handleCommitRevert() error {
return gui.createRevertMergeCommitMenu(commit) return gui.createRevertMergeCommitMenu(commit)
} else { } else {
gui.logAction(gui.Tr.Actions.RevertCommit) 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.surfaceError(err)
} }
return gui.afterRevertCommit() return gui.afterRevertCommit()
@ -481,7 +481,7 @@ func (gui *Gui) createRevertMergeCommitMenu(commit *models.Commit) error {
menuItems := make([]*menuItem, len(commit.Parents)) menuItems := make([]*menuItem, len(commit.Parents))
for i, parentSha := range commit.Parents { for i, parentSha := range commit.Parents {
i := i i := i
message, err := gui.GitCommand.GetCommitMessageFirstLine(parentSha) message, err := gui.GitCommand.Commit.GetCommitMessageFirstLine(parentSha)
if err != nil { if err != nil {
return gui.surfaceError(err) return gui.surfaceError(err)
} }
@ -491,7 +491,7 @@ func (gui *Gui) createRevertMergeCommitMenu(commit *models.Commit) error {
onPress: func() error { onPress: func() error {
parentNumber := i + 1 parentNumber := i + 1
gui.logAction(gui.Tr.Actions.RevertCommit) 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.surfaceError(err)
} }
return gui.afterRevertCommit() return gui.afterRevertCommit()
@ -538,7 +538,7 @@ func (gui *Gui) handleCreateFixupCommit() error {
prompt: prompt, prompt: prompt,
handleConfirm: func() error { handleConfirm: func() error {
gui.logAction(gui.Tr.Actions.CreateFixupCommit) 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) return gui.surfaceError(err)
} }
@ -570,7 +570,7 @@ func (gui *Gui) handleSquashAllAboveFixupCommits() error {
handleConfirm: func() error { handleConfirm: func() error {
return gui.WithWaitingStatus(gui.Tr.SquashingStatus, func() error { return gui.WithWaitingStatus(gui.Tr.SquashingStatus, func() error {
gui.logAction(gui.Tr.Actions.SquashAllAboveFixupCommits) gui.logAction(gui.Tr.Actions.SquashAllAboveFixupCommits)
err := gui.GitCommand.SquashAllAboveFixupCommits(commit.Sha) err := gui.GitCommand.Rebase.SquashAllAboveFixupCommits(commit.Sha)
return gui.handleGenericMergeCommandResult(err) return gui.handleGenericMergeCommandResult(err)
}) })
}, },
@ -618,7 +618,7 @@ func (gui *Gui) handleCreateAnnotatedTag(commitSha string) error {
title: gui.Tr.TagMessageTitle, title: gui.Tr.TagMessageTitle,
handleConfirm: func(msg string) error { handleConfirm: func(msg string) error {
gui.logAction(gui.Tr.Actions.CreateAnnotatedTag) 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.surfaceError(err)
} }
return gui.afterTagCreate(tagName) return gui.afterTagCreate(tagName)
@ -633,7 +633,7 @@ func (gui *Gui) handleCreateLightweightTag(commitSha string) error {
title: gui.Tr.TagNameTitle, title: gui.Tr.TagNameTitle,
handleConfirm: func(tagName string) error { handleConfirm: func(tagName string) error {
gui.logAction(gui.Tr.Actions.CreateLightweightTag) 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.surfaceError(err)
} }
return gui.afterTagCreate(tagName) return gui.afterTagCreate(tagName)
@ -702,7 +702,7 @@ func (gui *Gui) handleCopySelectedCommitMessageToClipboard() error {
return nil return nil
} }
message, err := gui.GitCommand.GetCommitMessage(commit.Sha) message, err := gui.GitCommand.Commit.GetCommitMessage(commit.Sha)
if err != nil { if err != nil {
return gui.surfaceError(err) return gui.surfaceError(err)
} }

View File

@ -39,7 +39,7 @@ func (gui *Gui) DecreaseContextInDiffView() error {
} }
func (gui *Gui) CheckCanChangeContext() error { func (gui *Gui) CheckCanChangeContext() error {
if gui.GitCommand.PatchManager.Active() { if gui.GitCommand.Patch.PatchManager.Active() {
return errors.New(gui.Tr.CantChangeContextSizeError) return errors.New(gui.Tr.CantChangeContextSizeError)
} }

View File

@ -27,7 +27,7 @@ func setupGuiForTest(gui *Gui) {
gui.g = &gocui.Gui{} gui.g = &gocui.Gui{}
gui.Views.Main, _ = gui.prepareView("main") gui.Views.Main, _ = gui.prepareView("main")
gui.Views.Secondary, _ = gui.prepareView("secondary") gui.Views.Secondary, _ = gui.prepareView("secondary")
gui.GitCommand.PatchManager = &patch.PatchManager{} gui.GitCommand.Patch.PatchManager = &patch.PatchManager{}
_, _ = gui.refreshLineByLinePanel(diffForTest, "", false, 11) _, _ = gui.refreshLineByLinePanel(diffForTest, "", false, 11)
} }
@ -136,7 +136,7 @@ func TestDoesntIncreaseContextInDiffViewInContextWhenInPatchBuildingMode(t *test
setupGuiForTest(gui) setupGuiForTest(gui)
gui.UserConfig.Git.DiffContextSize = 2 gui.UserConfig.Git.DiffContextSize = 2
_ = gui.pushContextDirect(gui.State.Contexts.CommitFiles) _ = 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 errorCount := 0
gui.PopupHandler = &TestPopupHandler{ gui.PopupHandler = &TestPopupHandler{
@ -158,7 +158,7 @@ func TestDoesntDecreaseContextInDiffViewInContextWhenInPatchBuildingMode(t *test
setupGuiForTest(gui) setupGuiForTest(gui)
gui.UserConfig.Git.DiffContextSize = 2 gui.UserConfig.Git.DiffContextSize = 2
_ = gui.pushContextDirect(gui.State.Contexts.CommitFiles) _ = 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 errorCount := 0
gui.PopupHandler = &TestPopupHandler{ gui.PopupHandler = &TestPopupHandler{

View File

@ -13,7 +13,7 @@ func (gui *Gui) handleCreateDiscardMenu() error {
displayString: gui.Tr.LcDiscardAllChanges, displayString: gui.Tr.LcDiscardAllChanges,
onPress: func() error { onPress: func() error {
gui.logAction(gui.Tr.Actions.DiscardAllChangesInDirectory) 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.surfaceError(err)
} }
return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []RefreshableView{FILES}}) return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []RefreshableView{FILES}})
@ -26,7 +26,7 @@ func (gui *Gui) handleCreateDiscardMenu() error {
displayString: gui.Tr.LcDiscardUnstagedChanges, displayString: gui.Tr.LcDiscardUnstagedChanges,
onPress: func() error { onPress: func() error {
gui.logAction(gui.Tr.Actions.DiscardUnstagedChangesInDirectory) 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) return gui.surfaceError(err)
} }
@ -55,7 +55,7 @@ func (gui *Gui) handleCreateDiscardMenu() error {
displayString: gui.Tr.LcDiscardAllChanges, displayString: gui.Tr.LcDiscardAllChanges,
onPress: func() error { onPress: func() error {
gui.logAction(gui.Tr.Actions.DiscardAllChangesInFile) 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.surfaceError(err)
} }
return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []RefreshableView{FILES}}) return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []RefreshableView{FILES}})
@ -68,7 +68,7 @@ func (gui *Gui) handleCreateDiscardMenu() error {
displayString: gui.Tr.LcDiscardUnstagedChanges, displayString: gui.Tr.LcDiscardUnstagedChanges,
onPress: func() error { onPress: func() error {
gui.logAction(gui.Tr.Actions.DiscardAllUnstagedChangesInFile) 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) return gui.surfaceError(err)
} }

View File

@ -1,7 +1,6 @@
package gui package gui
import ( import (
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands" "github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/config" "github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/updates" "github.com/jesseduffield/lazygit/pkg/updates"
@ -17,6 +16,6 @@ func NewDummyUpdater() *updates.Updater {
func NewDummyGui() *Gui { func NewDummyGui() *Gui {
newAppConfig := config.NewDummyAppConfig() 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 return dummyGui
} }

View File

@ -58,7 +58,7 @@ func (gui *Gui) filesRenderToMain() error {
return gui.refreshMergePanelWithLock() 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{ refreshOpts := refreshMainOpts{main: &viewUpdateOpts{
title: gui.Tr.UnstagedChanges, title: gui.Tr.UnstagedChanges,
@ -67,7 +67,7 @@ func (gui *Gui) filesRenderToMain() error {
if node.GetHasUnstagedChanges() { if node.GetHasUnstagedChanges() {
if node.GetHasStagedChanges() { 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{ refreshOpts.secondary = &viewUpdateOpts{
title: gui.Tr.StagedChanges, title: gui.Tr.StagedChanges,
@ -157,7 +157,7 @@ func (gui *Gui) stageSelectedFile() error {
return nil return nil
} }
return gui.GitCommand.StageFile(file.Name) return gui.GitCommand.WorkingTree.StageFile(file.Name)
} }
func (gui *Gui) handleEnterFile() error { func (gui *Gui) handleEnterFile() error {
@ -207,12 +207,12 @@ func (gui *Gui) handleFilePress() error {
if file.HasUnstagedChanges { if file.HasUnstagedChanges {
gui.logAction(gui.Tr.Actions.StageFile) 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) return gui.surfaceError(err)
} }
} else { } else {
gui.logAction(gui.Tr.Actions.UnstageFile) 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) return gui.surfaceError(err)
} }
} }
@ -225,13 +225,13 @@ func (gui *Gui) handleFilePress() error {
if node.GetHasUnstagedChanges() { if node.GetHasUnstagedChanges() {
gui.logAction(gui.Tr.Actions.StageFile) 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) return gui.surfaceError(err)
} }
} else { } else {
// pretty sure it doesn't matter that we're always passing true here // pretty sure it doesn't matter that we're always passing true here
gui.logAction(gui.Tr.Actions.UnstageFile) 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) return gui.surfaceError(err)
} }
} }
@ -262,10 +262,10 @@ func (gui *Gui) handleStageAll() error {
var err error var err error
if gui.allFilesStaged() { if gui.allFilesStaged() {
gui.logAction(gui.Tr.Actions.UnstageAllFiles) gui.logAction(gui.Tr.Actions.UnstageAllFiles)
err = gui.GitCommand.UnstageAll() err = gui.GitCommand.WorkingTree.UnstageAll()
} else { } else {
gui.logAction(gui.Tr.Actions.StageAllFiles) gui.logAction(gui.Tr.Actions.StageAllFiles)
err = gui.GitCommand.StageAll() err = gui.GitCommand.WorkingTree.StageAll()
} }
if err != nil { if err != nil {
_ = gui.surfaceError(err) _ = gui.surfaceError(err)
@ -288,12 +288,10 @@ func (gui *Gui) handleIgnoreFile() error {
return gui.createErrorPanel("Cannot ignore .gitignore") return gui.createErrorPanel("Cannot ignore .gitignore")
} }
gui.logAction(gui.Tr.Actions.IgnoreFile)
unstageFiles := func() error { unstageFiles := func() error {
return node.ForEachFile(func(file *models.File) error { return node.ForEachFile(func(file *models.File) error {
if file.HasStagedChanges { 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 return err
} }
} }
@ -307,16 +305,17 @@ func (gui *Gui) handleIgnoreFile() error {
title: gui.Tr.IgnoreTracked, title: gui.Tr.IgnoreTracked,
prompt: gui.Tr.IgnoreTrackedPrompt, prompt: gui.Tr.IgnoreTrackedPrompt,
handleConfirm: func() error { handleConfirm: func() error {
gui.logAction(gui.Tr.Actions.IgnoreFile)
// not 100% sure if this is necessary but I'll assume it is // not 100% sure if this is necessary but I'll assume it is
if err := unstageFiles(); err != nil { if err := unstageFiles(); err != nil {
return err return err
} }
if err := gui.GitCommand.RemoveTrackedFiles(node.GetPath()); err != nil { if err := gui.GitCommand.WorkingTree.RemoveTrackedFiles(node.GetPath()); err != nil {
return err return err
} }
if err := gui.GitCommand.Ignore(node.GetPath()); err != nil { if err := gui.GitCommand.WorkingTree.Ignore(node.GetPath()); err != nil {
return err return err
} }
return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{FILES}}) 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 { if err := unstageFiles(); err != nil {
return err 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) return gui.surfaceError(err)
} }
@ -362,7 +363,7 @@ func (gui *Gui) prepareFilesForCommit() error {
noStagedFiles := len(gui.stagedFiles()) == 0 noStagedFiles := len(gui.stagedFiles()) == 0
if noStagedFiles && gui.UserConfig.Gui.SkipNoStagedFilesWarning { if noStagedFiles && gui.UserConfig.Gui.SkipNoStagedFilesWarning {
gui.logAction(gui.Tr.Actions.StageAllFiles) gui.logAction(gui.Tr.Actions.StageAllFiles)
err := gui.GitCommand.StageAll() err := gui.GitCommand.WorkingTree.StageAll()
if err != nil { if err != nil {
return err return err
} }
@ -423,7 +424,7 @@ func (gui *Gui) promptToStageAllAndRetry(retry func() error) error {
prompt: gui.Tr.NoFilesStagedPrompt, prompt: gui.Tr.NoFilesStagedPrompt,
handleConfirm: func() error { handleConfirm: func() error {
gui.logAction(gui.Tr.Actions.StageAllFiles) 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) return gui.surfaceError(err)
} }
if err := gui.refreshFilesAndSubmodules(); err != nil { if err := gui.refreshFilesAndSubmodules(); err != nil {
@ -452,7 +453,7 @@ func (gui *Gui) handleAmendCommitPress() error {
title: strings.Title(gui.Tr.AmendLastCommit), title: strings.Title(gui.Tr.AmendLastCommit),
prompt: gui.Tr.SureToAmend, prompt: gui.Tr.SureToAmend,
handleConfirm: func() error { handleConfirm: func() error {
cmdObj := gui.GitCommand.AmendHeadCmdObj() cmdObj := gui.GitCommand.Commit.AmendHeadCmdObj()
gui.logAction(gui.Tr.Actions.AmendCommit) gui.logAction(gui.Tr.Actions.AmendCommit)
return gui.withGpgHandling(cmdObj, gui.Tr.AmendingStatus, nil) 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 { 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 { if err != nil {
return gui.surfaceError(err) return gui.surfaceError(err)
} }
@ -569,8 +570,7 @@ func (gui *Gui) refreshStateFiles() error {
prevNodes := gui.State.FileManager.GetAllItems() prevNodes := gui.State.FileManager.GetAllItems()
prevSelectedLineIdx := gui.State.Panels.Files.SelectedLineIdx prevSelectedLineIdx := gui.State.Panels.Files.SelectedLineIdx
files := loaders. files := gui.GitCommand.Loaders.Files.
NewFileLoader(gui.Common, gui.GitCommand.Cmd, gui.GitCommand.GitConfig).
GetStatusFiles(loaders.GetStatusFileOptions{}) GetStatusFiles(loaders.GetStatusFileOptions{})
// for when you stage the old file of a rename and the new file is in a collapsed dir // 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, initialContent: suggestedRemote + "/" + currentBranch.Name,
findSuggestionsFunc: gui.getRemoteBranchesSuggestionsFunc("/"), findSuggestionsFunc: gui.getRemoteBranchesSuggestionsFunc("/"),
handleConfirm: func(upstream string) error { 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() errorMessage := err.Error()
if strings.Contains(errorMessage, "does not exist") { 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) 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,9 +724,8 @@ func (gui *Gui) pullWithLock(opts PullFilesOptions) error {
gui.logAction(opts.action) gui.logAction(opts.action)
err := gui.GitCommand.Pull( err := gui.GitCommand.Sync.Pull(
commands.PullOptions{ commands.PullOptions{
PromptUserForCredential: gui.promptUserForCredential,
RemoteName: opts.RemoteName, RemoteName: opts.RemoteName,
BranchName: opts.BranchName, BranchName: opts.BranchName,
FastForwardOnly: opts.FastForwardOnly, FastForwardOnly: opts.FastForwardOnly,
@ -751,12 +750,12 @@ func (gui *Gui) push(opts pushOpts) error {
} }
go utils.Safe(func() { go utils.Safe(func() {
gui.logAction(gui.Tr.Actions.Push) gui.logAction(gui.Tr.Actions.Push)
err := gui.GitCommand.Push(commands.PushOpts{ err := gui.GitCommand.Sync.Push(commands.PushOpts{
Force: opts.force, Force: opts.force,
UpstreamRemote: opts.upstreamRemote, UpstreamRemote: opts.upstreamRemote,
UpstreamBranch: opts.upstreamBranch, UpstreamBranch: opts.upstreamBranch,
SetUpstream: opts.setUpstream, SetUpstream: opts.setUpstream,
}, gui.promptUserForCredential) })
if err != nil && !opts.force && strings.Contains(err.Error(), "Updates were rejected") { if err != nil && !opts.force && strings.Contains(err.Error(), "Updates were rejected") {
forcePushDisabled := gui.UserConfig.Git.DisableForcePushing forcePushDisabled := gui.UserConfig.Git.DisableForcePushing
@ -819,7 +818,7 @@ func (gui *Gui) pushFiles() error {
suggestedRemote := getSuggestedRemote(gui.State.Remotes) suggestedRemote := getSuggestedRemote(gui.State.Remotes)
if gui.GitCommand.PushToCurrent { if gui.GitCommand.Config.GetPushToCurrent() {
return gui.push(pushOpts{setUpstream: true}) return gui.push(pushOpts{setUpstream: true})
} else { } else {
return gui.prompt(promptOpts{ return gui.prompt(promptOpts{
@ -954,14 +953,14 @@ func (gui *Gui) handleCreateStashMenu() error {
displayString: gui.Tr.LcStashAllChanges, displayString: gui.Tr.LcStashAllChanges,
onPress: func() error { onPress: func() error {
gui.logAction(gui.Tr.Actions.StashAllChanges) gui.logAction(gui.Tr.Actions.StashAllChanges)
return gui.handleStashSave(gui.GitCommand.StashSave) return gui.handleStashSave(gui.GitCommand.Stash.Save)
}, },
}, },
{ {
displayString: gui.Tr.LcStashStagedChanges, displayString: gui.Tr.LcStashStagedChanges,
onPress: func() error { onPress: func() error {
gui.logAction(gui.Tr.Actions.StashStagedChanges) 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 { func (gui *Gui) handleStashChanges() error {
return gui.handleStashSave(gui.GitCommand.StashSave) return gui.handleStashSave(gui.GitCommand.Stash.Save)
} }
func (gui *Gui) handleCreateResetToUpstreamMenu() error { func (gui *Gui) handleCreateResetToUpstreamMenu() error {
@ -1026,7 +1025,7 @@ func (gui *Gui) handleOpenMergeTool() error {
handleConfirm: func() error { handleConfirm: func() error {
gui.logAction(gui.Tr.Actions.OpenMergeTool) gui.logAction(gui.Tr.Actions.OpenMergeTool)
return gui.runSubprocessWithSuspenseAndRefresh( return gui.runSubprocessWithSuspenseAndRefresh(
gui.GitCommand.OpenMergeToolCmdObj(), gui.GitCommand.WorkingTree.OpenMergeToolCmdObj(),
) )
}, },
}) })

View File

@ -215,8 +215,7 @@ func (gui *Gui) fetch() (err error) {
defer gui.Mutexes.FetchMutex.Unlock() defer gui.Mutexes.FetchMutex.Unlock()
gui.logAction("Fetch") gui.logAction("Fetch")
err = gui.GitCommand.Sync.Fetch(commands.FetchOptions{})
err = gui.GitCommand.Fetch(commands.FetchOptions{PromptUserForCredential: gui.promptUserForCredential})
if err != nil && strings.Contains(err.Error(), "exit status 128") { if err != nil && strings.Contains(err.Error(), "exit status 128") {
_ = gui.createErrorPanel(gui.Tr.PassUnameWrong) _ = gui.createErrorPanel(gui.Tr.PassUnameWrong)
@ -231,7 +230,7 @@ func (gui *Gui) backgroundFetch() (err error) {
gui.Mutexes.FetchMutex.Lock() gui.Mutexes.FetchMutex.Lock()
defer gui.Mutexes.FetchMutex.Unlock() 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}) _ = gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{BRANCHES, COMMITS, REMOTES, TAGS}, mode: ASYNC})

View File

@ -15,7 +15,7 @@ import (
func (gui *Gui) withGpgHandling(cmdObj oscommands.ICmdObj, waitingStatus string, onSuccess func() error) error { func (gui *Gui) withGpgHandling(cmdObj oscommands.ICmdObj, waitingStatus string, onSuccess func() error) error {
gui.logCommand(cmdObj.ToString(), true) gui.logCommand(cmdObj.ToString(), true)
useSubprocess := gui.GitCommand.UsingGpg() useSubprocess := gui.GitCommand.Config.UsingGpg()
if useSubprocess { if useSubprocess {
success, err := gui.runSubprocessWithSuspense(gui.OSCommand.Cmd.NewShell(cmdObj.ToString())) success, err := gui.runSubprocessWithSuspense(gui.OSCommand.Cmd.NewShell(cmdObj.ToString()))
if success && onSuccess != nil { if success && onSuccess != nil {

View File

@ -12,6 +12,7 @@ import (
"github.com/jesseduffield/gocui" "github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands" "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/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands" "github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/common" "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 // for now the split view will always be on
// NewGui builds a new gui handler // 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{ gui := &Gui{
Common: cmn, Common: cmn,
GitCommand: gitCommand,
OSCommand: oSCommand,
Config: config, Config: config,
Updater: updater, Updater: updater,
statusManager: &statusManager{}, statusManager: &statusManager{},
@ -455,11 +460,30 @@ func NewGui(cmn *common.Common, gitCommand *commands.GitCommand, oSCommand *osco
ShowExtrasWindow: cmn.UserConfig.Gui.ShowCommandLog && !config.GetAppState().HideCommandLog, 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.resetState(filterPath, false)
gui.watchFilesForChanges() gui.watchFilesForChanges()
oSCommand.SetLogCommandFn(gui.logCommand)
gui.PopupHandler = &RealPopupHandler{gui: gui} gui.PopupHandler = &RealPopupHandler{gui: gui}
authors.SetCustomAuthors(gui.UserConfig.Gui.AuthorColors) authors.SetCustomAuthors(gui.UserConfig.Gui.AuthorColors)

View File

@ -743,8 +743,8 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
ViewName: "commits", ViewName: "commits",
Contexts: []string{string(BRANCH_COMMITS_CONTEXT_KEY)}, Contexts: []string{string(BRANCH_COMMITS_CONTEXT_KEY)},
Key: gui.getKey(config.Commits.RenameCommit), Key: gui.getKey(config.Commits.RenameCommit),
Handler: gui.handleRenameCommit, Handler: gui.handleRewordCommit,
Description: gui.Tr.LcRenameCommit, Description: gui.Tr.LcRewordCommit,
}, },
{ {
ViewName: "commits", ViewName: "commits",

View File

@ -124,8 +124,6 @@ func (gui *Gui) createAllViews() error {
return err return err
} }
gui.GitCommand.GetCmdWriter = gui.getCmdWriter
return nil return nil
} }

View File

@ -144,7 +144,7 @@ func (gui *Gui) refreshMainViewForLineByLine(state *LblPanelState) error {
if gui.currentContext().GetKey() == gui.State.Contexts.PatchBuilding.GetKey() { if gui.currentContext().GetKey() == gui.State.Contexts.PatchBuilding.GetKey() {
filename := gui.getSelectedCommitFileName() filename := gui.getSelectedCommitFileName()
var err error var err error
includedLineIndices, err = gui.GitCommand.PatchManager.GetFileIncLineIndices(filename) includedLineIndices, err = gui.GitCommand.Patch.PatchManager.GetFileIncLineIndices(filename)
if err != nil { if err != nil {
return err return err
} }

View File

@ -312,7 +312,7 @@ func (gui *Gui) commitFilesListContext() IListContext {
return [][]string{{style.FgRed.Sprint("(none)")}} 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)) mappedLines := make([][]string, len(lines))
for i, line := range lines { for i, line := range lines {
mappedLines[i] = []string{line} mappedLines[i] = []string{line}

View File

@ -195,7 +195,7 @@ func (gui *Gui) catSelectedFile() (string, error) {
return "", errors.New(gui.Tr.NotAFile) return "", errors.New(gui.Tr.NotAFile)
} }
cat, err := gui.GitCommand.CatFile(item.Name) cat, err := gui.GitCommand.File.Cat(item.Name)
if err != nil { if err != nil {
gui.Log.Error(err) gui.Log.Error(err)
return "", 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 // if we got conflicts after unstashing, we don't want to call any git
// commands to continue rebasing/merging here // 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() return gui.handleEscapeMerge()
} }
// if there are no more files with merge conflicts, we should ask whether the user wants to continue // if there are no more files with merge conflicts, we should ask whether the user wants to continue

View File

@ -26,7 +26,7 @@ func (gui *Gui) modeStatuses() []modeStatus {
reset: gui.exitDiffMode, reset: gui.exitDiffMode,
}, },
{ {
isActive: gui.GitCommand.PatchManager.Active, isActive: gui.GitCommand.Patch.PatchManager.Active,
description: func() string { description: func() string {
return style.FgYellow.SetBold().Sprintf( return style.FgYellow.SetBold().Sprintf(
"%s %s", "%s %s",
@ -61,10 +61,10 @@ func (gui *Gui) modeStatuses() []modeStatus {
}, },
{ {
isActive: func() bool { isActive: func() bool {
return gui.GitCommand.WorkingTreeState() != enums.REBASE_MODE_NONE return gui.GitCommand.Status.WorkingTreeState() != enums.REBASE_MODE_NONE
}, },
description: func() string { description: func() string {
workingTreeState := gui.GitCommand.WorkingTreeState() workingTreeState := gui.GitCommand.Status.WorkingTreeState()
return style.FgYellow.Sprintf( return style.FgYellow.Sprintf(
"%s %s", "%s %s",
workingTreeState, workingTreeState,

View File

@ -18,7 +18,7 @@ func (gui *Gui) getFromAndReverseArgsForDiff(to string) (string, bool) {
} }
func (gui *Gui) refreshPatchBuildingPanel(selectedLineIdx int, state *LblPanelState) error { func (gui *Gui) refreshPatchBuildingPanel(selectedLineIdx int, state *LblPanelState) error {
if !gui.GitCommand.PatchManager.Active() { if !gui.GitCommand.Patch.PatchManager.Active() {
return gui.handleEscapePatchBuildingPanel() return gui.handleEscapePatchBuildingPanel()
} }
@ -33,12 +33,12 @@ func (gui *Gui) refreshPatchBuildingPanel(selectedLineIdx int, state *LblPanelSt
to := gui.State.CommitFileManager.GetParent() to := gui.State.CommitFileManager.GetParent()
from, reverse := gui.getFromAndReverseArgsForDiff(to) 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 { if err != nil {
return err 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 { if err != nil {
return err return err
} }
@ -64,15 +64,15 @@ func (gui *Gui) handleRefreshPatchBuildingPanel(selectedLineIdx int) error {
func (gui *Gui) handleToggleSelectionForPatch() error { func (gui *Gui) handleToggleSelectionForPatch() error {
err := gui.withLBLActiveCheck(func(state *LblPanelState) error { err := gui.withLBLActiveCheck(func(state *LblPanelState) error {
toggleFunc := gui.GitCommand.PatchManager.AddFileLineRange toggleFunc := gui.GitCommand.Patch.PatchManager.AddFileLineRange
filename := gui.getSelectedCommitFileName() filename := gui.getSelectedCommitFileName()
includedLineIndices, err := gui.GitCommand.PatchManager.GetFileIncLineIndices(filename) includedLineIndices, err := gui.GitCommand.Patch.PatchManager.GetFileIncLineIndices(filename)
if err != nil { if err != nil {
return err return err
} }
currentLineIsStaged := utils.IncludesInt(includedLineIndices, state.GetSelectedLineIdx()) currentLineIsStaged := utils.IncludesInt(includedLineIndices, state.GetSelectedLineIdx())
if currentLineIsStaged { if currentLineIsStaged {
toggleFunc = gui.GitCommand.PatchManager.RemoveFileLineRange toggleFunc = gui.GitCommand.Patch.PatchManager.RemoveFileLineRange
} }
// add range of lines to those set for the file // add range of lines to those set for the file
@ -105,8 +105,8 @@ func (gui *Gui) handleToggleSelectionForPatch() error {
func (gui *Gui) handleEscapePatchBuildingPanel() error { func (gui *Gui) handleEscapePatchBuildingPanel() error {
gui.escapeLineByLinePanel() gui.escapeLineByLinePanel()
if gui.GitCommand.PatchManager.IsEmpty() { if gui.GitCommand.Patch.PatchManager.IsEmpty() {
gui.GitCommand.PatchManager.Reset() gui.GitCommand.Patch.PatchManager.Reset()
} }
if gui.currentContext().GetKey() == gui.State.Contexts.PatchBuilding.GetKey() { if gui.currentContext().GetKey() == gui.State.Contexts.PatchBuilding.GetKey() {
@ -118,8 +118,8 @@ func (gui *Gui) handleEscapePatchBuildingPanel() error {
} }
func (gui *Gui) secondaryPatchPanelUpdateOpts() *viewUpdateOpts { func (gui *Gui) secondaryPatchPanelUpdateOpts() *viewUpdateOpts {
if gui.GitCommand.PatchManager.Active() { if gui.GitCommand.Patch.PatchManager.Active() {
patch := gui.GitCommand.PatchManager.RenderAggregatedPatchColored(false) patch := gui.GitCommand.Patch.PatchManager.RenderAggregatedPatchColored(false)
return &viewUpdateOpts{ return &viewUpdateOpts{
title: "Custom Patch", title: "Custom Patch",

View File

@ -7,7 +7,7 @@ import (
) )
func (gui *Gui) handleCreatePatchOptionsMenu() error { func (gui *Gui) handleCreatePatchOptionsMenu() error {
if !gui.GitCommand.PatchManager.Active() { if !gui.GitCommand.Patch.PatchManager.Active() {
return gui.createErrorPanel(gui.Tr.NoPatchError) 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{ 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, onPress: gui.handleDeletePatchFromCommit,
}, },
{ {
@ -44,7 +44,7 @@ func (gui *Gui) handleCreatePatchOptionsMenu() error {
if gui.currentContext().GetKey() == gui.State.Contexts.BranchCommits.GetKey() { if gui.currentContext().GetKey() == gui.State.Contexts.BranchCommits.GetKey() {
selectedCommit := gui.getSelectedLocalCommit() 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 // adding this option to index 1
menuItems = append( menuItems = append(
menuItems[:1], menuItems[:1],
@ -66,7 +66,7 @@ func (gui *Gui) handleCreatePatchOptionsMenu() error {
func (gui *Gui) getPatchCommitIndex() int { func (gui *Gui) getPatchCommitIndex() int {
for index, commit := range gui.State.Commits { for index, commit := range gui.State.Commits {
if commit.Sha == gui.GitCommand.PatchManager.To { if commit.Sha == gui.GitCommand.Patch.PatchManager.To {
return index return index
} }
} }
@ -74,7 +74,7 @@ func (gui *Gui) getPatchCommitIndex() int {
} }
func (gui *Gui) validateNormalWorkingTreeState() (bool, error) { 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 false, gui.createErrorPanel(gui.Tr.CantPatchWhileRebasingError)
} }
return true, nil return true, nil
@ -99,7 +99,7 @@ func (gui *Gui) handleDeletePatchFromCommit() error {
return gui.WithWaitingStatus(gui.Tr.RebasingStatus, func() error { return gui.WithWaitingStatus(gui.Tr.RebasingStatus, func() error {
commitIndex := gui.getPatchCommitIndex() commitIndex := gui.getPatchCommitIndex()
gui.logAction(gui.Tr.Actions.RemovePatchFromCommit) 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) return gui.handleGenericMergeCommandResult(err)
}) })
} }
@ -116,7 +116,7 @@ func (gui *Gui) handleMovePatchToSelectedCommit() error {
return gui.WithWaitingStatus(gui.Tr.RebasingStatus, func() error { return gui.WithWaitingStatus(gui.Tr.RebasingStatus, func() error {
commitIndex := gui.getPatchCommitIndex() commitIndex := gui.getPatchCommitIndex()
gui.logAction(gui.Tr.Actions.MovePatchToSelectedCommit) 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) return gui.handleGenericMergeCommandResult(err)
}) })
} }
@ -134,7 +134,7 @@ func (gui *Gui) handleMovePatchIntoWorkingTree() error {
return gui.WithWaitingStatus(gui.Tr.RebasingStatus, func() error { return gui.WithWaitingStatus(gui.Tr.RebasingStatus, func() error {
commitIndex := gui.getPatchCommitIndex() commitIndex := gui.getPatchCommitIndex()
gui.logAction(gui.Tr.Actions.MovePatchIntoIndex) 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) return gui.handleGenericMergeCommandResult(err)
}) })
} }
@ -164,7 +164,7 @@ func (gui *Gui) handlePullPatchIntoNewCommit() error {
return gui.WithWaitingStatus(gui.Tr.RebasingStatus, func() error { return gui.WithWaitingStatus(gui.Tr.RebasingStatus, func() error {
commitIndex := gui.getPatchCommitIndex() commitIndex := gui.getPatchCommitIndex()
gui.logAction(gui.Tr.Actions.MovePatchIntoNewCommit) 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) return gui.handleGenericMergeCommandResult(err)
}) })
} }
@ -179,14 +179,14 @@ func (gui *Gui) handleApplyPatch(reverse bool) error {
action = "Apply patch in reverse" action = "Apply patch in reverse"
} }
gui.logAction(action) 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.surfaceError(err)
} }
return gui.refreshSidePanels(refreshOptions{mode: ASYNC}) return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
} }
func (gui *Gui) handleResetPatch() error { func (gui *Gui) handleResetPatch() error {
gui.GitCommand.PatchManager.Reset() gui.GitCommand.Patch.PatchManager.Reset()
if gui.currentContextKeyIgnoringPopups() == MAIN_PATCH_BUILDING_CONTEXT_KEY { if gui.currentContextKeyIgnoringPopups() == MAIN_PATCH_BUILDING_CONTEXT_KEY {
if err := gui.pushContext(gui.State.Contexts.CommitFiles); err != nil { if err := gui.pushContext(gui.State.Contexts.CommitFiles); err != nil {
return err return err

View File

@ -41,7 +41,7 @@ func (gui *Gui) onResize() error {
// command. // command.
func (gui *Gui) newPtyTask(view *gocui.View, cmd *exec.Cmd, prefix string) error { func (gui *Gui) newPtyTask(view *gocui.View, cmd *exec.Cmd, prefix string) error {
width, _ := gui.Views.Main.Size() width, _ := gui.Views.Main.Size()
pager := gui.GitCommand.GetPager(width) pager := gui.GitCommand.Config.GetPager(width)
if pager == "" { if pager == "" {
// if we're not using a custom pager we don't need to use a pty // if we're not using a custom pager we don't need to use a pty

View File

@ -71,7 +71,7 @@ func (gui *Gui) createPullRequest(from string, to string) error {
} }
func (gui *Gui) getHostingServiceMgr() *hosting_service.HostingServiceMgr { func (gui *Gui) getHostingServiceMgr() *hosting_service.HostingServiceMgr {
remoteUrl := gui.GitCommand.GetRemoteURL() remoteUrl := gui.GitCommand.Config.GetRemoteURL()
configServices := gui.UserConfig.Services configServices := gui.UserConfig.Services
return hosting_service.NewHostingServiceMgr(gui.Log, gui.Tr, remoteUrl, configServices) return hosting_service.NewHostingServiceMgr(gui.Log, gui.Tr, remoteUrl, configServices)
} }

View File

@ -18,7 +18,7 @@ const (
func (gui *Gui) handleCreateRebaseOptionsMenu() error { func (gui *Gui) handleCreateRebaseOptionsMenu() error {
options := []string{REBASE_OPTION_CONTINUE, REBASE_OPTION_ABORT} 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) options = append(options, REBASE_OPTION_SKIP)
} }
@ -35,7 +35,7 @@ func (gui *Gui) handleCreateRebaseOptionsMenu() error {
} }
var title string var title string
if gui.GitCommand.WorkingTreeState() == enums.REBASE_MODE_MERGING { if gui.GitCommand.Status.WorkingTreeState() == enums.REBASE_MODE_MERGING {
title = gui.Tr.MergeOptionsTitle title = gui.Tr.MergeOptionsTitle
} else { } else {
title = gui.Tr.RebaseOptionsTitle title = gui.Tr.RebaseOptionsTitle
@ -45,7 +45,7 @@ func (gui *Gui) handleCreateRebaseOptionsMenu() error {
} }
func (gui *Gui) genericMergeCommand(command string) 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 { if status != enums.REBASE_MODE_MERGING && status != enums.REBASE_MODE_REBASING {
return gui.createErrorPanel(gui.Tr.NotMergingOrRebasing) return gui.createErrorPanel(gui.Tr.NotMergingOrRebasing)
@ -71,7 +71,7 @@ func (gui *Gui) genericMergeCommand(command string) error {
} }
return nil return nil
} }
result := gui.GitCommand.GenericMergeOrRebaseAction(commandType, command) result := gui.GitCommand.Rebase.GenericMergeOrRebaseAction(commandType, command)
if err := gui.handleGenericMergeCommandResult(result); err != nil { if err := gui.handleGenericMergeCommandResult(result); err != nil {
return err return err
} }
@ -142,7 +142,7 @@ func (gui *Gui) abortMergeOrRebaseWithConfirm() error {
} }
func (gui *Gui) workingTreeStateNoun() string { func (gui *Gui) workingTreeStateNoun() string {
workingTreeState := gui.GitCommand.WorkingTreeState() workingTreeState := gui.GitCommand.Status.WorkingTreeState()
switch workingTreeState { switch workingTreeState {
case enums.REBASE_MODE_NONE: case enums.REBASE_MODE_NONE:
return "" return ""

View File

@ -99,7 +99,7 @@ func (gui *Gui) dispatchSwitchToRepo(path string, reuse bool) error {
// updateRecentRepoList registers the fact that we opened lazygit in this repo, // updateRecentRepoList registers the fact that we opened lazygit in this repo,
// so that we can open the same repo via the 'recent repos' menu // so that we can open the same repo via the 'recent repos' menu
func (gui *Gui) updateRecentRepoList() error { 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 // 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 // worktree in our recent repos list, which is a change that would need to be
// backwards compatible // backwards compatible

View File

@ -1,7 +1,6 @@
package gui package gui
import ( import (
"github.com/jesseduffield/lazygit/pkg/commands/loaders"
"github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/commands/models"
) )
@ -23,7 +22,7 @@ func (gui *Gui) reflogCommitsRenderToMain() error {
if commit == nil { if commit == nil {
task = NewRenderStringTask("No reflog history") task = NewRenderStringTask("No reflog history")
} else { } 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()) task = NewRunPtyTask(cmdObj.GetCmd())
} }
@ -53,8 +52,7 @@ func (gui *Gui) refreshReflogCommits() error {
} }
refresh := func(stateCommits *[]*models.Commit, filterPath string) error { refresh := func(stateCommits *[]*models.Commit, filterPath string) error {
commits, onlyObtainedNewReflogCommits, err := loaders. commits, onlyObtainedNewReflogCommits, err := gui.GitCommand.Loaders.ReflogCommits.
NewReflogCommitLoader(gui.Common, gui.GitCommand.Cmd).
GetReflogCommits(lastReflogCommit, filterPath) GetReflogCommits(lastReflogCommit, filterPath)
if err != nil { if err != nil {
return gui.surfaceError(err) return gui.surfaceError(err)

View File

@ -24,7 +24,7 @@ func (gui *Gui) remoteBranchesRenderToMain() error {
if remoteBranch == nil { if remoteBranch == nil {
task = NewRenderStringTask("No branches for this remote") task = NewRenderStringTask("No branches for this remote")
} else { } else {
cmdObj := gui.GitCommand.GetBranchGraphCmdObj(remoteBranch.FullName()) cmdObj := gui.GitCommand.Branch.GetGraphCmdObj(remoteBranch.FullName())
task = NewRunCommandTask(cmdObj.GetCmd()) task = NewRunCommandTask(cmdObj.GetCmd())
} }
@ -58,7 +58,7 @@ func (gui *Gui) handleDeleteRemoteBranch() error {
handleConfirm: func() error { handleConfirm: func() error {
return gui.WithWaitingStatus(gui.Tr.DeletingStatus, func() error { return gui.WithWaitingStatus(gui.Tr.DeletingStatus, func() error {
gui.logAction(gui.Tr.Actions.DeleteRemoteBranch) 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) gui.handleCredentialsPopup(err)
return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{BRANCHES, REMOTES}}) return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{BRANCHES, REMOTES}})
@ -89,7 +89,7 @@ func (gui *Gui) handleSetBranchUpstream() error {
prompt: message, prompt: message,
handleConfirm: func() error { handleConfirm: func() error {
gui.logAction(gui.Tr.Actions.SetBranchUpstream) 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) return gui.surfaceError(err)
} }

View File

@ -86,7 +86,7 @@ func (gui *Gui) handleAddRemote() error {
title: gui.Tr.LcNewRemoteUrl, title: gui.Tr.LcNewRemoteUrl,
handleConfirm: func(remoteUrl string) error { handleConfirm: func(remoteUrl string) error {
gui.logAction(gui.Tr.Actions.AddRemote) 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 err
} }
return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{REMOTES}}) return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{REMOTES}})
@ -108,7 +108,7 @@ func (gui *Gui) handleRemoveRemote() error {
prompt: gui.Tr.LcRemoveRemotePrompt + " '" + remote.Name + "'?", prompt: gui.Tr.LcRemoveRemotePrompt + " '" + remote.Name + "'?",
handleConfirm: func() error { handleConfirm: func() error {
gui.logAction(gui.Tr.Actions.RemoveRemote) 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) return gui.surfaceError(err)
} }
@ -136,7 +136,7 @@ func (gui *Gui) handleEditRemote() error {
handleConfirm: func(updatedRemoteName string) error { handleConfirm: func(updatedRemoteName string) error {
if updatedRemoteName != remote.Name { if updatedRemoteName != remote.Name {
gui.logAction(gui.Tr.Actions.UpdateRemote) 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) return gui.surfaceError(err)
} }
} }
@ -159,7 +159,7 @@ func (gui *Gui) handleEditRemote() error {
initialContent: url, initialContent: url,
handleConfirm: func(updatedRemoteUrl string) error { handleConfirm: func(updatedRemoteUrl string) error {
gui.logAction(gui.Tr.Actions.UpdateRemote) 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.surfaceError(err)
} }
return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{BRANCHES, REMOTES}}) return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{BRANCHES, REMOTES}})
@ -179,7 +179,7 @@ func (gui *Gui) handleFetchRemote() error {
gui.Mutexes.FetchMutex.Lock() gui.Mutexes.FetchMutex.Lock()
defer gui.Mutexes.FetchMutex.Unlock() defer gui.Mutexes.FetchMutex.Unlock()
err := gui.GitCommand.FetchRemote(remote.Name, gui.promptUserForCredential) err := gui.GitCommand.Sync.FetchRemote(remote.Name)
gui.handleCredentialsPopup(err) gui.handleCredentialsPopup(err)
return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{BRANCHES, REMOTES}}) return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{BRANCHES, REMOTES}})

View File

@ -7,7 +7,7 @@ import (
) )
func (gui *Gui) resetToRef(ref string, strength string, envVars []string) error { 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) return gui.surfaceError(err)
} }

View File

@ -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 // 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) diff := gui.GitCommand.WorkingTree.WorktreeFileDiff(file, true, secondaryFocused, false)
secondaryDiff := gui.GitCommand.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 // 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 // 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") applyFlags = append(applyFlags, "cached")
} }
gui.logAction(gui.Tr.Actions.ApplyPatch) gui.logAction(gui.Tr.Actions.ApplyPatch)
err := gui.GitCommand.ApplyPatch(patch, applyFlags...) err := gui.GitCommand.WorkingTree.ApplyPatch(patch, applyFlags...)
if err != nil { if err != nil {
return gui.surfaceError(err) return gui.surfaceError(err)
} }

View File

@ -1,9 +1,7 @@
package gui package gui
import ( import (
"github.com/jesseduffield/lazygit/pkg/commands/loaders"
"github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/utils"
) )
// list panel functions // list panel functions
@ -23,7 +21,7 @@ func (gui *Gui) stashRenderToMain() error {
if stashEntry == nil { if stashEntry == nil {
task = NewRenderStringTask(gui.Tr.NoStashEntries) task = NewRenderStringTask(gui.Tr.NoStashEntries)
} else { } else {
task = NewRunPtyTask(gui.GitCommand.ShowStashEntryCmdObj(stashEntry.Index).GetCmd()) task = NewRunPtyTask(gui.GitCommand.Stash.ShowStashEntryCmdObj(stashEntry.Index).GetCmd())
} }
return gui.refreshMainViews(refreshMainOpts{ return gui.refreshMainViews(refreshMainOpts{
@ -35,8 +33,7 @@ func (gui *Gui) stashRenderToMain() error {
} }
func (gui *Gui) refreshStashEntries() error { func (gui *Gui) refreshStashEntries() error {
gui.State.StashEntries = loaders. gui.State.StashEntries = gui.GitCommand.Loaders.Stash.
NewStashLoader(gui.Common, gui.GitCommand.Cmd).
GetStashEntries(gui.State.Modes.Filtering.GetPath()) GetStashEntries(gui.State.Modes.Filtering.GetPath())
return gui.State.Contexts.Stash.HandleRender() return gui.State.Contexts.Stash.HandleRender()
@ -45,10 +42,19 @@ func (gui *Gui) refreshStashEntries() error {
// specific functions // specific functions
func (gui *Gui) handleStashApply() error { func (gui *Gui) handleStashApply() error {
stashEntry := gui.getSelectedStashEntry()
if stashEntry == nil {
return nil
}
skipStashWarning := gui.UserConfig.Gui.SkipStashWarning skipStashWarning := gui.UserConfig.Gui.SkipStashWarning
apply := func() error { 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 { if skipStashWarning {
@ -65,10 +71,19 @@ func (gui *Gui) handleStashApply() error {
} }
func (gui *Gui) handleStashPop() error { func (gui *Gui) handleStashPop() error {
stashEntry := gui.getSelectedStashEntry()
if stashEntry == nil {
return nil
}
skipStashWarning := gui.UserConfig.Gui.SkipStashWarning skipStashWarning := gui.UserConfig.Gui.SkipStashWarning
pop := func() error { 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 { if skipStashWarning {
@ -85,31 +100,25 @@ func (gui *Gui) handleStashPop() error {
} }
func (gui *Gui) handleStashDrop() error { func (gui *Gui) handleStashDrop() error {
stashEntry := gui.getSelectedStashEntry()
if stashEntry == nil {
return nil
}
return gui.ask(askOpts{ return gui.ask(askOpts{
title: gui.Tr.StashDrop, title: gui.Tr.StashDrop,
prompt: gui.Tr.SureDropStashEntry, prompt: gui.Tr.SureDropStashEntry,
handleConfirm: func() error { 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 { func (gui *Gui) postStashRefresh() 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)
}
return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{STASH, FILES}}) return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{STASH, FILES}})
} }

View File

@ -28,8 +28,8 @@ func (gui *Gui) refreshStatus() {
status += presentation.ColoredBranchStatus(currentBranch) + " " status += presentation.ColoredBranchStatus(currentBranch) + " "
} }
if gui.GitCommand.WorkingTreeState() != enums.REBASE_MODE_NONE { if gui.GitCommand.Status.WorkingTreeState() != enums.REBASE_MODE_NONE {
status += style.FgYellow.Sprintf("(%s) ", gui.GitCommand.WorkingTreeState()) status += style.FgYellow.Sprintf("(%s) ", gui.GitCommand.Status.WorkingTreeState())
} }
name := presentation.GetBranchTextStyle(currentBranch.Name).Sprint(currentBranch.Name) name := presentation.GetBranchTextStyle(currentBranch.Name).Sprint(currentBranch.Name)
@ -71,7 +71,7 @@ func (gui *Gui) handleStatusClick() error {
cx, _ := gui.Views.Status.Cursor() cx, _ := gui.Views.Status.Cursor()
upstreamStatus := presentation.BranchStatus(currentBranch) upstreamStatus := presentation.BranchStatus(currentBranch)
repoName := utils.GetCurrentRepoName() repoName := utils.GetCurrentRepoName()
workingTreeState := gui.GitCommand.WorkingTreeState() workingTreeState := gui.GitCommand.Status.WorkingTreeState()
switch workingTreeState { switch workingTreeState {
case enums.REBASE_MODE_REBASING, enums.REBASE_MODE_MERGING: case enums.REBASE_MODE_REBASING, enums.REBASE_MODE_MERGING:
var formattedState string var formattedState string

View File

@ -23,7 +23,7 @@ func (gui *Gui) subCommitsRenderToMain() error {
if commit == nil { if commit == nil {
task = NewRenderStringTask("No commits") task = NewRenderStringTask("No commits")
} else { } 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()) task = NewRunPtyTask(cmdObj.GetCmd())
} }

View File

@ -36,7 +36,7 @@ func (gui *Gui) submodulesRenderToMain() error {
if file == nil { if file == nil {
task = NewRenderStringTask(prefix) task = NewRenderStringTask(prefix)
} else { } 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) task = NewRunCommandTaskWithPrefix(cmdObj.GetCmd(), prefix)
} }
} }
@ -50,7 +50,7 @@ func (gui *Gui) submodulesRenderToMain() error {
} }
func (gui *Gui) refreshStateSubmoduleConfigs() error { func (gui *Gui) refreshStateSubmoduleConfigs() error {
configs, err := gui.GitCommand.GetSubmoduleConfigs() configs, err := gui.GitCommand.Submodule.GetConfigs()
if err != nil { if err != nil {
return err return err
} }
@ -80,7 +80,7 @@ func (gui *Gui) removeSubmodule(submodule *models.SubmoduleConfig) error {
prompt: fmt.Sprintf(gui.Tr.RemoveSubmodulePrompt, submodule.Name), prompt: fmt.Sprintf(gui.Tr.RemoveSubmodulePrompt, submodule.Name),
handleConfirm: func() error { handleConfirm: func() error {
gui.logAction(gui.Tr.Actions.RemoveSubmodule) 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) return gui.surfaceError(err)
} }
@ -110,15 +110,15 @@ func (gui *Gui) resetSubmodule(submodule *models.SubmoduleConfig) error {
file := gui.fileForSubmodule(submodule) file := gui.fileForSubmodule(submodule)
if file != nil { 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) 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) 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) return gui.surfaceError(err)
} }
@ -142,7 +142,7 @@ func (gui *Gui) handleAddSubmodule() error {
handleConfirm: func(submodulePath string) error { handleConfirm: func(submodulePath string) error {
return gui.WithWaitingStatus(gui.Tr.LcAddingSubmoduleStatus, func() error { return gui.WithWaitingStatus(gui.Tr.LcAddingSubmoduleStatus, func() error {
gui.logAction(gui.Tr.Actions.AddSubmodule) gui.logAction(gui.Tr.Actions.AddSubmodule)
err := gui.GitCommand.SubmoduleAdd(submoduleName, submodulePath, submoduleUrl) err := gui.GitCommand.Submodule.Add(submoduleName, submodulePath, submoduleUrl)
gui.handleCredentialsPopup(err) gui.handleCredentialsPopup(err)
return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{SUBMODULES}}) return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{SUBMODULES}})
@ -163,7 +163,7 @@ func (gui *Gui) handleEditSubmoduleUrl(submodule *models.SubmoduleConfig) error
handleConfirm: func(newUrl string) error { handleConfirm: func(newUrl string) error {
return gui.WithWaitingStatus(gui.Tr.LcUpdatingSubmoduleUrlStatus, func() error { return gui.WithWaitingStatus(gui.Tr.LcUpdatingSubmoduleUrlStatus, func() error {
gui.logAction(gui.Tr.Actions.UpdateSubmoduleUrl) 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) gui.handleCredentialsPopup(err)
return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{SUBMODULES}}) 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 { func (gui *Gui) handleSubmoduleInit(submodule *models.SubmoduleConfig) error {
return gui.WithWaitingStatus(gui.Tr.LcInitializingSubmoduleStatus, func() error { return gui.WithWaitingStatus(gui.Tr.LcInitializingSubmoduleStatus, func() error {
gui.logAction(gui.Tr.Actions.InitialiseSubmodule) gui.logAction(gui.Tr.Actions.InitialiseSubmodule)
err := gui.GitCommand.SubmoduleInit(submodule.Path) err := gui.GitCommand.Submodule.Init(submodule.Path)
gui.handleCredentialsPopup(err) gui.handleCredentialsPopup(err)
return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{SUBMODULES}}) return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{SUBMODULES}})
@ -215,11 +215,11 @@ func (gui *Gui) handleResetRemoveSubmodule(submodule *models.SubmoduleConfig) er
func (gui *Gui) handleBulkSubmoduleActionsMenu() error { func (gui *Gui) handleBulkSubmoduleActionsMenu() error {
menuItems := []*menuItem{ 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 { onPress: func() error {
return gui.WithWaitingStatus(gui.Tr.LcRunningCommand, func() error { return gui.WithWaitingStatus(gui.Tr.LcRunningCommand, func() error {
gui.logAction(gui.Tr.Actions.BulkInitialiseSubmodules) gui.logAction(gui.Tr.Actions.BulkInitialiseSubmodules)
err := gui.GitCommand.SubmoduleBulkInitCmdObj().Run() err := gui.GitCommand.Submodule.BulkInitCmdObj().Run()
if err != nil { if err != nil {
return gui.surfaceError(err) 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 { onPress: func() error {
return gui.WithWaitingStatus(gui.Tr.LcRunningCommand, func() error { return gui.WithWaitingStatus(gui.Tr.LcRunningCommand, func() error {
gui.logAction(gui.Tr.Actions.BulkUpdateSubmodules) 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) 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 { onPress: func() error {
return gui.WithWaitingStatus(gui.Tr.LcRunningCommand, func() error { return gui.WithWaitingStatus(gui.Tr.LcRunningCommand, func() error {
gui.logAction(gui.Tr.Actions.BulkStashAndResetSubmodules) 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) 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 { onPress: func() error {
return gui.WithWaitingStatus(gui.Tr.LcRunningCommand, func() error { return gui.WithWaitingStatus(gui.Tr.LcRunningCommand, func() error {
gui.logAction(gui.Tr.Actions.BulkDeinitialiseSubmodules) 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) return gui.surfaceError(err)
} }
@ -275,7 +275,7 @@ func (gui *Gui) handleBulkSubmoduleActionsMenu() error {
func (gui *Gui) handleUpdateSubmodule(submodule *models.SubmoduleConfig) error { func (gui *Gui) handleUpdateSubmodule(submodule *models.SubmoduleConfig) error {
return gui.WithWaitingStatus(gui.Tr.LcUpdatingSubmoduleStatus, func() error { return gui.WithWaitingStatus(gui.Tr.LcUpdatingSubmoduleStatus, func() error {
gui.logAction(gui.Tr.Actions.UpdateSubmodule) gui.logAction(gui.Tr.Actions.UpdateSubmodule)
err := gui.GitCommand.SubmoduleUpdate(submodule.Path) err := gui.GitCommand.Submodule.Update(submodule.Path)
gui.handleCredentialsPopup(err) gui.handleCredentialsPopup(err)
return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{SUBMODULES}}) return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{SUBMODULES}})

View File

@ -25,7 +25,7 @@ func (gui *Gui) tagsRenderToMain() error {
if tag == nil { if tag == nil {
task = NewRenderStringTask("No tags") task = NewRenderStringTask("No tags")
} else { } else {
cmdObj := gui.GitCommand.GetBranchGraphCmdObj(tag.Name) cmdObj := gui.GitCommand.Branch.GetGraphCmdObj(tag.Name)
task = NewRunCommandTask(cmdObj.GetCmd()) task = NewRunCommandTask(cmdObj.GetCmd())
} }
@ -83,7 +83,7 @@ func (gui *Gui) handleDeleteTag(tag *models.Tag) error {
prompt: prompt, prompt: prompt,
handleConfirm: func() error { handleConfirm: func() error {
gui.logAction(gui.Tr.Actions.DeleteTag) 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.surfaceError(err)
} }
return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []RefreshableView{COMMITS, TAGS}}) 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 { handleConfirm: func(response string) error {
return gui.WithWaitingStatus(gui.Tr.PushingTagStatus, func() error { return gui.WithWaitingStatus(gui.Tr.PushingTagStatus, func() error {
gui.logAction(gui.Tr.Actions.PushTag) 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) gui.handleCredentialsPopup(err)
return nil return nil

View File

@ -88,7 +88,7 @@ func (gui *Gui) reflogUndo() error {
undoEnvVars := []string{"GIT_REFLOG_ACTION=[lazygit undo]"} undoEnvVars := []string{"GIT_REFLOG_ACTION=[lazygit undo]"}
undoingStatus := gui.Tr.UndoingStatus 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) return gui.createErrorPanel(gui.Tr.LcCantUndoWhileRebasing)
} }
@ -121,7 +121,7 @@ func (gui *Gui) reflogRedo() error {
redoEnvVars := []string{"GIT_REFLOG_ACTION=[lazygit redo]"} redoEnvVars := []string{"GIT_REFLOG_ACTION=[lazygit redo]"}
redoingStatus := gui.Tr.RedoingStatus 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) return gui.createErrorPanel(gui.Tr.LcCantRedoWhileRebasing)
} }
@ -176,14 +176,14 @@ func (gui *Gui) handleHardResetWithAutoStash(commitSha string, options handleHar
prompt: gui.Tr.AutoStashPrompt, prompt: gui.Tr.AutoStashPrompt,
handleConfirm: func() error { handleConfirm: func() error {
return gui.WithWaitingStatus(options.WaitingStatus, 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) return gui.surfaceError(err)
} }
if err := reset(); err != nil { if err := reset(); err != nil {
return err return err
} }
err := gui.GitCommand.StashDo(0, "pop") err := gui.GitCommand.Stash.Pop(0)
if err := gui.refreshSidePanels(refreshOptions{}); err != nil { if err := gui.refreshSidePanels(refreshOptions{}); err != nil {
return err return err
} }

View File

@ -22,7 +22,7 @@ func (gui *Gui) handleCreateResetMenu() error {
}, },
onPress: func() error { onPress: func() error {
gui.logAction(gui.Tr.Actions.NukeWorkingTree) 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) return gui.surfaceError(err)
} }
@ -36,7 +36,7 @@ func (gui *Gui) handleCreateResetMenu() error {
}, },
onPress: func() error { onPress: func() error {
gui.logAction(gui.Tr.Actions.DiscardUnstagedFileChanges) 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) return gui.surfaceError(err)
} }
@ -50,7 +50,7 @@ func (gui *Gui) handleCreateResetMenu() error {
}, },
onPress: func() error { onPress: func() error {
gui.logAction(gui.Tr.Actions.RemoveUntrackedFiles) 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) return gui.surfaceError(err)
} }
@ -64,7 +64,7 @@ func (gui *Gui) handleCreateResetMenu() error {
}, },
onPress: func() error { onPress: func() error {
gui.logAction(gui.Tr.Actions.SoftReset) 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) return gui.surfaceError(err)
} }
@ -78,7 +78,7 @@ func (gui *Gui) handleCreateResetMenu() error {
}, },
onPress: func() error { onPress: func() error {
gui.logAction(gui.Tr.Actions.MixedReset) 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) return gui.surfaceError(err)
} }
@ -92,7 +92,7 @@ func (gui *Gui) handleCreateResetMenu() error {
}, },
onPress: func() error { onPress: func() error {
gui.logAction(gui.Tr.Actions.HardReset) 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) return gui.surfaceError(err)
} }

View File

@ -109,8 +109,8 @@ func chineseTranslationSet() TranslationSet {
Squash: "压缩", Squash: "压缩",
LcPickCommit: "选择提交(变基过程中)", LcPickCommit: "选择提交(变基过程中)",
LcRevertCommit: "还原提交", LcRevertCommit: "还原提交",
OnlyRenameTopCommit: "只能从 lazygit 内部重写最高的提交。请使用 shift-R", OnlyRewordTopCommit: "只能从 lazygit 内部重写最高的提交。请使用 shift-R",
LcRenameCommit: "改写提交", LcRewordCommit: "改写提交",
LcDeleteCommit: "删除提交", LcDeleteCommit: "删除提交",
LcMoveDownCommit: "下移提交", LcMoveDownCommit: "下移提交",
LcMoveUpCommit: "上移提交", LcMoveUpCommit: "上移提交",
@ -135,7 +135,6 @@ func chineseTranslationSet() TranslationSet {
SurePopStashEntry: "您确定要应用并删除此贮藏条目吗?", SurePopStashEntry: "您确定要应用并删除此贮藏条目吗?",
StashApply: "应用贮藏", StashApply: "应用贮藏",
SureApplyStashEntry: "您确定要应用此贮藏条目?", SureApplyStashEntry: "您确定要应用此贮藏条目?",
NoStashTo: "没有贮藏条目可以 {{.method}}",
NoTrackedStagedFilesStash: "没有可以贮藏的已跟踪/暂存文件", NoTrackedStagedFilesStash: "没有可以贮藏的已跟踪/暂存文件",
StashChanges: "贮藏更改", StashChanges: "贮藏更改",
MergeAborted: "合并中止", MergeAborted: "合并中止",

View File

@ -79,8 +79,8 @@ func dutchTranslationSet() TranslationSet {
Squash: "Squash", Squash: "Squash",
LcPickCommit: "kies commit (wanneer midden in rebase)", LcPickCommit: "kies commit (wanneer midden in rebase)",
LcRevertCommit: "commit ongedaan maken", LcRevertCommit: "commit ongedaan maken",
OnlyRenameTopCommit: "Je kan alleen de bovenste commit hernoemen", OnlyRewordTopCommit: "Je kan alleen de bovenste commit hernoemen",
LcRenameCommit: "hernoem commit", LcRewordCommit: "hernoem commit",
LcDeleteCommit: "verwijder commit", LcDeleteCommit: "verwijder commit",
LcMoveDownCommit: "verplaats commit 1 naar beneden", LcMoveDownCommit: "verplaats commit 1 naar beneden",
LcMoveUpCommit: "verplaats commit 1 naar boven", LcMoveUpCommit: "verplaats commit 1 naar boven",
@ -106,7 +106,6 @@ func dutchTranslationSet() TranslationSet {
SurePopStashEntry: "Weet je zeker dat je deze stash entry wil poppen?", SurePopStashEntry: "Weet je zeker dat je deze stash entry wil poppen?",
StashApply: "Stash toepassen", StashApply: "Stash toepassen",
SureApplyStashEntry: "Weet je zeker dat je deze stash entry wil 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", NoTrackedStagedFilesStash: "Je hebt geen tracked/staged bestanden om te laten stashen",
StashChanges: "Stash veranderingen", StashChanges: "Stash veranderingen",
NoChangedFiles: "Geen veranderde bestanden", NoChangedFiles: "Geen veranderde bestanden",

View File

@ -93,8 +93,8 @@ type TranslationSet struct {
Squash string Squash string
LcPickCommit string LcPickCommit string
LcRevertCommit string LcRevertCommit string
OnlyRenameTopCommit string OnlyRewordTopCommit string
LcRenameCommit string LcRewordCommit string
LcDeleteCommit string LcDeleteCommit string
LcMoveDownCommit string LcMoveDownCommit string
LcMoveUpCommit string LcMoveUpCommit string
@ -120,7 +120,6 @@ type TranslationSet struct {
SurePopStashEntry string SurePopStashEntry string
StashApply string StashApply string
SureApplyStashEntry string SureApplyStashEntry string
NoStashTo string
NoTrackedStagedFilesStash string NoTrackedStagedFilesStash string
StashChanges string StashChanges string
MergeAborted string MergeAborted string
@ -646,8 +645,8 @@ func EnglishTranslationSet() TranslationSet {
Squash: "Squash", Squash: "Squash",
LcPickCommit: "pick commit (when mid-rebase)", LcPickCommit: "pick commit (when mid-rebase)",
LcRevertCommit: "revert commit", LcRevertCommit: "revert commit",
OnlyRenameTopCommit: "Can only reword topmost commit from within lazygit. Use shift+R instead", OnlyRewordTopCommit: "Can only reword topmost commit from within lazygit. Use shift+R instead",
LcRenameCommit: "reword commit", LcRewordCommit: "reword commit",
LcDeleteCommit: "delete commit", LcDeleteCommit: "delete commit",
LcMoveDownCommit: "move commit down one", LcMoveDownCommit: "move commit down one",
LcMoveUpCommit: "move commit up one", LcMoveUpCommit: "move commit up one",
@ -672,7 +671,6 @@ func EnglishTranslationSet() TranslationSet {
SurePopStashEntry: "Are you sure you want to pop this stash entry?", SurePopStashEntry: "Are you sure you want to pop this stash entry?",
StashApply: "Stash apply", StashApply: "Stash apply",
SureApplyStashEntry: "Are you sure you want to apply this stash entry?", 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", NoTrackedStagedFilesStash: "You have no tracked/staged files to stash",
StashChanges: "Stash changes", StashChanges: "Stash changes",
MergeAborted: "Merge aborted", MergeAborted: "Merge aborted",

View File

@ -69,8 +69,8 @@ func polishTranslationSet() TranslationSet {
YouNoCommitsToSquash: "Nie masz commitów do spłaszczenia", YouNoCommitsToSquash: "Nie masz commitów do spłaszczenia",
Fixup: "Napraw", Fixup: "Napraw",
SureFixupThisCommit: "Jesteś pewny, ze chcesz naprawić ten commit? Commit poniżej zostanie spłaszczony w górę wraz z tym", 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", OnlyRewordTopCommit: "Można zmienić nazwę tylko ostatniemu commitowi",
LcRenameCommit: "zmień nazwę commita", LcRewordCommit: "zmień nazwę commita",
LcRenameCommitEditor: "zmień nazwę commita w edytorze", LcRenameCommitEditor: "zmień nazwę commita w edytorze",
Error: "Błąd", Error: "Błąd",
LcSelectHunk: "wybierz kawałek", LcSelectHunk: "wybierz kawałek",
@ -84,7 +84,6 @@ func polishTranslationSet() TranslationSet {
NoStashEntries: "Brak pozycji w schowku", NoStashEntries: "Brak pozycji w schowku",
StashDrop: "Porzuć schowek", StashDrop: "Porzuć schowek",
SureDropStashEntry: "Jesteś pewny, że chcesz porzucić tę pozycję w schowku?", SureDropStashEntry: "Jesteś pewny, że chcesz porzucić tę pozycję w schowku?",
NoStashTo: "Brak schowka dla {{.method}}",
NoTrackedStagedFilesStash: "Nie masz śledzonych/zatwierdzonych plików do przechowania", NoTrackedStagedFilesStash: "Nie masz śledzonych/zatwierdzonych plików do przechowania",
StashChanges: "Przechowaj zmiany", StashChanges: "Przechowaj zmiany",
MergeAborted: "Scalanie anulowane", MergeAborted: "Scalanie anulowane",

View File

@ -3,6 +3,7 @@ package utils
import ( import (
"io/ioutil" "io/ioutil"
"github.com/jesseduffield/lazygit/pkg/commands/git_config"
"github.com/jesseduffield/lazygit/pkg/common" "github.com/jesseduffield/lazygit/pkg/common"
"github.com/jesseduffield/lazygit/pkg/config" "github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/i18n" "github.com/jesseduffield/lazygit/pkg/i18n"
@ -24,3 +25,7 @@ func NewDummyCommon() *common.Common {
UserConfig: config.GetDefaultConfig(), UserConfig: config.GetDefaultConfig(),
} }
} }
func NewDummyGitConfig() git_config.IGitConfig {
return git_config.NewFakeGitConfig(map[string]string{})
}