diff --git a/go.mod b/go.mod index 6ca0594de..566576679 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,7 @@ require ( github.com/imdario/mergo v0.3.11 github.com/integrii/flaggy v1.4.0 github.com/jesseduffield/go-git/v5 v5.1.2-0.20201006095850-341962be15a4 - github.com/jesseduffield/gocui v0.3.1-0.20210409121040-210802112d8a + github.com/jesseduffield/gocui v0.3.1-0.20210410011117-a2bb4baca390 github.com/jesseduffield/termbox-go v0.0.0-20200823212418-a2289ed6aafe // indirect github.com/jesseduffield/yaml v2.1.0+incompatible github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 diff --git a/go.sum b/go.sum index 7bcf90094..0c9ed0165 100644 --- a/go.sum +++ b/go.sum @@ -108,6 +108,8 @@ github.com/jesseduffield/gocui v0.3.1-0.20210406065942-1b0c68414064 h1:Oe+QJuUIO github.com/jesseduffield/gocui v0.3.1-0.20210406065942-1b0c68414064/go.mod h1:QWq79xplEoyhQO+dgpk3sojjTVRKjQklyTlzm5nC5Kg= github.com/jesseduffield/gocui v0.3.1-0.20210409121040-210802112d8a h1:ocrSuZxQIgWWt27b+rjiyIIPz6fzfFeoL5Q4cpa2cAo= github.com/jesseduffield/gocui v0.3.1-0.20210409121040-210802112d8a/go.mod h1:QWq79xplEoyhQO+dgpk3sojjTVRKjQklyTlzm5nC5Kg= +github.com/jesseduffield/gocui v0.3.1-0.20210410011117-a2bb4baca390 h1:Es72JiUjt01TtvqCugdvOR91baB3DhuWF1DNuxA0frA= +github.com/jesseduffield/gocui v0.3.1-0.20210410011117-a2bb4baca390/go.mod h1:QWq79xplEoyhQO+dgpk3sojjTVRKjQklyTlzm5nC5Kg= github.com/jesseduffield/termbox-go v0.0.0-20200823212418-a2289ed6aafe h1:qsVhCf2RFyyKIUe/+gJblbCpXMUki9rZrHuEctg6M/E= github.com/jesseduffield/termbox-go v0.0.0-20200823212418-a2289ed6aafe/go.mod h1:anMibpZtqNxjDbxrcDEAwSdaJ37vyUeM1f/M4uekib4= github.com/jesseduffield/yaml v2.1.0+incompatible h1:HWQJ1gIv2zHKbDYNp0Jwjlj24K8aqpFHnMCynY1EpmE= diff --git a/pkg/commands/branches_test.go b/pkg/commands/branches_test.go new file mode 100644 index 000000000..85c477523 --- /dev/null +++ b/pkg/commands/branches_test.go @@ -0,0 +1,338 @@ +package commands + +import ( + "os/exec" + "testing" + + "github.com/jesseduffield/lazygit/pkg/secureexec" + "github.com/jesseduffield/lazygit/pkg/test" + "github.com/stretchr/testify/assert" +) + +// TestGitCommandGetCommitDifferences is a function. +func TestGitCommandGetCommitDifferences(t *testing.T) { + type scenario struct { + testName string + command func(string, ...string) *exec.Cmd + test func(string, string) + } + + scenarios := []scenario{ + { + "Can't retrieve pushable count", + func(string, ...string) *exec.Cmd { + return secureexec.Command("test") + }, + func(pushableCount string, pullableCount string) { + assert.EqualValues(t, "?", pushableCount) + assert.EqualValues(t, "?", pullableCount) + }, + }, + { + "Can't retrieve pullable count", + func(cmd string, args ...string) *exec.Cmd { + if args[1] == "HEAD..@{u}" { + return secureexec.Command("test") + } + + return secureexec.Command("echo") + }, + func(pushableCount string, pullableCount string) { + assert.EqualValues(t, "?", pushableCount) + assert.EqualValues(t, "?", pullableCount) + }, + }, + { + "Retrieve pullable and pushable count", + func(cmd string, args ...string) *exec.Cmd { + if args[1] == "HEAD..@{u}" { + return secureexec.Command("echo", "10") + } + + return secureexec.Command("echo", "11") + }, + func(pushableCount string, pullableCount string) { + assert.EqualValues(t, "11", pushableCount) + assert.EqualValues(t, "10", pullableCount) + }, + }, + } + + for _, s := range scenarios { + t.Run(s.testName, func(t *testing.T) { + gitCmd := NewDummyGitCommand() + gitCmd.OSCommand.Command = s.command + s.test(gitCmd.GetCommitDifferences("HEAD", "@{u}")) + }) + } +} + +// TestGitCommandNewBranch is a function. +func TestGitCommandNewBranch(t *testing.T) { + gitCmd := NewDummyGitCommand() + gitCmd.OSCommand.Command = func(cmd string, args ...string) *exec.Cmd { + assert.EqualValues(t, "git", cmd) + assert.EqualValues(t, []string{"checkout", "-b", "test", "master"}, args) + + return secureexec.Command("echo") + } + + assert.NoError(t, gitCmd.NewBranch("test", "master")) +} + +// TestGitCommandDeleteBranch is a function. +func TestGitCommandDeleteBranch(t *testing.T) { + type scenario struct { + testName string + branch string + force bool + command func(string, ...string) *exec.Cmd + test func(error) + } + + scenarios := []scenario{ + { + "Delete a branch", + "test", + false, + func(cmd string, args ...string) *exec.Cmd { + assert.EqualValues(t, "git", cmd) + assert.EqualValues(t, []string{"branch", "-d", "test"}, args) + + return secureexec.Command("echo") + }, + func(err error) { + assert.NoError(t, err) + }, + }, + { + "Force delete a branch", + "test", + true, + func(cmd string, args ...string) *exec.Cmd { + assert.EqualValues(t, "git", cmd) + assert.EqualValues(t, []string{"branch", "-D", "test"}, args) + + return secureexec.Command("echo") + }, + func(err error) { + assert.NoError(t, err) + }, + }, + } + + for _, s := range scenarios { + t.Run(s.testName, func(t *testing.T) { + gitCmd := NewDummyGitCommand() + gitCmd.OSCommand.Command = s.command + s.test(gitCmd.DeleteBranch(s.branch, s.force)) + }) + } +} + +// TestGitCommandMerge is a function. +func TestGitCommandMerge(t *testing.T) { + gitCmd := NewDummyGitCommand() + gitCmd.OSCommand.Command = func(cmd string, args ...string) *exec.Cmd { + assert.EqualValues(t, "git", cmd) + assert.EqualValues(t, []string{"merge", "--no-edit", "test"}, args) + + return secureexec.Command("echo") + } + + assert.NoError(t, gitCmd.Merge("test", MergeOpts{})) +} + +// TestGitCommandCheckout is a function. +func TestGitCommandCheckout(t *testing.T) { + type scenario struct { + testName string + command func(string, ...string) *exec.Cmd + test func(error) + force bool + } + + scenarios := []scenario{ + { + "Checkout", + func(cmd string, args ...string) *exec.Cmd { + assert.EqualValues(t, "git", cmd) + assert.EqualValues(t, []string{"checkout", "test"}, args) + + return secureexec.Command("echo") + }, + func(err error) { + assert.NoError(t, err) + }, + false, + }, + { + "Checkout forced", + func(cmd string, args ...string) *exec.Cmd { + assert.EqualValues(t, "git", cmd) + assert.EqualValues(t, []string{"checkout", "--force", "test"}, args) + + return secureexec.Command("echo") + }, + func(err error) { + assert.NoError(t, err) + }, + true, + }, + } + + for _, s := range scenarios { + t.Run(s.testName, func(t *testing.T) { + gitCmd := NewDummyGitCommand() + gitCmd.OSCommand.Command = s.command + s.test(gitCmd.Checkout("test", CheckoutOptions{Force: s.force})) + }) + } +} + +// TestGitCommandGetBranchGraph is a function. +func TestGitCommandGetBranchGraph(t *testing.T) { + gitCmd := NewDummyGitCommand() + gitCmd.OSCommand.Command = func(cmd string, args ...string) *exec.Cmd { + assert.EqualValues(t, "git", cmd) + assert.EqualValues(t, []string{"log", "--graph", "--color=always", "--abbrev-commit", "--decorate", "--date=relative", "--pretty=medium", "test", "--"}, args) + return secureexec.Command("echo") + } + _, err := gitCmd.GetBranchGraph("test") + assert.NoError(t, err) +} + +func TestGitCommandGetAllBranchGraph(t *testing.T) { + gitCmd := NewDummyGitCommand() + gitCmd.OSCommand.Command = func(cmd string, args ...string) *exec.Cmd { + assert.EqualValues(t, "git", cmd) + assert.EqualValues(t, []string{"log", "--graph", "--all", "--color=always", "--abbrev-commit", "--decorate", "--date=relative", "--pretty=medium"}, args) + return secureexec.Command("echo") + } + cmdStr := gitCmd.Config.GetUserConfig().Git.AllBranchesLogCmd + _, err := gitCmd.OSCommand.RunCommandWithOutput(cmdStr) + assert.NoError(t, err) +} + +// TestGitCommandCurrentBranchName is a function. +func TestGitCommandCurrentBranchName(t *testing.T) { + type scenario struct { + testName string + command func(string, ...string) *exec.Cmd + test func(string, string, error) + } + + scenarios := []scenario{ + { + "says we are on the master branch if we are", + func(cmd string, args ...string) *exec.Cmd { + assert.Equal(t, "git", cmd) + return secureexec.Command("echo", "master") + }, + func(name string, displayname string, err error) { + assert.NoError(t, err) + assert.EqualValues(t, "master", name) + assert.EqualValues(t, "master", displayname) + }, + }, + { + "falls back to git `git branch --contains` if symbolic-ref fails", + func(cmd string, args ...string) *exec.Cmd { + assert.EqualValues(t, "git", cmd) + + switch args[0] { + case "symbolic-ref": + assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args) + return secureexec.Command("test") + case "branch": + assert.EqualValues(t, []string{"branch", "--contains"}, args) + return secureexec.Command("echo", "* master") + } + + return nil + }, + func(name string, displayname string, err error) { + assert.NoError(t, err) + assert.EqualValues(t, "master", name) + assert.EqualValues(t, "master", displayname) + }, + }, + { + "handles a detached head", + func(cmd string, args ...string) *exec.Cmd { + assert.EqualValues(t, "git", cmd) + + switch args[0] { + case "symbolic-ref": + assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args) + return secureexec.Command("test") + case "branch": + assert.EqualValues(t, []string{"branch", "--contains"}, args) + return secureexec.Command("echo", "* (HEAD detached at 123abcd)") + } + + return nil + }, + func(name string, displayname string, err error) { + assert.NoError(t, err) + assert.EqualValues(t, "123abcd", name) + assert.EqualValues(t, "(HEAD detached at 123abcd)", displayname) + }, + }, + { + "bubbles up error if there is one", + func(cmd string, args ...string) *exec.Cmd { + assert.Equal(t, "git", cmd) + return secureexec.Command("test") + }, + func(name string, displayname string, err error) { + assert.Error(t, err) + assert.EqualValues(t, "", name) + assert.EqualValues(t, "", displayname) + }, + }, + } + + for _, s := range scenarios { + t.Run(s.testName, func(t *testing.T) { + gitCmd := NewDummyGitCommand() + gitCmd.OSCommand.Command = s.command + s.test(gitCmd.CurrentBranchName()) + }) + } +} + +// TestGitCommandResetHard is a function. +func TestGitCommandResetHard(t *testing.T) { + type scenario struct { + testName string + ref string + command func(string, ...string) *exec.Cmd + test func(error) + } + + scenarios := []scenario{ + { + "valid case", + "HEAD", + test.CreateMockCommand(t, []*test.CommandSwapper{ + { + Expect: `git reset --hard HEAD`, + Replace: "echo", + }, + }), + func(err error) { + assert.NoError(t, err) + }, + }, + } + + gitCmd := NewDummyGitCommand() + + for _, s := range scenarios { + t.Run(s.testName, func(t *testing.T) { + gitCmd.OSCommand.Command = s.command + s.test(gitCmd.ResetHard(s.ref)) + }) + } +} diff --git a/pkg/commands/commits.go b/pkg/commands/commits.go index 74043518d..3582131a6 100644 --- a/pkg/commands/commits.go +++ b/pkg/commands/commits.go @@ -19,20 +19,19 @@ func (c *GitCommand) ResetToCommit(sha string, strength string, options oscomman return c.OSCommand.RunCommandWithOptions(fmt.Sprintf("git reset --%s %s", strength, sha), options) } -// Commit commits to git -func (c *GitCommand) Commit(message string, flags string) (*exec.Cmd, error) { +func (c *GitCommand) CommitCmdStr(message string, flags string) string { splitMessage := strings.Split(message, "\n") lineArgs := "" for _, line := range splitMessage { lineArgs += fmt.Sprintf(" -m %s", c.OSCommand.Quote(line)) } - command := fmt.Sprintf("git commit %s%s", flags, lineArgs) - if c.usingGpg() { - return c.OSCommand.ShellCommandFromString(command), nil + flagsStr := "" + if flags != "" { + flagsStr = fmt.Sprintf(" %s", flags) } - return nil, c.OSCommand.RunCommand(command) + return fmt.Sprintf("git commit%s%s", flagsStr, lineArgs) } // Get the subject of the HEAD commit @@ -50,18 +49,17 @@ func (c *GitCommand) GetCommitMessage(commitSha string) (string, error) { } // AmendHead amends HEAD with whatever is staged in your working tree -func (c *GitCommand) AmendHead() (*exec.Cmd, error) { - command := "git commit --amend --no-edit --allow-empty" - if c.usingGpg() { - return c.OSCommand.ShellCommandFromString(command), nil - } - - return nil, c.OSCommand.RunCommand(command) +func (c *GitCommand) AmendHead() error { + return c.OSCommand.RunCommand(c.AmendHeadCmdStr()) } -// PrepareCommitAmendSubProcess prepares a subprocess for `git commit --amend --allow-empty` -func (c *GitCommand) PrepareCommitAmendSubProcess() *exec.Cmd { - return c.OSCommand.PrepareSubProcess("git", "commit", "--amend", "--allow-empty") +// PrepareCommitAmendHeadSubProcess prepares a subprocess for `git commit --amend --allow-empty` +func (c *GitCommand) PrepareCommitAmendHeadSubProcess() *exec.Cmd { + return c.OSCommand.ShellCommandFromString(c.AmendHeadCmdStr()) +} + +func (c *GitCommand) AmendHeadCmdStr() string { + return "git commit --amend --no-edit --allow-empty" } func (c *GitCommand) ShowCmdStr(sha string, filterPath string) string { diff --git a/pkg/commands/commits_test.go b/pkg/commands/commits_test.go new file mode 100644 index 000000000..f979e9507 --- /dev/null +++ b/pkg/commands/commits_test.go @@ -0,0 +1,111 @@ +package commands + +import ( + "os/exec" + "testing" + + "github.com/jesseduffield/lazygit/pkg/commands/oscommands" + "github.com/jesseduffield/lazygit/pkg/secureexec" + "github.com/jesseduffield/lazygit/pkg/test" + "github.com/stretchr/testify/assert" +) + +// TestGitCommandRenameCommit is a function. +func TestGitCommandRenameCommit(t *testing.T) { + gitCmd := NewDummyGitCommand() + gitCmd.OSCommand.Command = func(cmd string, args ...string) *exec.Cmd { + assert.EqualValues(t, "git", cmd) + assert.EqualValues(t, []string{"commit", "--allow-empty", "--amend", "--only", "-m", "test"}, args) + + return secureexec.Command("echo") + } + + assert.NoError(t, gitCmd.RenameCommit("test")) +} + +// TestGitCommandResetToCommit is a function. +func TestGitCommandResetToCommit(t *testing.T) { + gitCmd := NewDummyGitCommand() + gitCmd.OSCommand.Command = func(cmd string, args ...string) *exec.Cmd { + assert.EqualValues(t, "git", cmd) + assert.EqualValues(t, []string{"reset", "--hard", "78976bc"}, args) + + return secureexec.Command("echo") + } + + assert.NoError(t, gitCmd.ResetToCommit("78976bc", "hard", oscommands.RunCommandOptions{})) +} + +// TestGitCommandCommitStr is a function. +func TestGitCommandCommitStr(t *testing.T) { + type scenario struct { + testName string + message string + flags string + expected string + } + + scenarios := []scenario{ + { + testName: "Commit", + message: "test", + flags: "", + expected: "git commit -m \"test\"", + }, + { + testName: "Commit with --no-verify flag", + message: "test", + flags: "--no-verify", + expected: "git commit --no-verify -m \"test\"", + }, + { + testName: "Commit with multiline message", + message: "line1\nline2", + flags: "", + expected: "git commit -m \"line1\" -m \"line2\"", + }, + } + + for _, s := range scenarios { + t.Run(s.testName, func(t *testing.T) { + gitCmd := NewDummyGitCommand() + cmdStr := gitCmd.CommitCmdStr(s.message, s.flags) + assert.Equal(t, s.expected, cmdStr) + }) + } +} + +// TestGitCommandCreateFixupCommit is a function. +func TestGitCommandCreateFixupCommit(t *testing.T) { + type scenario struct { + testName string + sha string + command func(string, ...string) *exec.Cmd + test func(error) + } + + scenarios := []scenario{ + { + "valid case", + "12345", + test.CreateMockCommand(t, []*test.CommandSwapper{ + { + Expect: `git commit --fixup=12345`, + Replace: "echo", + }, + }), + func(err error) { + assert.NoError(t, err) + }, + }, + } + + gitCmd := NewDummyGitCommand() + + for _, s := range scenarios { + t.Run(s.testName, func(t *testing.T) { + gitCmd.OSCommand.Command = s.command + s.test(gitCmd.CreateFixupCommit(s.sha)) + }) + } +} diff --git a/pkg/commands/config.go b/pkg/commands/config.go index 0bb310e6d..9600c3403 100644 --- a/pkg/commands/config.go +++ b/pkg/commands/config.go @@ -46,3 +46,17 @@ func (c *GitCommand) GetConfigValue(key string) string { output, _ := c.getGitConfigValue(key) return output } + +// UsingGpg tells us whether the user has gpg enabled so that we can know +// whether we need to run a subprocess to allow them to enter their password +func (c *GitCommand) UsingGpg() bool { + overrideGpg := c.Config.GetUserConfig().Git.OverrideGpg + if overrideGpg { + return false + } + + gpgsign := c.GetConfigValue("commit.gpgsign") + value := strings.ToLower(gpgsign) + + return value == "true" || value == "1" || value == "yes" || value == "on" +} diff --git a/pkg/commands/config_test.go b/pkg/commands/config_test.go new file mode 100644 index 000000000..c16b53901 --- /dev/null +++ b/pkg/commands/config_test.go @@ -0,0 +1,70 @@ +package commands + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +// TestGitCommandUsingGpg is a function. +func TestGitCommandUsingGpg(t *testing.T) { + type scenario struct { + testName string + getGitConfigValue func(string) (string, error) + test func(bool) + } + + scenarios := []scenario{ + { + "Option global and local config commit.gpgsign is not set", + func(string) (string, error) { return "", nil }, + func(gpgEnabled bool) { + assert.False(t, gpgEnabled) + }, + }, + { + "Option commit.gpgsign is true", + func(string) (string, error) { + return "True", nil + }, + func(gpgEnabled bool) { + assert.True(t, gpgEnabled) + }, + }, + { + "Option commit.gpgsign is on", + func(string) (string, error) { + return "ON", nil + }, + func(gpgEnabled bool) { + assert.True(t, gpgEnabled) + }, + }, + { + "Option commit.gpgsign is yes", + func(string) (string, error) { + return "YeS", nil + }, + func(gpgEnabled bool) { + assert.True(t, gpgEnabled) + }, + }, + { + "Option commit.gpgsign is 1", + func(string) (string, error) { + return "1", nil + }, + func(gpgEnabled bool) { + assert.True(t, gpgEnabled) + }, + }, + } + + for _, s := range scenarios { + t.Run(s.testName, func(t *testing.T) { + gitCmd := NewDummyGitCommand() + gitCmd.getGitConfigValue = s.getGitConfigValue + s.test(gitCmd.UsingGpg()) + }) + } +} diff --git a/pkg/commands/files.go b/pkg/commands/files.go index fcf3ea51e..579a3a252 100644 --- a/pkg/commands/files.go +++ b/pkg/commands/files.go @@ -2,16 +2,20 @@ package commands import ( "fmt" + "io/ioutil" "os" "os/exec" "path/filepath" + "testing" "time" "github.com/go-errors/errors" "github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/gui/filetree" + "github.com/jesseduffield/lazygit/pkg/secureexec" + "github.com/jesseduffield/lazygit/pkg/test" "github.com/jesseduffield/lazygit/pkg/utils" - "github.com/mgutz/str" + "github.com/stretchr/testify/assert" ) // CatFile obtains the content of a file @@ -267,10 +271,7 @@ func (c *GitCommand) DiscardOldFileChanges(commits []*models.Commit, commitIndex } // amend the commit - cmd, err := c.AmendHead() - if cmd != nil { - return errors.New("received unexpected pointer to cmd") - } + err := c.AmendHead() if err != nil { return err } @@ -314,9 +315,7 @@ func (c *GitCommand) ResetAndClean() error { return c.RemoveUntrackedFiles() } -// EditFile opens a file in a subprocess using whatever editor is available, -// falling back to core.editor, GIT_EDITOR, VISUAL, EDITOR, then vi -func (c *GitCommand) EditFile(filename string) (*exec.Cmd, error) { +func (c *GitCommand) EditFileCmdStr(filename string) (string, error) { editor := c.GetConfigValue("core.editor") if editor == "" { @@ -334,10 +333,386 @@ func (c *GitCommand) EditFile(filename string) (*exec.Cmd, error) { } } if editor == "" { - return nil, errors.New("No editor defined in $GIT_EDITOR, $VISUAL, $EDITOR, or git config") + return "", errors.New("No editor defined in $GIT_EDITOR, $VISUAL, $EDITOR, or git config") } - splitCmd := str.ToArgv(fmt.Sprintf("%s %s", editor, c.OSCommand.Quote(filename))) - - return c.OSCommand.PrepareSubProcess(splitCmd[0], splitCmd[1:]...), nil + return fmt.Sprintf("%s %s", editor, c.OSCommand.Quote(filename)), nil +} + +func TestGitCommandApplyPatch(t *testing.T) { + type scenario struct { + testName string + command func(string, ...string) *exec.Cmd + test func(error) + } + + scenarios := []scenario{ + { + "valid case", + func(cmd string, args ...string) *exec.Cmd { + assert.Equal(t, "git", cmd) + assert.EqualValues(t, []string{"apply", "--cached"}, args[0:2]) + filename := args[2] + content, err := ioutil.ReadFile(filename) + assert.NoError(t, err) + + assert.Equal(t, "test", string(content)) + + return secureexec.Command("echo", "done") + }, + func(err error) { + assert.NoError(t, err) + }, + }, + { + "command returns error", + func(cmd string, args ...string) *exec.Cmd { + assert.Equal(t, "git", cmd) + assert.EqualValues(t, []string{"apply", "--cached"}, args[0:2]) + filename := args[2] + // TODO: Ideally we want to mock out OSCommand here so that we're not + // double handling testing it's CreateTempFile functionality, + // but it is going to take a bit of work to make a proper mock for it + // so I'm leaving it for another PR + content, err := ioutil.ReadFile(filename) + assert.NoError(t, err) + + assert.Equal(t, "test", string(content)) + + return secureexec.Command("test") + }, + func(err error) { + assert.Error(t, err) + }, + }, + } + + for _, s := range scenarios { + t.Run(s.testName, func(t *testing.T) { + gitCmd := NewDummyGitCommand() + gitCmd.OSCommand.Command = s.command + s.test(gitCmd.ApplyPatch("test", "cached")) + }) + } +} + +// TestGitCommandDiscardOldFileChanges is a function. +func TestGitCommandDiscardOldFileChanges(t *testing.T) { + type scenario struct { + testName string + getGitConfigValue func(string) (string, error) + commits []*models.Commit + commitIndex int + fileName string + command func(string, ...string) *exec.Cmd + test func(error) + } + + scenarios := []scenario{ + { + "returns error when index outside of range of commits", + func(string) (string, error) { + return "", nil + }, + []*models.Commit{}, + 0, + "test999.txt", + nil, + func(err error) { + assert.Error(t, err) + }, + }, + { + "returns error when using gpg", + func(string) (string, error) { + return "true", nil + }, + []*models.Commit{{Name: "commit", Sha: "123456"}}, + 0, + "test999.txt", + nil, + func(err error) { + assert.Error(t, err) + }, + }, + { + "checks out file if it already existed", + func(string) (string, error) { + return "", nil + }, + []*models.Commit{ + {Name: "commit", Sha: "123456"}, + {Name: "commit2", Sha: "abcdef"}, + }, + 0, + "test999.txt", + test.CreateMockCommand(t, []*test.CommandSwapper{ + { + Expect: "git rebase --interactive --autostash --keep-empty abcdef", + Replace: "echo", + }, + { + Expect: "git cat-file -e HEAD^:test999.txt", + Replace: "echo", + }, + { + Expect: "git checkout HEAD^ test999.txt", + Replace: "echo", + }, + { + Expect: "git commit --amend --no-edit --allow-empty", + Replace: "echo", + }, + { + Expect: "git rebase --continue", + Replace: "echo", + }, + }), + 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 + } + + gitCmd := NewDummyGitCommand() + + for _, s := range scenarios { + t.Run(s.testName, func(t *testing.T) { + gitCmd.OSCommand.Command = s.command + gitCmd.getGitConfigValue = s.getGitConfigValue + s.test(gitCmd.DiscardOldFileChanges(s.commits, s.commitIndex, s.fileName)) + }) + } +} + +// TestGitCommandDiscardUnstagedFileChanges is a function. +func TestGitCommandDiscardUnstagedFileChanges(t *testing.T) { + type scenario struct { + testName string + file *models.File + command func(string, ...string) *exec.Cmd + test func(error) + } + + scenarios := []scenario{ + { + "valid case", + &models.File{Name: "test.txt"}, + test.CreateMockCommand(t, []*test.CommandSwapper{ + { + Expect: `git checkout -- "test.txt"`, + Replace: "echo", + }, + }), + func(err error) { + assert.NoError(t, err) + }, + }, + } + + gitCmd := NewDummyGitCommand() + + for _, s := range scenarios { + t.Run(s.testName, func(t *testing.T) { + gitCmd.OSCommand.Command = s.command + s.test(gitCmd.DiscardUnstagedFileChanges(s.file)) + }) + } +} + +// TestGitCommandDiscardAnyUnstagedFileChanges is a function. +func TestGitCommandDiscardAnyUnstagedFileChanges(t *testing.T) { + type scenario struct { + testName string + command func(string, ...string) *exec.Cmd + test func(error) + } + + scenarios := []scenario{ + { + "valid case", + test.CreateMockCommand(t, []*test.CommandSwapper{ + { + Expect: `git checkout -- .`, + Replace: "echo", + }, + }), + func(err error) { + assert.NoError(t, err) + }, + }, + } + + gitCmd := NewDummyGitCommand() + + for _, s := range scenarios { + t.Run(s.testName, func(t *testing.T) { + gitCmd.OSCommand.Command = s.command + s.test(gitCmd.DiscardAnyUnstagedFileChanges()) + }) + } +} + +// TestGitCommandRemoveUntrackedFiles is a function. +func TestGitCommandRemoveUntrackedFiles(t *testing.T) { + type scenario struct { + testName string + command func(string, ...string) *exec.Cmd + test func(error) + } + + scenarios := []scenario{ + { + "valid case", + test.CreateMockCommand(t, []*test.CommandSwapper{ + { + Expect: `git clean -fd`, + Replace: "echo", + }, + }), + func(err error) { + assert.NoError(t, err) + }, + }, + } + + gitCmd := NewDummyGitCommand() + + for _, s := range scenarios { + t.Run(s.testName, func(t *testing.T) { + gitCmd.OSCommand.Command = s.command + s.test(gitCmd.RemoveUntrackedFiles()) + }) + } +} + +// TestEditFileCmdStr is a function. +func TestEditFileCmdStr(t *testing.T) { + type scenario struct { + filename string + command func(string, ...string) *exec.Cmd + getenv func(string) string + getGitConfigValue func(string) (string, error) + test func(string, error) + } + + scenarios := []scenario{ + { + "test", + func(name string, arg ...string) *exec.Cmd { + return secureexec.Command("exit", "1") + }, + func(env string) string { + return "" + }, + func(cf string) (string, error) { + return "", nil + }, + func(cmdStr string, err error) { + assert.EqualError(t, err, "No editor defined in $GIT_EDITOR, $VISUAL, $EDITOR, or git config") + }, + }, + { + "test", + func(name string, arg ...string) *exec.Cmd { + assert.Equal(t, "which", name) + return secureexec.Command("exit", "1") + }, + func(env string) string { + return "" + }, + func(cf string) (string, error) { + return "nano", nil + }, + func(cmdStr string, err error) { + assert.NoError(t, err) + assert.Equal(t, "nano \"test\"", cmdStr) + }, + }, + { + "test", + func(name string, arg ...string) *exec.Cmd { + assert.Equal(t, "which", name) + return secureexec.Command("exit", "1") + }, + func(env string) string { + if env == "VISUAL" { + return "nano" + } + + return "" + }, + func(cf string) (string, error) { + return "", nil + }, + func(cmdStr string, err error) { + assert.NoError(t, err) + }, + }, + { + "test", + func(name string, arg ...string) *exec.Cmd { + assert.Equal(t, "which", name) + return secureexec.Command("exit", "1") + }, + func(env string) string { + if env == "EDITOR" { + return "emacs" + } + + return "" + }, + func(cf string) (string, error) { + return "", nil + }, + func(cmdStr string, err error) { + assert.NoError(t, err) + assert.Equal(t, "emacs \"test\"", cmdStr) + }, + }, + { + "test", + func(name string, arg ...string) *exec.Cmd { + assert.Equal(t, "which", name) + return secureexec.Command("echo") + }, + func(env string) string { + return "" + }, + func(cf string) (string, error) { + return "", nil + }, + func(cmdStr string, err error) { + assert.NoError(t, err) + assert.Equal(t, "vi \"test\"", cmdStr) + }, + }, + { + "file/with space", + func(name string, args ...string) *exec.Cmd { + assert.Equal(t, "which", name) + return secureexec.Command("echo") + }, + func(env string) string { + return "" + }, + func(cf string) (string, error) { + return "", nil + }, + func(cmdStr string, err error) { + assert.NoError(t, err) + assert.Equal(t, "vi \"file/with space\"", cmdStr) + }, + }, + } + + for _, s := range scenarios { + gitCmd := NewDummyGitCommand() + gitCmd.OSCommand.Command = s.command + gitCmd.OSCommand.Getenv = s.getenv + gitCmd.getGitConfigValue = s.getGitConfigValue + s.test(gitCmd.EditFileCmdStr(s.filename)) + } } diff --git a/pkg/commands/files_test.go b/pkg/commands/files_test.go new file mode 100644 index 000000000..62aa388cb --- /dev/null +++ b/pkg/commands/files_test.go @@ -0,0 +1,467 @@ +package commands + +import ( + "fmt" + "os/exec" + "runtime" + "testing" + + "github.com/jesseduffield/lazygit/pkg/commands/models" + "github.com/jesseduffield/lazygit/pkg/secureexec" + "github.com/jesseduffield/lazygit/pkg/test" + "github.com/stretchr/testify/assert" +) + +// TestGitCommandCatFile tests emitting a file using commands, where commands vary by OS. +func TestGitCommandCatFile(t *testing.T) { + var osCmd string + switch os := runtime.GOOS; os { + case "windows": + osCmd = "type" + default: + osCmd = "cat" + } + gitCmd := NewDummyGitCommand() + gitCmd.OSCommand.Command = func(cmd string, args ...string) *exec.Cmd { + assert.EqualValues(t, osCmd, cmd) + assert.EqualValues(t, []string{"test.txt"}, args) + + return secureexec.Command("echo", "-n", "test") + } + + o, err := gitCmd.CatFile("test.txt") + assert.NoError(t, err) + assert.Equal(t, "test", o) +} + +// TestGitCommandStageFile is a function. +func TestGitCommandStageFile(t *testing.T) { + gitCmd := NewDummyGitCommand() + gitCmd.OSCommand.Command = func(cmd string, args ...string) *exec.Cmd { + assert.EqualValues(t, "git", cmd) + assert.EqualValues(t, []string{"add", "--", "test.txt"}, args) + + return secureexec.Command("echo") + } + + assert.NoError(t, gitCmd.StageFile("test.txt")) +} + +// TestGitCommandUnstageFile is a function. +func TestGitCommandUnstageFile(t *testing.T) { + type scenario struct { + testName string + command func(string, ...string) *exec.Cmd + test func(error) + reset bool + } + + scenarios := []scenario{ + { + "Remove an untracked file from staging", + func(cmd string, args ...string) *exec.Cmd { + assert.EqualValues(t, "git", cmd) + assert.EqualValues(t, []string{"rm", "--cached", "--force", "--", "test.txt"}, args) + + return secureexec.Command("echo") + }, + func(err error) { + assert.NoError(t, err) + }, + false, + }, + { + "Remove a tracked file from staging", + func(cmd string, args ...string) *exec.Cmd { + assert.EqualValues(t, "git", cmd) + assert.EqualValues(t, []string{"reset", "HEAD", "--", "test.txt"}, args) + + return secureexec.Command("echo") + }, + func(err error) { + assert.NoError(t, err) + }, + true, + }, + } + + for _, s := range scenarios { + t.Run(s.testName, func(t *testing.T) { + gitCmd := NewDummyGitCommand() + gitCmd.OSCommand.Command = s.command + s.test(gitCmd.UnStageFile([]string{"test.txt"}, s.reset)) + }) + } +} + +// TestGitCommandDiscardAllFileChanges is a function. +// 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 + command func() (func(string, ...string) *exec.Cmd, *[][]string) + test func(*[][]string, error) + file *models.File + removeFile func(string) error + } + + scenarios := []scenario{ + { + "An error occurred when resetting", + func() (func(string, ...string) *exec.Cmd, *[][]string) { + cmdsCalled := [][]string{} + return func(cmd string, args ...string) *exec.Cmd { + cmdsCalled = append(cmdsCalled, args) + + return secureexec.Command("test") + }, &cmdsCalled + }, + func(cmdsCalled *[][]string, err error) { + assert.Error(t, err) + assert.Len(t, *cmdsCalled, 1) + assert.EqualValues(t, *cmdsCalled, [][]string{ + {"reset", "--", "test"}, + }) + }, + &models.File{ + Name: "test", + HasStagedChanges: true, + }, + func(string) error { + return nil + }, + }, + { + "An error occurred when removing file", + func() (func(string, ...string) *exec.Cmd, *[][]string) { + cmdsCalled := [][]string{} + return func(cmd string, args ...string) *exec.Cmd { + cmdsCalled = append(cmdsCalled, args) + + return secureexec.Command("test") + }, &cmdsCalled + }, + func(cmdsCalled *[][]string, err error) { + assert.Error(t, err) + assert.EqualError(t, err, "an error occurred when removing file") + assert.Len(t, *cmdsCalled, 0) + }, + &models.File{ + Name: "test", + Tracked: false, + Added: true, + }, + func(string) error { + return fmt.Errorf("an error occurred when removing file") + }, + }, + { + "An error occurred with checkout", + func() (func(string, ...string) *exec.Cmd, *[][]string) { + cmdsCalled := [][]string{} + return func(cmd string, args ...string) *exec.Cmd { + cmdsCalled = append(cmdsCalled, args) + + return secureexec.Command("test") + }, &cmdsCalled + }, + func(cmdsCalled *[][]string, err error) { + assert.Error(t, err) + assert.Len(t, *cmdsCalled, 1) + assert.EqualValues(t, *cmdsCalled, [][]string{ + {"checkout", "--", "test"}, + }) + }, + &models.File{ + Name: "test", + Tracked: true, + HasStagedChanges: false, + }, + func(string) error { + return nil + }, + }, + { + "Checkout only", + func() (func(string, ...string) *exec.Cmd, *[][]string) { + cmdsCalled := [][]string{} + return func(cmd string, args ...string) *exec.Cmd { + cmdsCalled = append(cmdsCalled, args) + + return secureexec.Command("echo") + }, &cmdsCalled + }, + func(cmdsCalled *[][]string, err error) { + assert.NoError(t, err) + assert.Len(t, *cmdsCalled, 1) + assert.EqualValues(t, *cmdsCalled, [][]string{ + {"checkout", "--", "test"}, + }) + }, + &models.File{ + Name: "test", + Tracked: true, + HasStagedChanges: false, + }, + func(string) error { + return nil + }, + }, + { + "Reset and checkout staged changes", + func() (func(string, ...string) *exec.Cmd, *[][]string) { + cmdsCalled := [][]string{} + return func(cmd string, args ...string) *exec.Cmd { + cmdsCalled = append(cmdsCalled, args) + + return secureexec.Command("echo") + }, &cmdsCalled + }, + func(cmdsCalled *[][]string, err error) { + assert.NoError(t, err) + assert.Len(t, *cmdsCalled, 2) + assert.EqualValues(t, *cmdsCalled, [][]string{ + {"reset", "--", "test"}, + {"checkout", "--", "test"}, + }) + }, + &models.File{ + Name: "test", + Tracked: true, + HasStagedChanges: true, + }, + func(string) error { + return nil + }, + }, + { + "Reset and checkout merge conflicts", + func() (func(string, ...string) *exec.Cmd, *[][]string) { + cmdsCalled := [][]string{} + return func(cmd string, args ...string) *exec.Cmd { + cmdsCalled = append(cmdsCalled, args) + + return secureexec.Command("echo") + }, &cmdsCalled + }, + func(cmdsCalled *[][]string, err error) { + assert.NoError(t, err) + assert.Len(t, *cmdsCalled, 2) + assert.EqualValues(t, *cmdsCalled, [][]string{ + {"reset", "--", "test"}, + {"checkout", "--", "test"}, + }) + }, + &models.File{ + Name: "test", + Tracked: true, + HasMergeConflicts: true, + }, + func(string) error { + return nil + }, + }, + { + "Reset and remove", + func() (func(string, ...string) *exec.Cmd, *[][]string) { + cmdsCalled := [][]string{} + return func(cmd string, args ...string) *exec.Cmd { + cmdsCalled = append(cmdsCalled, args) + + return secureexec.Command("echo") + }, &cmdsCalled + }, + func(cmdsCalled *[][]string, err error) { + assert.NoError(t, err) + assert.Len(t, *cmdsCalled, 1) + assert.EqualValues(t, *cmdsCalled, [][]string{ + {"reset", "--", "test"}, + }) + }, + &models.File{ + Name: "test", + Tracked: false, + Added: true, + HasStagedChanges: true, + }, + func(filename string) error { + assert.Equal(t, "test", filename) + return nil + }, + }, + { + "Remove only", + func() (func(string, ...string) *exec.Cmd, *[][]string) { + cmdsCalled := [][]string{} + return func(cmd string, args ...string) *exec.Cmd { + cmdsCalled = append(cmdsCalled, args) + + return secureexec.Command("echo") + }, &cmdsCalled + }, + func(cmdsCalled *[][]string, err error) { + assert.NoError(t, err) + assert.Len(t, *cmdsCalled, 0) + }, + &models.File{ + Name: "test", + Tracked: false, + Added: true, + HasStagedChanges: false, + }, + func(filename string) error { + assert.Equal(t, "test", filename) + return nil + }, + }, + } + + for _, s := range scenarios { + t.Run(s.testName, func(t *testing.T) { + var cmdsCalled *[][]string + gitCmd := NewDummyGitCommand() + gitCmd.OSCommand.Command, cmdsCalled = s.command() + gitCmd.removeFile = s.removeFile + s.test(cmdsCalled, gitCmd.DiscardAllFileChanges(s.file)) + }) + } +} + +// TestGitCommandDiff is a function. +func TestGitCommandDiff(t *testing.T) { + type scenario struct { + testName string + command func(string, ...string) *exec.Cmd + file *models.File + plain bool + cached bool + } + + scenarios := []scenario{ + { + "Default case", + func(cmd string, args ...string) *exec.Cmd { + assert.EqualValues(t, "git", cmd) + assert.EqualValues(t, []string{"diff", "--submodule", "--no-ext-diff", "--color=always", "--", "test.txt"}, args) + + return secureexec.Command("echo") + }, + &models.File{ + Name: "test.txt", + HasStagedChanges: false, + Tracked: true, + }, + false, + false, + }, + { + "cached", + func(cmd string, args ...string) *exec.Cmd { + assert.EqualValues(t, "git", cmd) + assert.EqualValues(t, []string{"diff", "--submodule", "--no-ext-diff", "--color=always", "--cached", "--", "test.txt"}, args) + + return secureexec.Command("echo") + }, + &models.File{ + Name: "test.txt", + HasStagedChanges: false, + Tracked: true, + }, + false, + true, + }, + { + "plain", + func(cmd string, args ...string) *exec.Cmd { + assert.EqualValues(t, "git", cmd) + assert.EqualValues(t, []string{"diff", "--submodule", "--no-ext-diff", "--color=never", "--", "test.txt"}, args) + + return secureexec.Command("echo") + }, + &models.File{ + Name: "test.txt", + HasStagedChanges: false, + Tracked: true, + }, + true, + false, + }, + { + "File not tracked and file has no staged changes", + func(cmd string, args ...string) *exec.Cmd { + assert.EqualValues(t, "git", cmd) + assert.EqualValues(t, []string{"diff", "--submodule", "--no-ext-diff", "--color=always", "--no-index", "--", "/dev/null", "test.txt"}, args) + + return secureexec.Command("echo") + }, + &models.File{ + Name: "test.txt", + HasStagedChanges: false, + Tracked: false, + }, + false, + false, + }, + } + + for _, s := range scenarios { + t.Run(s.testName, func(t *testing.T) { + gitCmd := NewDummyGitCommand() + gitCmd.OSCommand.Command = s.command + gitCmd.WorktreeFileDiff(s.file, s.plain, s.cached) + }) + } +} + +// TestGitCommandCheckoutFile is a function. +func TestGitCommandCheckoutFile(t *testing.T) { + type scenario struct { + testName string + commitSha string + fileName string + command func(string, ...string) *exec.Cmd + test func(error) + } + + scenarios := []scenario{ + { + "typical case", + "11af912", + "test999.txt", + test.CreateMockCommand(t, []*test.CommandSwapper{ + { + Expect: "git checkout 11af912 test999.txt", + Replace: "echo", + }, + }), + func(err error) { + assert.NoError(t, err) + }, + }, + { + "returns error if there is one", + "11af912", + "test999.txt", + test.CreateMockCommand(t, []*test.CommandSwapper{ + { + Expect: "git checkout 11af912 test999.txt", + Replace: "test", + }, + }), + func(err error) { + assert.Error(t, err) + }, + }, + } + + gitCmd := NewDummyGitCommand() + + for _, s := range scenarios { + t.Run(s.testName, func(t *testing.T) { + gitCmd.OSCommand.Command = s.command + s.test(gitCmd.CheckoutFile(s.commitSha, s.fileName)) + }) + } +} diff --git a/pkg/commands/git_test.go b/pkg/commands/git_test.go index afefb921c..e31fe8910 100644 --- a/pkg/commands/git_test.go +++ b/pkg/commands/git_test.go @@ -2,22 +2,15 @@ package commands import ( "fmt" - "io/ioutil" "os" - "os/exec" - "regexp" - "runtime" "testing" "time" "github.com/go-errors/errors" gogit "github.com/jesseduffield/go-git/v5" - "github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/commands/oscommands" "github.com/jesseduffield/lazygit/pkg/config" "github.com/jesseduffield/lazygit/pkg/i18n" - "github.com/jesseduffield/lazygit/pkg/secureexec" - "github.com/jesseduffield/lazygit/pkg/test" "github.com/jesseduffield/lazygit/pkg/utils" "github.com/stretchr/testify/assert" ) @@ -221,1700 +214,6 @@ func TestNewGitCommand(t *testing.T) { } } -// TestGitCommandGetStashEntries is a function. -func TestGitCommandGetStashEntries(t *testing.T) { - type scenario struct { - testName string - command func(string, ...string) *exec.Cmd - test func([]*models.StashEntry) - } - - scenarios := []scenario{ - { - "No stash entries found", - func(string, ...string) *exec.Cmd { - return secureexec.Command("echo") - }, - func(entries []*models.StashEntry) { - assert.Len(t, entries, 0) - }, - }, - { - "Several stash entries found", - func(string, ...string) *exec.Cmd { - return secureexec.Command("echo", "WIP on add-pkg-commands-test: 55c6af2 increase parallel build\nWIP on master: bb86a3f update github template") - }, - func(entries []*models.StashEntry) { - expected := []*models.StashEntry{ - { - Index: 0, - Name: "WIP on add-pkg-commands-test: 55c6af2 increase parallel build", - }, - { - Index: 1, - Name: "WIP on master: bb86a3f update github template", - }, - } - - assert.Len(t, entries, 2) - assert.EqualValues(t, expected, entries) - }, - }, - } - - for _, s := range scenarios { - t.Run(s.testName, func(t *testing.T) { - gitCmd := NewDummyGitCommand() - gitCmd.OSCommand.Command = s.command - - s.test(gitCmd.GetStashEntries("")) - }) - } -} - -// TestGitCommandGetStatusFiles is a function. -func TestGitCommandGetStatusFiles(t *testing.T) { - type scenario struct { - testName string - command func(string, ...string) *exec.Cmd - test func([]*models.File) - } - - scenarios := []scenario{ - { - "No files found", - func(cmd string, args ...string) *exec.Cmd { - return secureexec.Command("echo") - }, - func(files []*models.File) { - assert.Len(t, files, 0) - }, - }, - { - "Several files found", - func(cmd string, args ...string) *exec.Cmd { - return secureexec.Command( - "echo", - "MM file1.txt\nA file3.txt\nAM file2.txt\n?? file4.txt\nUU file5.txt", - ) - }, - func(files []*models.File) { - assert.Len(t, files, 5) - - expected := []*models.File{ - { - Name: "file1.txt", - HasStagedChanges: true, - HasUnstagedChanges: true, - Tracked: true, - Added: false, - Deleted: false, - HasMergeConflicts: false, - HasInlineMergeConflicts: false, - DisplayString: "MM file1.txt", - Type: "other", - ShortStatus: "MM", - }, - { - Name: "file3.txt", - HasStagedChanges: true, - HasUnstagedChanges: false, - Tracked: false, - Added: true, - Deleted: false, - HasMergeConflicts: false, - HasInlineMergeConflicts: false, - DisplayString: "A file3.txt", - Type: "other", - ShortStatus: "A ", - }, - { - Name: "file2.txt", - HasStagedChanges: true, - HasUnstagedChanges: true, - Tracked: false, - Added: true, - Deleted: false, - HasMergeConflicts: false, - HasInlineMergeConflicts: false, - DisplayString: "AM file2.txt", - Type: "other", - ShortStatus: "AM", - }, - { - Name: "file4.txt", - HasStagedChanges: false, - HasUnstagedChanges: true, - Tracked: false, - Added: true, - Deleted: false, - HasMergeConflicts: false, - HasInlineMergeConflicts: false, - DisplayString: "?? file4.txt", - Type: "other", - ShortStatus: "??", - }, - { - Name: "file5.txt", - HasStagedChanges: false, - HasUnstagedChanges: true, - Tracked: true, - Added: false, - Deleted: false, - HasMergeConflicts: true, - HasInlineMergeConflicts: true, - DisplayString: "UU file5.txt", - Type: "other", - ShortStatus: "UU", - }, - } - - assert.EqualValues(t, expected, files) - }, - }, - } - - for _, s := range scenarios { - t.Run(s.testName, func(t *testing.T) { - gitCmd := NewDummyGitCommand() - gitCmd.OSCommand.Command = s.command - - s.test(gitCmd.GetStatusFiles(GetStatusFileOptions{})) - }) - } -} - -// TestGitCommandStashDo is a function. -func TestGitCommandStashDo(t *testing.T) { - gitCmd := NewDummyGitCommand() - gitCmd.OSCommand.Command = func(cmd string, args ...string) *exec.Cmd { - assert.EqualValues(t, "git", cmd) - assert.EqualValues(t, []string{"stash", "drop", "stash@{1}"}, args) - - return secureexec.Command("echo") - } - - assert.NoError(t, gitCmd.StashDo(1, "drop")) -} - -// TestGitCommandStashSave is a function. -func TestGitCommandStashSave(t *testing.T) { - gitCmd := NewDummyGitCommand() - gitCmd.OSCommand.Command = func(cmd string, args ...string) *exec.Cmd { - assert.EqualValues(t, "git", cmd) - assert.EqualValues(t, []string{"stash", "save", "A stash message"}, args) - - return secureexec.Command("echo") - } - - assert.NoError(t, gitCmd.StashSave("A stash message")) -} - -// TestGitCommandCommitAmend is a function. -func TestGitCommandCommitAmend(t *testing.T) { - gitCmd := NewDummyGitCommand() - gitCmd.OSCommand.Command = func(cmd string, args ...string) *exec.Cmd { - assert.EqualValues(t, "git", cmd) - assert.EqualValues(t, []string{"commit", "--amend", "--allow-empty"}, args) - - return secureexec.Command("echo") - } - - _, err := gitCmd.PrepareCommitAmendSubProcess().CombinedOutput() - assert.NoError(t, err) -} - -// TestGitCommandGetCommitDifferences is a function. -func TestGitCommandGetCommitDifferences(t *testing.T) { - type scenario struct { - testName string - command func(string, ...string) *exec.Cmd - test func(string, string) - } - - scenarios := []scenario{ - { - "Can't retrieve pushable count", - func(string, ...string) *exec.Cmd { - return secureexec.Command("test") - }, - func(pushableCount string, pullableCount string) { - assert.EqualValues(t, "?", pushableCount) - assert.EqualValues(t, "?", pullableCount) - }, - }, - { - "Can't retrieve pullable count", - func(cmd string, args ...string) *exec.Cmd { - if args[1] == "HEAD..@{u}" { - return secureexec.Command("test") - } - - return secureexec.Command("echo") - }, - func(pushableCount string, pullableCount string) { - assert.EqualValues(t, "?", pushableCount) - assert.EqualValues(t, "?", pullableCount) - }, - }, - { - "Retrieve pullable and pushable count", - func(cmd string, args ...string) *exec.Cmd { - if args[1] == "HEAD..@{u}" { - return secureexec.Command("echo", "10") - } - - return secureexec.Command("echo", "11") - }, - func(pushableCount string, pullableCount string) { - assert.EqualValues(t, "11", pushableCount) - assert.EqualValues(t, "10", pullableCount) - }, - }, - } - - for _, s := range scenarios { - t.Run(s.testName, func(t *testing.T) { - gitCmd := NewDummyGitCommand() - gitCmd.OSCommand.Command = s.command - s.test(gitCmd.GetCommitDifferences("HEAD", "@{u}")) - }) - } -} - -// TestGitCommandRenameCommit is a function. -func TestGitCommandRenameCommit(t *testing.T) { - gitCmd := NewDummyGitCommand() - gitCmd.OSCommand.Command = func(cmd string, args ...string) *exec.Cmd { - assert.EqualValues(t, "git", cmd) - assert.EqualValues(t, []string{"commit", "--allow-empty", "--amend", "--only", "-m", "test"}, args) - - return secureexec.Command("echo") - } - - assert.NoError(t, gitCmd.RenameCommit("test")) -} - -// TestGitCommandResetToCommit is a function. -func TestGitCommandResetToCommit(t *testing.T) { - gitCmd := NewDummyGitCommand() - gitCmd.OSCommand.Command = func(cmd string, args ...string) *exec.Cmd { - assert.EqualValues(t, "git", cmd) - assert.EqualValues(t, []string{"reset", "--hard", "78976bc"}, args) - - return secureexec.Command("echo") - } - - assert.NoError(t, gitCmd.ResetToCommit("78976bc", "hard", oscommands.RunCommandOptions{})) -} - -// TestGitCommandNewBranch is a function. -func TestGitCommandNewBranch(t *testing.T) { - gitCmd := NewDummyGitCommand() - gitCmd.OSCommand.Command = func(cmd string, args ...string) *exec.Cmd { - assert.EqualValues(t, "git", cmd) - assert.EqualValues(t, []string{"checkout", "-b", "test", "master"}, args) - - return secureexec.Command("echo") - } - - assert.NoError(t, gitCmd.NewBranch("test", "master")) -} - -// TestGitCommandDeleteBranch is a function. -func TestGitCommandDeleteBranch(t *testing.T) { - type scenario struct { - testName string - branch string - force bool - command func(string, ...string) *exec.Cmd - test func(error) - } - - scenarios := []scenario{ - { - "Delete a branch", - "test", - false, - func(cmd string, args ...string) *exec.Cmd { - assert.EqualValues(t, "git", cmd) - assert.EqualValues(t, []string{"branch", "-d", "test"}, args) - - return secureexec.Command("echo") - }, - func(err error) { - assert.NoError(t, err) - }, - }, - { - "Force delete a branch", - "test", - true, - func(cmd string, args ...string) *exec.Cmd { - assert.EqualValues(t, "git", cmd) - assert.EqualValues(t, []string{"branch", "-D", "test"}, args) - - return secureexec.Command("echo") - }, - func(err error) { - assert.NoError(t, err) - }, - }, - } - - for _, s := range scenarios { - t.Run(s.testName, func(t *testing.T) { - gitCmd := NewDummyGitCommand() - gitCmd.OSCommand.Command = s.command - s.test(gitCmd.DeleteBranch(s.branch, s.force)) - }) - } -} - -// TestGitCommandMerge is a function. -func TestGitCommandMerge(t *testing.T) { - gitCmd := NewDummyGitCommand() - gitCmd.OSCommand.Command = func(cmd string, args ...string) *exec.Cmd { - assert.EqualValues(t, "git", cmd) - assert.EqualValues(t, []string{"merge", "--no-edit", "test"}, args) - - return secureexec.Command("echo") - } - - assert.NoError(t, gitCmd.Merge("test", MergeOpts{})) -} - -// TestGitCommandUsingGpg is a function. -func TestGitCommandUsingGpg(t *testing.T) { - type scenario struct { - testName string - getGitConfigValue func(string) (string, error) - test func(bool) - } - - scenarios := []scenario{ - { - "Option global and local config commit.gpgsign is not set", - func(string) (string, error) { return "", nil }, - func(gpgEnabled bool) { - assert.False(t, gpgEnabled) - }, - }, - { - "Option commit.gpgsign is true", - func(string) (string, error) { - return "True", nil - }, - func(gpgEnabled bool) { - assert.True(t, gpgEnabled) - }, - }, - { - "Option commit.gpgsign is on", - func(string) (string, error) { - return "ON", nil - }, - func(gpgEnabled bool) { - assert.True(t, gpgEnabled) - }, - }, - { - "Option commit.gpgsign is yes", - func(string) (string, error) { - return "YeS", nil - }, - func(gpgEnabled bool) { - assert.True(t, gpgEnabled) - }, - }, - { - "Option commit.gpgsign is 1", - func(string) (string, error) { - return "1", nil - }, - func(gpgEnabled bool) { - assert.True(t, gpgEnabled) - }, - }, - } - - for _, s := range scenarios { - t.Run(s.testName, func(t *testing.T) { - gitCmd := NewDummyGitCommand() - gitCmd.getGitConfigValue = s.getGitConfigValue - s.test(gitCmd.usingGpg()) - }) - } -} - -// TestGitCommandCommit is a function. -func TestGitCommandCommit(t *testing.T) { - type scenario struct { - testName string - command func(string, ...string) *exec.Cmd - getGitConfigValue func(string) (string, error) - test func(*exec.Cmd, error) - flags string - } - - scenarios := []scenario{ - { - "Commit using gpg", - func(cmd string, args ...string) *exec.Cmd { - assert.EqualValues(t, "bash", cmd) - assert.EqualValues(t, []string{"-c", "git commit -m \"test\""}, args) - - return secureexec.Command("echo") - }, - func(string) (string, error) { - return "true", nil - }, - func(cmd *exec.Cmd, err error) { - assert.NotNil(t, cmd) - assert.Nil(t, err) - }, - "", - }, - { - "Commit without using gpg", - func(cmd string, args ...string) *exec.Cmd { - assert.EqualValues(t, "git", cmd) - assert.EqualValues(t, []string{"commit", "-m", "test"}, args) - - return secureexec.Command("echo") - }, - func(string) (string, error) { - return "false", nil - }, - func(cmd *exec.Cmd, err error) { - assert.Nil(t, cmd) - assert.Nil(t, err) - }, - "", - }, - { - "Commit with --no-verify flag", - func(cmd string, args ...string) *exec.Cmd { - assert.EqualValues(t, "git", cmd) - assert.EqualValues(t, []string{"commit", "--no-verify", "-m", "test"}, args) - - return secureexec.Command("echo") - }, - func(string) (string, error) { - return "false", nil - }, - func(cmd *exec.Cmd, err error) { - assert.Nil(t, cmd) - assert.Nil(t, err) - }, - "--no-verify", - }, - { - "Commit without using gpg with an error", - func(cmd string, args ...string) *exec.Cmd { - assert.EqualValues(t, "git", cmd) - assert.EqualValues(t, []string{"commit", "-m", "test"}, args) - - return secureexec.Command("test") - }, - func(string) (string, error) { - return "false", nil - }, - func(cmd *exec.Cmd, err error) { - assert.Nil(t, cmd) - assert.Error(t, err) - }, - "", - }, - } - - for _, s := range scenarios { - t.Run(s.testName, func(t *testing.T) { - gitCmd := NewDummyGitCommand() - gitCmd.getGitConfigValue = s.getGitConfigValue - gitCmd.OSCommand.Command = s.command - s.test(gitCmd.Commit("test", s.flags)) - }) - } -} - -// TestGitCommandAmendHead is a function. -func TestGitCommandAmendHead(t *testing.T) { - type scenario struct { - testName string - command func(string, ...string) *exec.Cmd - getGitConfigValue func(string) (string, error) - test func(*exec.Cmd, error) - } - - scenarios := []scenario{ - { - "Amend commit using gpg", - func(cmd string, args ...string) *exec.Cmd { - assert.EqualValues(t, "bash", cmd) - assert.EqualValues(t, []string{"-c", "git commit --amend --no-edit --allow-empty"}, args) - - return secureexec.Command("echo") - }, - func(string) (string, error) { - return "true", nil - }, - func(cmd *exec.Cmd, err error) { - assert.NotNil(t, cmd) - assert.Nil(t, err) - }, - }, - { - "Amend commit without using gpg", - func(cmd string, args ...string) *exec.Cmd { - assert.EqualValues(t, "git", cmd) - assert.EqualValues(t, []string{"commit", "--amend", "--no-edit", "--allow-empty"}, args) - - return secureexec.Command("echo") - }, - func(string) (string, error) { - return "false", nil - }, - func(cmd *exec.Cmd, err error) { - assert.Nil(t, cmd) - assert.Nil(t, err) - }, - }, - { - "Amend commit without using gpg with an error", - func(cmd string, args ...string) *exec.Cmd { - assert.EqualValues(t, "git", cmd) - assert.EqualValues(t, []string{"commit", "--amend", "--no-edit", "--allow-empty"}, args) - - return secureexec.Command("test") - }, - func(string) (string, error) { - return "false", nil - }, - func(cmd *exec.Cmd, err error) { - assert.Nil(t, cmd) - assert.Error(t, err) - }, - }, - } - - for _, s := range scenarios { - t.Run(s.testName, func(t *testing.T) { - gitCmd := NewDummyGitCommand() - gitCmd.getGitConfigValue = s.getGitConfigValue - gitCmd.OSCommand.Command = s.command - s.test(gitCmd.AmendHead()) - }) - } -} - -// TestGitCommandPush is a function. -func TestGitCommandPush(t *testing.T) { - type scenario struct { - testName string - getGitConfigValue func(string) (string, error) - command func(string, ...string) *exec.Cmd - forcePush bool - test func(error) - } - - scenarios := []scenario{ - { - "Push with force disabled, follow-tags on", - func(string) (string, error) { - return "", nil - }, - func(cmd string, args ...string) *exec.Cmd { - assert.EqualValues(t, "git", cmd) - assert.EqualValues(t, []string{"push", "--follow-tags"}, args) - - return secureexec.Command("echo") - }, - false, - func(err error) { - assert.NoError(t, err) - }, - }, - { - "Push with force enabled, follow-tags on", - func(string) (string, error) { - return "", nil - }, - func(cmd string, args ...string) *exec.Cmd { - assert.EqualValues(t, "git", cmd) - assert.EqualValues(t, []string{"push", "--follow-tags", "--force-with-lease"}, args) - - return secureexec.Command("echo") - }, - true, - func(err error) { - assert.NoError(t, err) - }, - }, - { - "Push with force disabled, follow-tags off", - func(string) (string, error) { - return "false", nil - }, - func(cmd string, args ...string) *exec.Cmd { - assert.EqualValues(t, "git", cmd) - assert.EqualValues(t, []string{"push"}, args) - - return secureexec.Command("echo") - }, - false, - func(err error) { - assert.NoError(t, err) - }, - }, - { - "Push with an error occurring, follow-tags on", - func(string) (string, error) { - return "", nil - }, - func(cmd string, args ...string) *exec.Cmd { - assert.EqualValues(t, "git", cmd) - assert.EqualValues(t, []string{"push", "--follow-tags"}, args) - return secureexec.Command("test") - }, - false, - func(err error) { - assert.Error(t, err) - }, - }, - } - - for _, s := range scenarios { - t.Run(s.testName, func(t *testing.T) { - gitCmd := NewDummyGitCommand() - gitCmd.OSCommand.Command = s.command - gitCmd.getGitConfigValue = s.getGitConfigValue - err := gitCmd.Push("test", s.forcePush, "", "", func(passOrUname string) string { - return "\n" - }) - s.test(err) - }) - } -} - -// TestGitCommandCatFile tests emitting a file using commands, where commands vary by OS. -func TestGitCommandCatFile(t *testing.T) { - var osCmd string - switch os := runtime.GOOS; os { - case "windows": - osCmd = "type" - default: - osCmd = "cat" - } - gitCmd := NewDummyGitCommand() - gitCmd.OSCommand.Command = func(cmd string, args ...string) *exec.Cmd { - assert.EqualValues(t, osCmd, cmd) - assert.EqualValues(t, []string{"test.txt"}, args) - - return secureexec.Command("echo", "-n", "test") - } - - o, err := gitCmd.CatFile("test.txt") - assert.NoError(t, err) - assert.Equal(t, "test", o) -} - -// TestGitCommandStageFile is a function. -func TestGitCommandStageFile(t *testing.T) { - gitCmd := NewDummyGitCommand() - gitCmd.OSCommand.Command = func(cmd string, args ...string) *exec.Cmd { - assert.EqualValues(t, "git", cmd) - assert.EqualValues(t, []string{"add", "--", "test.txt"}, args) - - return secureexec.Command("echo") - } - - assert.NoError(t, gitCmd.StageFile("test.txt")) -} - -// TestGitCommandUnstageFile is a function. -func TestGitCommandUnstageFile(t *testing.T) { - type scenario struct { - testName string - command func(string, ...string) *exec.Cmd - test func(error) - reset bool - } - - scenarios := []scenario{ - { - "Remove an untracked file from staging", - func(cmd string, args ...string) *exec.Cmd { - assert.EqualValues(t, "git", cmd) - assert.EqualValues(t, []string{"rm", "--cached", "--force", "--", "test.txt"}, args) - - return secureexec.Command("echo") - }, - func(err error) { - assert.NoError(t, err) - }, - false, - }, - { - "Remove a tracked file from staging", - func(cmd string, args ...string) *exec.Cmd { - assert.EqualValues(t, "git", cmd) - assert.EqualValues(t, []string{"reset", "HEAD", "--", "test.txt"}, args) - - return secureexec.Command("echo") - }, - func(err error) { - assert.NoError(t, err) - }, - true, - }, - } - - for _, s := range scenarios { - t.Run(s.testName, func(t *testing.T) { - gitCmd := NewDummyGitCommand() - gitCmd.OSCommand.Command = s.command - s.test(gitCmd.UnStageFile([]string{"test.txt"}, s.reset)) - }) - } -} - -// TestGitCommandDiscardAllFileChanges is a function. -// 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 - command func() (func(string, ...string) *exec.Cmd, *[][]string) - test func(*[][]string, error) - file *models.File - removeFile func(string) error - } - - scenarios := []scenario{ - { - "An error occurred when resetting", - func() (func(string, ...string) *exec.Cmd, *[][]string) { - cmdsCalled := [][]string{} - return func(cmd string, args ...string) *exec.Cmd { - cmdsCalled = append(cmdsCalled, args) - - return secureexec.Command("test") - }, &cmdsCalled - }, - func(cmdsCalled *[][]string, err error) { - assert.Error(t, err) - assert.Len(t, *cmdsCalled, 1) - assert.EqualValues(t, *cmdsCalled, [][]string{ - {"reset", "--", "test"}, - }) - }, - &models.File{ - Name: "test", - HasStagedChanges: true, - }, - func(string) error { - return nil - }, - }, - { - "An error occurred when removing file", - func() (func(string, ...string) *exec.Cmd, *[][]string) { - cmdsCalled := [][]string{} - return func(cmd string, args ...string) *exec.Cmd { - cmdsCalled = append(cmdsCalled, args) - - return secureexec.Command("test") - }, &cmdsCalled - }, - func(cmdsCalled *[][]string, err error) { - assert.Error(t, err) - assert.EqualError(t, err, "an error occurred when removing file") - assert.Len(t, *cmdsCalled, 0) - }, - &models.File{ - Name: "test", - Tracked: false, - Added: true, - }, - func(string) error { - return fmt.Errorf("an error occurred when removing file") - }, - }, - { - "An error occurred with checkout", - func() (func(string, ...string) *exec.Cmd, *[][]string) { - cmdsCalled := [][]string{} - return func(cmd string, args ...string) *exec.Cmd { - cmdsCalled = append(cmdsCalled, args) - - return secureexec.Command("test") - }, &cmdsCalled - }, - func(cmdsCalled *[][]string, err error) { - assert.Error(t, err) - assert.Len(t, *cmdsCalled, 1) - assert.EqualValues(t, *cmdsCalled, [][]string{ - {"checkout", "--", "test"}, - }) - }, - &models.File{ - Name: "test", - Tracked: true, - HasStagedChanges: false, - }, - func(string) error { - return nil - }, - }, - { - "Checkout only", - func() (func(string, ...string) *exec.Cmd, *[][]string) { - cmdsCalled := [][]string{} - return func(cmd string, args ...string) *exec.Cmd { - cmdsCalled = append(cmdsCalled, args) - - return secureexec.Command("echo") - }, &cmdsCalled - }, - func(cmdsCalled *[][]string, err error) { - assert.NoError(t, err) - assert.Len(t, *cmdsCalled, 1) - assert.EqualValues(t, *cmdsCalled, [][]string{ - {"checkout", "--", "test"}, - }) - }, - &models.File{ - Name: "test", - Tracked: true, - HasStagedChanges: false, - }, - func(string) error { - return nil - }, - }, - { - "Reset and checkout staged changes", - func() (func(string, ...string) *exec.Cmd, *[][]string) { - cmdsCalled := [][]string{} - return func(cmd string, args ...string) *exec.Cmd { - cmdsCalled = append(cmdsCalled, args) - - return secureexec.Command("echo") - }, &cmdsCalled - }, - func(cmdsCalled *[][]string, err error) { - assert.NoError(t, err) - assert.Len(t, *cmdsCalled, 2) - assert.EqualValues(t, *cmdsCalled, [][]string{ - {"reset", "--", "test"}, - {"checkout", "--", "test"}, - }) - }, - &models.File{ - Name: "test", - Tracked: true, - HasStagedChanges: true, - }, - func(string) error { - return nil - }, - }, - { - "Reset and checkout merge conflicts", - func() (func(string, ...string) *exec.Cmd, *[][]string) { - cmdsCalled := [][]string{} - return func(cmd string, args ...string) *exec.Cmd { - cmdsCalled = append(cmdsCalled, args) - - return secureexec.Command("echo") - }, &cmdsCalled - }, - func(cmdsCalled *[][]string, err error) { - assert.NoError(t, err) - assert.Len(t, *cmdsCalled, 2) - assert.EqualValues(t, *cmdsCalled, [][]string{ - {"reset", "--", "test"}, - {"checkout", "--", "test"}, - }) - }, - &models.File{ - Name: "test", - Tracked: true, - HasMergeConflicts: true, - }, - func(string) error { - return nil - }, - }, - { - "Reset and remove", - func() (func(string, ...string) *exec.Cmd, *[][]string) { - cmdsCalled := [][]string{} - return func(cmd string, args ...string) *exec.Cmd { - cmdsCalled = append(cmdsCalled, args) - - return secureexec.Command("echo") - }, &cmdsCalled - }, - func(cmdsCalled *[][]string, err error) { - assert.NoError(t, err) - assert.Len(t, *cmdsCalled, 1) - assert.EqualValues(t, *cmdsCalled, [][]string{ - {"reset", "--", "test"}, - }) - }, - &models.File{ - Name: "test", - Tracked: false, - Added: true, - HasStagedChanges: true, - }, - func(filename string) error { - assert.Equal(t, "test", filename) - return nil - }, - }, - { - "Remove only", - func() (func(string, ...string) *exec.Cmd, *[][]string) { - cmdsCalled := [][]string{} - return func(cmd string, args ...string) *exec.Cmd { - cmdsCalled = append(cmdsCalled, args) - - return secureexec.Command("echo") - }, &cmdsCalled - }, - func(cmdsCalled *[][]string, err error) { - assert.NoError(t, err) - assert.Len(t, *cmdsCalled, 0) - }, - &models.File{ - Name: "test", - Tracked: false, - Added: true, - HasStagedChanges: false, - }, - func(filename string) error { - assert.Equal(t, "test", filename) - return nil - }, - }, - } - - for _, s := range scenarios { - t.Run(s.testName, func(t *testing.T) { - var cmdsCalled *[][]string - gitCmd := NewDummyGitCommand() - gitCmd.OSCommand.Command, cmdsCalled = s.command() - gitCmd.removeFile = s.removeFile - s.test(cmdsCalled, gitCmd.DiscardAllFileChanges(s.file)) - }) - } -} - -// TestGitCommandCheckout is a function. -func TestGitCommandCheckout(t *testing.T) { - type scenario struct { - testName string - command func(string, ...string) *exec.Cmd - test func(error) - force bool - } - - scenarios := []scenario{ - { - "Checkout", - func(cmd string, args ...string) *exec.Cmd { - assert.EqualValues(t, "git", cmd) - assert.EqualValues(t, []string{"checkout", "test"}, args) - - return secureexec.Command("echo") - }, - func(err error) { - assert.NoError(t, err) - }, - false, - }, - { - "Checkout forced", - func(cmd string, args ...string) *exec.Cmd { - assert.EqualValues(t, "git", cmd) - assert.EqualValues(t, []string{"checkout", "--force", "test"}, args) - - return secureexec.Command("echo") - }, - func(err error) { - assert.NoError(t, err) - }, - true, - }, - } - - for _, s := range scenarios { - t.Run(s.testName, func(t *testing.T) { - gitCmd := NewDummyGitCommand() - gitCmd.OSCommand.Command = s.command - s.test(gitCmd.Checkout("test", CheckoutOptions{Force: s.force})) - }) - } -} - -// TestGitCommandGetBranchGraph is a function. -func TestGitCommandGetBranchGraph(t *testing.T) { - gitCmd := NewDummyGitCommand() - gitCmd.OSCommand.Command = func(cmd string, args ...string) *exec.Cmd { - assert.EqualValues(t, "git", cmd) - assert.EqualValues(t, []string{"log", "--graph", "--color=always", "--abbrev-commit", "--decorate", "--date=relative", "--pretty=medium", "test", "--"}, args) - return secureexec.Command("echo") - } - _, err := gitCmd.GetBranchGraph("test") - assert.NoError(t, err) -} - -func TestGitCommandGetAllBranchGraph(t *testing.T) { - gitCmd := NewDummyGitCommand() - gitCmd.OSCommand.Command = func(cmd string, args ...string) *exec.Cmd { - assert.EqualValues(t, "git", cmd) - assert.EqualValues(t, []string{"log", "--graph", "--all", "--color=always", "--abbrev-commit", "--decorate", "--date=relative", "--pretty=medium"}, args) - return secureexec.Command("echo") - } - cmdStr := gitCmd.Config.GetUserConfig().Git.AllBranchesLogCmd - _, err := gitCmd.OSCommand.RunCommandWithOutput(cmdStr) - assert.NoError(t, err) -} - -// TestGitCommandDiff is a function. -func TestGitCommandDiff(t *testing.T) { - type scenario struct { - testName string - command func(string, ...string) *exec.Cmd - file *models.File - plain bool - cached bool - } - - scenarios := []scenario{ - { - "Default case", - func(cmd string, args ...string) *exec.Cmd { - assert.EqualValues(t, "git", cmd) - assert.EqualValues(t, []string{"diff", "--submodule", "--no-ext-diff", "--color=always", "--", "test.txt"}, args) - - return secureexec.Command("echo") - }, - &models.File{ - Name: "test.txt", - HasStagedChanges: false, - Tracked: true, - }, - false, - false, - }, - { - "cached", - func(cmd string, args ...string) *exec.Cmd { - assert.EqualValues(t, "git", cmd) - assert.EqualValues(t, []string{"diff", "--submodule", "--no-ext-diff", "--color=always", "--cached", "--", "test.txt"}, args) - - return secureexec.Command("echo") - }, - &models.File{ - Name: "test.txt", - HasStagedChanges: false, - Tracked: true, - }, - false, - true, - }, - { - "plain", - func(cmd string, args ...string) *exec.Cmd { - assert.EqualValues(t, "git", cmd) - assert.EqualValues(t, []string{"diff", "--submodule", "--no-ext-diff", "--color=never", "--", "test.txt"}, args) - - return secureexec.Command("echo") - }, - &models.File{ - Name: "test.txt", - HasStagedChanges: false, - Tracked: true, - }, - true, - false, - }, - { - "File not tracked and file has no staged changes", - func(cmd string, args ...string) *exec.Cmd { - assert.EqualValues(t, "git", cmd) - assert.EqualValues(t, []string{"diff", "--submodule", "--no-ext-diff", "--color=always", "--no-index", "--", "/dev/null", "test.txt"}, args) - - return secureexec.Command("echo") - }, - &models.File{ - Name: "test.txt", - HasStagedChanges: false, - Tracked: false, - }, - false, - false, - }, - } - - for _, s := range scenarios { - t.Run(s.testName, func(t *testing.T) { - gitCmd := NewDummyGitCommand() - gitCmd.OSCommand.Command = s.command - gitCmd.WorktreeFileDiff(s.file, s.plain, s.cached) - }) - } -} - -// TestGitCommandCurrentBranchName is a function. -func TestGitCommandCurrentBranchName(t *testing.T) { - type scenario struct { - testName string - command func(string, ...string) *exec.Cmd - test func(string, string, error) - } - - scenarios := []scenario{ - { - "says we are on the master branch if we are", - func(cmd string, args ...string) *exec.Cmd { - assert.Equal(t, "git", cmd) - return secureexec.Command("echo", "master") - }, - func(name string, displayname string, err error) { - assert.NoError(t, err) - assert.EqualValues(t, "master", name) - assert.EqualValues(t, "master", displayname) - }, - }, - { - "falls back to git `git branch --contains` if symbolic-ref fails", - func(cmd string, args ...string) *exec.Cmd { - assert.EqualValues(t, "git", cmd) - - switch args[0] { - case "symbolic-ref": - assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args) - return secureexec.Command("test") - case "branch": - assert.EqualValues(t, []string{"branch", "--contains"}, args) - return secureexec.Command("echo", "* master") - } - - return nil - }, - func(name string, displayname string, err error) { - assert.NoError(t, err) - assert.EqualValues(t, "master", name) - assert.EqualValues(t, "master", displayname) - }, - }, - { - "handles a detached head", - func(cmd string, args ...string) *exec.Cmd { - assert.EqualValues(t, "git", cmd) - - switch args[0] { - case "symbolic-ref": - assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args) - return secureexec.Command("test") - case "branch": - assert.EqualValues(t, []string{"branch", "--contains"}, args) - return secureexec.Command("echo", "* (HEAD detached at 123abcd)") - } - - return nil - }, - func(name string, displayname string, err error) { - assert.NoError(t, err) - assert.EqualValues(t, "123abcd", name) - assert.EqualValues(t, "(HEAD detached at 123abcd)", displayname) - }, - }, - { - "bubbles up error if there is one", - func(cmd string, args ...string) *exec.Cmd { - assert.Equal(t, "git", cmd) - return secureexec.Command("test") - }, - func(name string, displayname string, err error) { - assert.Error(t, err) - assert.EqualValues(t, "", name) - assert.EqualValues(t, "", displayname) - }, - }, - } - - for _, s := range scenarios { - t.Run(s.testName, func(t *testing.T) { - gitCmd := NewDummyGitCommand() - gitCmd.OSCommand.Command = s.command - s.test(gitCmd.CurrentBranchName()) - }) - } -} - -func TestGitCommandApplyPatch(t *testing.T) { - type scenario struct { - testName string - command func(string, ...string) *exec.Cmd - test func(error) - } - - scenarios := []scenario{ - { - "valid case", - func(cmd string, args ...string) *exec.Cmd { - assert.Equal(t, "git", cmd) - assert.EqualValues(t, []string{"apply", "--cached"}, args[0:2]) - filename := args[2] - content, err := ioutil.ReadFile(filename) - assert.NoError(t, err) - - assert.Equal(t, "test", string(content)) - - return secureexec.Command("echo", "done") - }, - func(err error) { - assert.NoError(t, err) - }, - }, - { - "command returns error", - func(cmd string, args ...string) *exec.Cmd { - assert.Equal(t, "git", cmd) - assert.EqualValues(t, []string{"apply", "--cached"}, args[0:2]) - filename := args[2] - // TODO: Ideally we want to mock out OSCommand here so that we're not - // double handling testing it's CreateTempFile functionality, - // but it is going to take a bit of work to make a proper mock for it - // so I'm leaving it for another PR - content, err := ioutil.ReadFile(filename) - assert.NoError(t, err) - - assert.Equal(t, "test", string(content)) - - return secureexec.Command("test") - }, - func(err error) { - assert.Error(t, err) - }, - }, - } - - for _, s := range scenarios { - t.Run(s.testName, func(t *testing.T) { - gitCmd := NewDummyGitCommand() - gitCmd.OSCommand.Command = s.command - s.test(gitCmd.ApplyPatch("test", "cached")) - }) - } -} - -// TestGitCommandRebaseBranch is a function. -func TestGitCommandRebaseBranch(t *testing.T) { - type scenario struct { - testName string - arg string - command func(string, ...string) *exec.Cmd - test func(error) - } - - scenarios := []scenario{ - { - "successful rebase", - "master", - test.CreateMockCommand(t, []*test.CommandSwapper{ - { - Expect: "git rebase --interactive --autostash --keep-empty master", - Replace: "echo", - }, - }), - func(err error) { - assert.NoError(t, err) - }, - }, - { - "unsuccessful rebase", - "master", - test.CreateMockCommand(t, []*test.CommandSwapper{ - { - Expect: "git rebase --interactive --autostash --keep-empty master", - Replace: "test", - }, - }), - func(err error) { - assert.Error(t, err) - }, - }, - } - - gitCmd := NewDummyGitCommand() - - for _, s := range scenarios { - t.Run(s.testName, func(t *testing.T) { - gitCmd.OSCommand.Command = s.command - s.test(gitCmd.RebaseBranch(s.arg)) - }) - } -} - -// TestGitCommandCheckoutFile is a function. -func TestGitCommandCheckoutFile(t *testing.T) { - type scenario struct { - testName string - commitSha string - fileName string - command func(string, ...string) *exec.Cmd - test func(error) - } - - scenarios := []scenario{ - { - "typical case", - "11af912", - "test999.txt", - test.CreateMockCommand(t, []*test.CommandSwapper{ - { - Expect: "git checkout 11af912 test999.txt", - Replace: "echo", - }, - }), - func(err error) { - assert.NoError(t, err) - }, - }, - { - "returns error if there is one", - "11af912", - "test999.txt", - test.CreateMockCommand(t, []*test.CommandSwapper{ - { - Expect: "git checkout 11af912 test999.txt", - Replace: "test", - }, - }), - func(err error) { - assert.Error(t, err) - }, - }, - } - - gitCmd := NewDummyGitCommand() - - for _, s := range scenarios { - t.Run(s.testName, func(t *testing.T) { - gitCmd.OSCommand.Command = s.command - s.test(gitCmd.CheckoutFile(s.commitSha, s.fileName)) - }) - } -} - -// TestGitCommandDiscardOldFileChanges is a function. -func TestGitCommandDiscardOldFileChanges(t *testing.T) { - type scenario struct { - testName string - getGitConfigValue func(string) (string, error) - commits []*models.Commit - commitIndex int - fileName string - command func(string, ...string) *exec.Cmd - test func(error) - } - - scenarios := []scenario{ - { - "returns error when index outside of range of commits", - func(string) (string, error) { - return "", nil - }, - []*models.Commit{}, - 0, - "test999.txt", - nil, - func(err error) { - assert.Error(t, err) - }, - }, - { - "returns error when using gpg", - func(string) (string, error) { - return "true", nil - }, - []*models.Commit{{Name: "commit", Sha: "123456"}}, - 0, - "test999.txt", - nil, - func(err error) { - assert.Error(t, err) - }, - }, - { - "checks out file if it already existed", - func(string) (string, error) { - return "", nil - }, - []*models.Commit{ - {Name: "commit", Sha: "123456"}, - {Name: "commit2", Sha: "abcdef"}, - }, - 0, - "test999.txt", - test.CreateMockCommand(t, []*test.CommandSwapper{ - { - Expect: "git rebase --interactive --autostash --keep-empty abcdef", - Replace: "echo", - }, - { - Expect: "git cat-file -e HEAD^:test999.txt", - Replace: "echo", - }, - { - Expect: "git checkout HEAD^ test999.txt", - Replace: "echo", - }, - { - Expect: "git commit --amend --no-edit --allow-empty", - Replace: "echo", - }, - { - Expect: "git rebase --continue", - Replace: "echo", - }, - }), - 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 - } - - gitCmd := NewDummyGitCommand() - - for _, s := range scenarios { - t.Run(s.testName, func(t *testing.T) { - gitCmd.OSCommand.Command = s.command - gitCmd.getGitConfigValue = s.getGitConfigValue - s.test(gitCmd.DiscardOldFileChanges(s.commits, s.commitIndex, s.fileName)) - }) - } -} - -// TestGitCommandDiscardUnstagedFileChanges is a function. -func TestGitCommandDiscardUnstagedFileChanges(t *testing.T) { - type scenario struct { - testName string - file *models.File - command func(string, ...string) *exec.Cmd - test func(error) - } - - scenarios := []scenario{ - { - "valid case", - &models.File{Name: "test.txt"}, - test.CreateMockCommand(t, []*test.CommandSwapper{ - { - Expect: `git checkout -- "test.txt"`, - Replace: "echo", - }, - }), - func(err error) { - assert.NoError(t, err) - }, - }, - } - - gitCmd := NewDummyGitCommand() - - for _, s := range scenarios { - t.Run(s.testName, func(t *testing.T) { - gitCmd.OSCommand.Command = s.command - s.test(gitCmd.DiscardUnstagedFileChanges(s.file)) - }) - } -} - -// TestGitCommandDiscardAnyUnstagedFileChanges is a function. -func TestGitCommandDiscardAnyUnstagedFileChanges(t *testing.T) { - type scenario struct { - testName string - command func(string, ...string) *exec.Cmd - test func(error) - } - - scenarios := []scenario{ - { - "valid case", - test.CreateMockCommand(t, []*test.CommandSwapper{ - { - Expect: `git checkout -- .`, - Replace: "echo", - }, - }), - func(err error) { - assert.NoError(t, err) - }, - }, - } - - gitCmd := NewDummyGitCommand() - - for _, s := range scenarios { - t.Run(s.testName, func(t *testing.T) { - gitCmd.OSCommand.Command = s.command - s.test(gitCmd.DiscardAnyUnstagedFileChanges()) - }) - } -} - -// TestGitCommandRemoveUntrackedFiles is a function. -func TestGitCommandRemoveUntrackedFiles(t *testing.T) { - type scenario struct { - testName string - command func(string, ...string) *exec.Cmd - test func(error) - } - - scenarios := []scenario{ - { - "valid case", - test.CreateMockCommand(t, []*test.CommandSwapper{ - { - Expect: `git clean -fd`, - Replace: "echo", - }, - }), - func(err error) { - assert.NoError(t, err) - }, - }, - } - - gitCmd := NewDummyGitCommand() - - for _, s := range scenarios { - t.Run(s.testName, func(t *testing.T) { - gitCmd.OSCommand.Command = s.command - s.test(gitCmd.RemoveUntrackedFiles()) - }) - } -} - -// TestGitCommandResetHard is a function. -func TestGitCommandResetHard(t *testing.T) { - type scenario struct { - testName string - ref string - command func(string, ...string) *exec.Cmd - test func(error) - } - - scenarios := []scenario{ - { - "valid case", - "HEAD", - test.CreateMockCommand(t, []*test.CommandSwapper{ - { - Expect: `git reset --hard HEAD`, - Replace: "echo", - }, - }), - func(err error) { - assert.NoError(t, err) - }, - }, - } - - gitCmd := NewDummyGitCommand() - - for _, s := range scenarios { - t.Run(s.testName, func(t *testing.T) { - gitCmd.OSCommand.Command = s.command - s.test(gitCmd.ResetHard(s.ref)) - }) - } -} - -// TestGitCommandCreateFixupCommit is a function. -func TestGitCommandCreateFixupCommit(t *testing.T) { - type scenario struct { - testName string - sha string - command func(string, ...string) *exec.Cmd - test func(error) - } - - scenarios := []scenario{ - { - "valid case", - "12345", - test.CreateMockCommand(t, []*test.CommandSwapper{ - { - Expect: `git commit --fixup=12345`, - Replace: "echo", - }, - }), - func(err error) { - assert.NoError(t, err) - }, - }, - } - - gitCmd := NewDummyGitCommand() - - for _, s := range scenarios { - t.Run(s.testName, func(t *testing.T) { - gitCmd.OSCommand.Command = s.command - s.test(gitCmd.CreateFixupCommit(s.sha)) - }) - } -} - -// TestGitCommandSkipEditorCommand confirms that SkipEditorCommand injects -// environment variables that suppress an interactive editor -func TestGitCommandSkipEditorCommand(t *testing.T) { - cmd := NewDummyGitCommand() - - cmd.OSCommand.SetBeforeExecuteCmd(func(cmd *exec.Cmd) { - test.AssertContainsMatch( - t, - cmd.Env, - regexp.MustCompile("^VISUAL="), - "expected VISUAL to be set for a non-interactive external command", - ) - - test.AssertContainsMatch( - t, - cmd.Env, - regexp.MustCompile("^EDITOR="), - "expected EDITOR to be set for a non-interactive external command", - ) - - test.AssertContainsMatch( - t, - cmd.Env, - regexp.MustCompile("^GIT_EDITOR="), - "expected GIT_EDITOR to be set for a non-interactive external command", - ) - - test.AssertContainsMatch( - t, - cmd.Env, - regexp.MustCompile("^LAZYGIT_CLIENT_COMMAND=EXIT_IMMEDIATELY$"), - "expected LAZYGIT_CLIENT_COMMAND to be set for a non-interactive external command", - ) - }) - - _ = cmd.runSkipEditorCommand("true") -} - func TestFindDotGitDir(t *testing.T) { type scenario struct { testName string @@ -1989,154 +288,3 @@ func TestFindDotGitDir(t *testing.T) { }) } } - -// TestEditFile is a function. -func TestEditFile(t *testing.T) { - type scenario struct { - filename string - command func(string, ...string) *exec.Cmd - getenv func(string) string - getGitConfigValue func(string) (string, error) - test func(*exec.Cmd, error) - } - - scenarios := []scenario{ - { - "test", - func(name string, arg ...string) *exec.Cmd { - return secureexec.Command("exit", "1") - }, - func(env string) string { - return "" - }, - func(cf string) (string, error) { - return "", nil - }, - func(cmd *exec.Cmd, err error) { - assert.EqualError(t, err, "No editor defined in $GIT_EDITOR, $VISUAL, $EDITOR, or git config") - }, - }, - { - "test", - func(name string, arg ...string) *exec.Cmd { - if name == "which" { - return secureexec.Command("exit", "1") - } - - assert.EqualValues(t, "nano", name) - - return nil - }, - func(env string) string { - return "" - }, - func(cf string) (string, error) { - return "nano", nil - }, - func(cmd *exec.Cmd, err error) { - assert.NoError(t, err) - }, - }, - { - "test", - func(name string, arg ...string) *exec.Cmd { - if name == "which" { - return secureexec.Command("exit", "1") - } - - assert.EqualValues(t, "nano", name) - - return nil - }, - func(env string) string { - if env == "VISUAL" { - return "nano" - } - - return "" - }, - func(cf string) (string, error) { - return "", nil - }, - func(cmd *exec.Cmd, err error) { - assert.NoError(t, err) - }, - }, - { - "test", - func(name string, arg ...string) *exec.Cmd { - if name == "which" { - return secureexec.Command("exit", "1") - } - - assert.EqualValues(t, "emacs", name) - - return nil - }, - func(env string) string { - if env == "EDITOR" { - return "emacs" - } - - return "" - }, - func(cf string) (string, error) { - return "", nil - }, - func(cmd *exec.Cmd, err error) { - assert.NoError(t, err) - }, - }, - { - "test", - func(name string, arg ...string) *exec.Cmd { - if name == "which" { - return secureexec.Command("echo") - } - - assert.EqualValues(t, "vi", name) - - return nil - }, - func(env string) string { - return "" - }, - func(cf string) (string, error) { - return "", nil - }, - func(cmd *exec.Cmd, err error) { - assert.NoError(t, err) - }, - }, - { - "file/with space", - func(name string, args ...string) *exec.Cmd { - if name == "which" { - return secureexec.Command("echo") - } - - assert.EqualValues(t, "vi", name) - assert.EqualValues(t, "file/with space", args[0]) - - return nil - }, - func(env string) string { - return "" - }, - func(cf string) (string, error) { - return "", nil - }, - func(cmd *exec.Cmd, err error) { - assert.NoError(t, err) - }, - }, - } - - for _, s := range scenarios { - gitCmd := NewDummyGitCommand() - gitCmd.OSCommand.Command = s.command - gitCmd.OSCommand.Getenv = s.getenv - gitCmd.getGitConfigValue = s.getGitConfigValue - s.test(gitCmd.EditFile(s.filename)) - } -} diff --git a/pkg/commands/loading_files_test.go b/pkg/commands/loading_files_test.go new file mode 100644 index 000000000..bb0a5971a --- /dev/null +++ b/pkg/commands/loading_files_test.go @@ -0,0 +1,122 @@ +package commands + +import ( + "os/exec" + "testing" + + "github.com/jesseduffield/lazygit/pkg/commands/models" + "github.com/jesseduffield/lazygit/pkg/secureexec" + "github.com/stretchr/testify/assert" +) + +// TestGitCommandGetStatusFiles is a function. +func TestGitCommandGetStatusFiles(t *testing.T) { + type scenario struct { + testName string + command func(string, ...string) *exec.Cmd + test func([]*models.File) + } + + scenarios := []scenario{ + { + "No files found", + func(cmd string, args ...string) *exec.Cmd { + return secureexec.Command("echo") + }, + func(files []*models.File) { + assert.Len(t, files, 0) + }, + }, + { + "Several files found", + func(cmd string, args ...string) *exec.Cmd { + return secureexec.Command( + "echo", + "MM file1.txt\nA file3.txt\nAM file2.txt\n?? file4.txt\nUU file5.txt", + ) + }, + func(files []*models.File) { + assert.Len(t, files, 5) + + expected := []*models.File{ + { + Name: "file1.txt", + HasStagedChanges: true, + HasUnstagedChanges: true, + Tracked: true, + Added: false, + Deleted: false, + HasMergeConflicts: false, + HasInlineMergeConflicts: false, + DisplayString: "MM file1.txt", + Type: "other", + ShortStatus: "MM", + }, + { + Name: "file3.txt", + HasStagedChanges: true, + HasUnstagedChanges: false, + Tracked: false, + Added: true, + Deleted: false, + HasMergeConflicts: false, + HasInlineMergeConflicts: false, + DisplayString: "A file3.txt", + Type: "other", + ShortStatus: "A ", + }, + { + Name: "file2.txt", + HasStagedChanges: true, + HasUnstagedChanges: true, + Tracked: false, + Added: true, + Deleted: false, + HasMergeConflicts: false, + HasInlineMergeConflicts: false, + DisplayString: "AM file2.txt", + Type: "other", + ShortStatus: "AM", + }, + { + Name: "file4.txt", + HasStagedChanges: false, + HasUnstagedChanges: true, + Tracked: false, + Added: true, + Deleted: false, + HasMergeConflicts: false, + HasInlineMergeConflicts: false, + DisplayString: "?? file4.txt", + Type: "other", + ShortStatus: "??", + }, + { + Name: "file5.txt", + HasStagedChanges: false, + HasUnstagedChanges: true, + Tracked: true, + Added: false, + Deleted: false, + HasMergeConflicts: true, + HasInlineMergeConflicts: true, + DisplayString: "UU file5.txt", + Type: "other", + ShortStatus: "UU", + }, + } + + assert.EqualValues(t, expected, files) + }, + }, + } + + for _, s := range scenarios { + t.Run(s.testName, func(t *testing.T) { + gitCmd := NewDummyGitCommand() + gitCmd.OSCommand.Command = s.command + + s.test(gitCmd.GetStatusFiles(GetStatusFileOptions{})) + }) + } +} diff --git a/pkg/commands/loading_stash_test.go b/pkg/commands/loading_stash_test.go new file mode 100644 index 000000000..ea1f86ec1 --- /dev/null +++ b/pkg/commands/loading_stash_test.go @@ -0,0 +1,61 @@ +package commands + +import ( + "os/exec" + "testing" + + "github.com/jesseduffield/lazygit/pkg/commands/models" + "github.com/jesseduffield/lazygit/pkg/secureexec" + "github.com/stretchr/testify/assert" +) + +// TestGitCommandGetStashEntries is a function. +func TestGitCommandGetStashEntries(t *testing.T) { + type scenario struct { + testName string + command func(string, ...string) *exec.Cmd + test func([]*models.StashEntry) + } + + scenarios := []scenario{ + { + "No stash entries found", + func(string, ...string) *exec.Cmd { + return secureexec.Command("echo") + }, + func(entries []*models.StashEntry) { + assert.Len(t, entries, 0) + }, + }, + { + "Several stash entries found", + func(string, ...string) *exec.Cmd { + return secureexec.Command("echo", "WIP on add-pkg-commands-test: 55c6af2 increase parallel build\nWIP on master: bb86a3f update github template") + }, + func(entries []*models.StashEntry) { + expected := []*models.StashEntry{ + { + Index: 0, + Name: "WIP on add-pkg-commands-test: 55c6af2 increase parallel build", + }, + { + Index: 1, + Name: "WIP on master: bb86a3f update github template", + }, + } + + assert.Len(t, entries, 2) + assert.EqualValues(t, expected, entries) + }, + }, + } + + for _, s := range scenarios { + t.Run(s.testName, func(t *testing.T) { + gitCmd := NewDummyGitCommand() + gitCmd.OSCommand.Command = s.command + + s.test(gitCmd.GetStashEntries("")) + }) + } +} diff --git a/pkg/commands/patch_rebases.go b/pkg/commands/patch_rebases.go index 2c3b5b1d3..6f9e08679 100644 --- a/pkg/commands/patch_rebases.go +++ b/pkg/commands/patch_rebases.go @@ -23,7 +23,7 @@ func (c *GitCommand) DeletePatchesFromCommit(commits []*models.Commit, commitInd } // time to amend the selected commit - if _, err := c.AmendHead(); err != nil { + if err := c.AmendHead(); err != nil { return err } @@ -51,7 +51,7 @@ func (c *GitCommand) MovePatchToSelectedCommit(commits []*models.Commit, sourceC } // amend the destination commit - if _, err := c.AmendHead(); err != nil { + if err := c.AmendHead(); err != nil { return err } @@ -71,7 +71,7 @@ func (c *GitCommand) MovePatchToSelectedCommit(commits []*models.Commit, sourceC // we can make this GPG thing possible it just means we need to do this in two parts: // one where we handle the possibility of a credential request, and the other // where we continue the rebase - if c.usingGpg() { + if c.UsingGpg() { return errors.New(c.Tr.DisabledForGPG) } @@ -103,7 +103,7 @@ func (c *GitCommand) MovePatchToSelectedCommit(commits []*models.Commit, sourceC } // amend the source commit - if _, err := c.AmendHead(); err != nil { + if err := c.AmendHead(); err != nil { return err } @@ -122,7 +122,7 @@ func (c *GitCommand) MovePatchToSelectedCommit(commits []*models.Commit, sourceC } // amend the destination commit - if _, err := c.AmendHead(); err != nil { + if err := c.AmendHead(); err != nil { return err } @@ -158,7 +158,7 @@ func (c *GitCommand) PullPatchIntoIndex(commits []*models.Commit, commitIdx int, } // amend the commit - if _, err := c.AmendHead(); err != nil { + if err := c.AmendHead(); err != nil { return err } @@ -203,7 +203,7 @@ func (c *GitCommand) PullPatchIntoNewCommit(commits []*models.Commit, commitIdx } // amend the commit - if _, err := c.AmendHead(); err != nil { + if err := c.AmendHead(); err != nil { return err } @@ -217,7 +217,7 @@ func (c *GitCommand) PullPatchIntoNewCommit(commits []*models.Commit, commitIdx head_message, _ := c.GetHeadCommitMessage() new_message := fmt.Sprintf("Split from \"%s\"", head_message) - _, err := c.Commit(new_message, "") + err := c.OSCommand.RunCommand(c.CommitCmdStr(new_message, "")) if err != nil { return err } diff --git a/pkg/commands/rebasing.go b/pkg/commands/rebasing.go index e7c28bd8d..b3ff894fa 100644 --- a/pkg/commands/rebasing.go +++ b/pkg/commands/rebasing.go @@ -211,7 +211,7 @@ func (c *GitCommand) BeginInteractiveRebaseForCommit(commits []*models.Commit, c // we can make this GPG thing possible it just means we need to do this in two parts: // one where we handle the possibility of a credential request, and the other // where we continue the rebase - if c.usingGpg() { + if c.UsingGpg() { return errors.New(c.Tr.DisabledForGPG) } diff --git a/pkg/commands/rebasing_test.go b/pkg/commands/rebasing_test.go new file mode 100644 index 000000000..7c4a954c9 --- /dev/null +++ b/pkg/commands/rebasing_test.go @@ -0,0 +1,96 @@ +package commands + +import ( + "os/exec" + "regexp" + "testing" + + "github.com/jesseduffield/lazygit/pkg/test" + "github.com/stretchr/testify/assert" +) + +// TestGitCommandRebaseBranch is a function. +func TestGitCommandRebaseBranch(t *testing.T) { + type scenario struct { + testName string + arg string + command func(string, ...string) *exec.Cmd + test func(error) + } + + scenarios := []scenario{ + { + "successful rebase", + "master", + test.CreateMockCommand(t, []*test.CommandSwapper{ + { + Expect: "git rebase --interactive --autostash --keep-empty master", + Replace: "echo", + }, + }), + func(err error) { + assert.NoError(t, err) + }, + }, + { + "unsuccessful rebase", + "master", + test.CreateMockCommand(t, []*test.CommandSwapper{ + { + Expect: "git rebase --interactive --autostash --keep-empty master", + Replace: "test", + }, + }), + func(err error) { + assert.Error(t, err) + }, + }, + } + + gitCmd := NewDummyGitCommand() + + for _, s := range scenarios { + t.Run(s.testName, func(t *testing.T) { + gitCmd.OSCommand.Command = s.command + s.test(gitCmd.RebaseBranch(s.arg)) + }) + } +} + +// TestGitCommandSkipEditorCommand confirms that SkipEditorCommand injects +// environment variables that suppress an interactive editor +func TestGitCommandSkipEditorCommand(t *testing.T) { + cmd := NewDummyGitCommand() + + cmd.OSCommand.SetBeforeExecuteCmd(func(cmd *exec.Cmd) { + test.AssertContainsMatch( + t, + cmd.Env, + regexp.MustCompile("^VISUAL="), + "expected VISUAL to be set for a non-interactive external command", + ) + + test.AssertContainsMatch( + t, + cmd.Env, + regexp.MustCompile("^EDITOR="), + "expected EDITOR to be set for a non-interactive external command", + ) + + test.AssertContainsMatch( + t, + cmd.Env, + regexp.MustCompile("^GIT_EDITOR="), + "expected GIT_EDITOR to be set for a non-interactive external command", + ) + + test.AssertContainsMatch( + t, + cmd.Env, + regexp.MustCompile("^LAZYGIT_CLIENT_COMMAND=EXIT_IMMEDIATELY$"), + "expected LAZYGIT_CLIENT_COMMAND to be set for a non-interactive external command", + ) + }) + + _ = cmd.runSkipEditorCommand("true") +} diff --git a/pkg/commands/stash_entries_test.go b/pkg/commands/stash_entries_test.go new file mode 100644 index 000000000..bb4778dc6 --- /dev/null +++ b/pkg/commands/stash_entries_test.go @@ -0,0 +1,35 @@ +package commands + +import ( + "os/exec" + "testing" + + "github.com/jesseduffield/lazygit/pkg/secureexec" + "github.com/stretchr/testify/assert" +) + +// TestGitCommandStashDo is a function. +func TestGitCommandStashDo(t *testing.T) { + gitCmd := NewDummyGitCommand() + gitCmd.OSCommand.Command = func(cmd string, args ...string) *exec.Cmd { + assert.EqualValues(t, "git", cmd) + assert.EqualValues(t, []string{"stash", "drop", "stash@{1}"}, args) + + return secureexec.Command("echo") + } + + assert.NoError(t, gitCmd.StashDo(1, "drop")) +} + +// TestGitCommandStashSave is a function. +func TestGitCommandStashSave(t *testing.T) { + gitCmd := NewDummyGitCommand() + gitCmd.OSCommand.Command = func(cmd string, args ...string) *exec.Cmd { + assert.EqualValues(t, "git", cmd) + assert.EqualValues(t, []string{"stash", "save", "A stash message"}, args) + + return secureexec.Command("echo") + } + + assert.NoError(t, gitCmd.StashSave("A stash message")) +} diff --git a/pkg/commands/sync.go b/pkg/commands/sync.go index 97a6e4748..e3be30dc1 100644 --- a/pkg/commands/sync.go +++ b/pkg/commands/sync.go @@ -2,23 +2,8 @@ package commands import ( "fmt" - "strings" ) -// usingGpg tells us whether the user has gpg enabled so that we can know -// whether we need to run a subprocess to allow them to enter their password -func (c *GitCommand) usingGpg() bool { - overrideGpg := c.Config.GetUserConfig().Git.OverrideGpg - if overrideGpg { - return false - } - - gpgsign := c.GetConfigValue("commit.gpgsign") - value := strings.ToLower(gpgsign) - - return value == "true" || value == "1" || value == "yes" || value == "on" -} - // Push pushes to a branch func (c *GitCommand) Push(branchName string, force bool, upstream string, args string, promptUserForCredential func(string) string) error { followTagsFlag := "--follow-tags" diff --git a/pkg/commands/sync_test.go b/pkg/commands/sync_test.go new file mode 100644 index 000000000..b6a52fe63 --- /dev/null +++ b/pkg/commands/sync_test.go @@ -0,0 +1,98 @@ +package commands + +import ( + "os/exec" + "testing" + + "github.com/jesseduffield/lazygit/pkg/secureexec" + "github.com/stretchr/testify/assert" +) + +// TestGitCommandPush is a function. +func TestGitCommandPush(t *testing.T) { + type scenario struct { + testName string + getGitConfigValue func(string) (string, error) + command func(string, ...string) *exec.Cmd + forcePush bool + test func(error) + } + + scenarios := []scenario{ + { + "Push with force disabled, follow-tags on", + func(string) (string, error) { + return "", nil + }, + func(cmd string, args ...string) *exec.Cmd { + assert.EqualValues(t, "git", cmd) + assert.EqualValues(t, []string{"push", "--follow-tags"}, args) + + return secureexec.Command("echo") + }, + false, + func(err error) { + assert.NoError(t, err) + }, + }, + { + "Push with force enabled, follow-tags on", + func(string) (string, error) { + return "", nil + }, + func(cmd string, args ...string) *exec.Cmd { + assert.EqualValues(t, "git", cmd) + assert.EqualValues(t, []string{"push", "--follow-tags", "--force-with-lease"}, args) + + return secureexec.Command("echo") + }, + true, + func(err error) { + assert.NoError(t, err) + }, + }, + { + "Push with force disabled, follow-tags off", + func(string) (string, error) { + return "false", nil + }, + func(cmd string, args ...string) *exec.Cmd { + assert.EqualValues(t, "git", cmd) + assert.EqualValues(t, []string{"push"}, args) + + return secureexec.Command("echo") + }, + false, + func(err error) { + assert.NoError(t, err) + }, + }, + { + "Push with an error occurring, follow-tags on", + func(string) (string, error) { + return "", nil + }, + func(cmd string, args ...string) *exec.Cmd { + assert.EqualValues(t, "git", cmd) + assert.EqualValues(t, []string{"push", "--follow-tags"}, args) + return secureexec.Command("test") + }, + false, + func(err error) { + assert.Error(t, err) + }, + }, + } + + for _, s := range scenarios { + t.Run(s.testName, func(t *testing.T) { + gitCmd := NewDummyGitCommand() + gitCmd.OSCommand.Command = s.command + gitCmd.getGitConfigValue = s.getGitConfigValue + err := gitCmd.Push("test", s.forcePush, "", "", func(passOrUname string) string { + return "\n" + }) + s.test(err) + }) + } +} diff --git a/pkg/gui/commit_message_panel.go b/pkg/gui/commit_message_panel.go index 0d6b5b780..133174d77 100644 --- a/pkg/gui/commit_message_panel.go +++ b/pkg/gui/commit_message_panel.go @@ -1,7 +1,6 @@ package gui import ( - "os/exec" "strconv" "strings" @@ -9,24 +8,6 @@ import ( "github.com/jesseduffield/lazygit/pkg/utils" ) -// runSyncOrAsyncCommand takes the output of a command that may have returned -// either no error, an error, or a subprocess to execute, and if a subprocess -// needs to be run, it runs it -func (gui *Gui) runSyncOrAsyncCommand(sub *exec.Cmd, err error) (bool, error) { - if err != nil { - return false, gui.surfaceError(err) - } - if sub == nil { - return true, nil - } - - err = gui.runSubprocessWithSuspense(sub) - if err != nil { - return false, err - } - return true, nil -} - func (gui *Gui) handleCommitConfirm() error { message := gui.trimmedContent(gui.Views.CommitMessage) if message == "" { @@ -37,19 +18,12 @@ func (gui *Gui) handleCommitConfirm() error { if skipHookPrefix != "" && strings.HasPrefix(message, skipHookPrefix) { flags = "--no-verify" } - ok, err := gui.runSyncOrAsyncCommand(gui.GitCommand.Commit(message, flags)) - if err != nil { - return err - } - _ = gui.returnFromContext() - - if !ok { + return gui.withGpgHandling(gui.GitCommand.CommitCmdStr(message, flags), gui.Tr.CommittingStatus, func() error { + _ = gui.returnFromContext() + gui.clearEditorView(gui.Views.CommitMessage) return nil - } - - gui.clearEditorView(gui.Views.CommitMessage) - return gui.refreshSidePanels(refreshOptions{mode: ASYNC}) + }) } func (gui *Gui) handleCommitClose() error { diff --git a/pkg/gui/commits_panel.go b/pkg/gui/commits_panel.go index 026316700..4e360f290 100644 --- a/pkg/gui/commits_panel.go +++ b/pkg/gui/commits_panel.go @@ -263,7 +263,7 @@ func (gui *Gui) handleRenameCommitEditor() error { return gui.surfaceError(err) } if subProcess != nil { - return gui.runSubprocessWithSuspense(subProcess) + return gui.runSubprocessWithSuspenseAndRefresh(subProcess) } return nil diff --git a/pkg/gui/custom_commands.go b/pkg/gui/custom_commands.go index 59c08bcf1..c365f4516 100644 --- a/pkg/gui/custom_commands.go +++ b/pkg/gui/custom_commands.go @@ -60,7 +60,7 @@ func (gui *Gui) handleCustomCommandKeybinding(customCommand config.CustomCommand } if customCommand.Subprocess { - return gui.runSubprocessWithSuspense(gui.OSCommand.PrepareShellSubProcess(cmdStr)) + return gui.runSubprocessWithSuspenseAndRefresh(gui.OSCommand.PrepareShellSubProcess(cmdStr)) } loadingText := customCommand.LoadingText diff --git a/pkg/gui/files_panel.go b/pkg/gui/files_panel.go index 341849061..7d73f3d5b 100644 --- a/pkg/gui/files_panel.go +++ b/pkg/gui/files_panel.go @@ -448,17 +448,7 @@ func (gui *Gui) handleAmendCommitPress() error { title: strings.Title(gui.Tr.AmendLastCommit), prompt: gui.Tr.SureToAmend, handleConfirm: func() error { - return gui.WithWaitingStatus(gui.Tr.AmendingStatus, func() error { - ok, err := gui.runSyncOrAsyncCommand(gui.GitCommand.AmendHead()) - if err != nil { - return err - } - if !ok { - return nil - } - - return gui.refreshSidePanels(refreshOptions{mode: ASYNC}) - }) + return gui.withGpgHandling(gui.GitCommand.AmendHeadCmdStr(), gui.Tr.AmendingStatus, nil) }, }) } @@ -474,14 +464,20 @@ func (gui *Gui) handleCommitEditorPress() error { return gui.promptToStageAllAndRetry(gui.handleCommitEditorPress) } - return gui.runSubprocessWithSuspense( + return gui.runSubprocessWithSuspenseAndRefresh( gui.OSCommand.PrepareSubProcess("git", "commit"), ) } func (gui *Gui) editFile(filename string) error { - _, err := gui.runSyncOrAsyncCommand(gui.GitCommand.EditFile(filename)) - return err + cmdStr, err := gui.GitCommand.EditFileCmdStr(filename) + if err != nil { + return gui.surfaceError(err) + } + + return gui.runSubprocessWithSuspenseAndRefresh( + gui.OSCommand.PrepareShellSubProcess(cmdStr), + ) } func (gui *Gui) handleFileEdit() error { @@ -808,7 +804,7 @@ func (gui *Gui) handleCustomCommand() error { return gui.prompt(promptOpts{ title: gui.Tr.CustomCommand, handleConfirm: func(command string) error { - return gui.runSubprocessWithSuspense( + return gui.runSubprocessWithSuspenseAndRefresh( gui.OSCommand.PrepareShellSubProcess(command), ) }, diff --git a/pkg/gui/git_flow.go b/pkg/gui/git_flow.go index b19b32306..4549e6639 100644 --- a/pkg/gui/git_flow.go +++ b/pkg/gui/git_flow.go @@ -31,7 +31,7 @@ func (gui *Gui) gitFlowFinishBranch(gitFlowConfig string, branchName string) err return gui.createErrorPanel(gui.Tr.NotAGitFlowBranch) } - return gui.runSubprocessWithSuspense( + return gui.runSubprocessWithSuspenseAndRefresh( gui.OSCommand.PrepareSubProcess("git", "flow", branchType, "finish", suffix), ) } @@ -55,7 +55,7 @@ func (gui *Gui) handleCreateGitFlowMenu() error { return gui.prompt(promptOpts{ title: title, handleConfirm: func(name string) error { - return gui.runSubprocessWithSuspense( + return gui.runSubprocessWithSuspenseAndRefresh( gui.OSCommand.PrepareSubProcess("git", "flow", branchType, "start", name), ) }, diff --git a/pkg/gui/gpg.go b/pkg/gui/gpg.go new file mode 100644 index 000000000..e172f3720 --- /dev/null +++ b/pkg/gui/gpg.go @@ -0,0 +1,41 @@ +package gui + +// Currently there is a bug where if we switch to a subprocess from within +// WithWaitingStatus we get stuck there and can't return to lazygit. We could +// fix this bug, or just stop running subprocesses from within there, given that +// we don't need to see a loading status if we're in a subprocess. +func (gui *Gui) withGpgHandling(cmdStr string, waitingStatus string, onSuccess func() error) error { + useSubprocess := gui.GitCommand.UsingGpg() + if useSubprocess { + // Need to remember why we use the shell for the subprocess but not in the other case + // Maybe there's no good reason + success, err := gui.runSubprocessWithSuspense(gui.OSCommand.ShellCommandFromString(cmdStr)) + if success && onSuccess != nil { + if err := onSuccess(); err != nil { + return err + } + } + if err := gui.refreshSidePanels(refreshOptions{mode: ASYNC}); err != nil { + return err + } + + if err != nil { + return err + } + } else { + return gui.WithWaitingStatus(waitingStatus, func() error { + err := gui.OSCommand.RunCommand(cmdStr) + if err != nil { + return err + } else if onSuccess != nil { + if err := onSuccess(); err != nil { + return err + } + } + + return gui.refreshSidePanels(refreshOptions{mode: ASYNC}) + }) + } + + return nil +} diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index 5b5a3426d..937b5b6e6 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -90,7 +90,7 @@ type Gui struct { // recent repo with the recent repos popup showing showRecentRepos bool - Mutexes guiStateMutexes + Mutexes guiMutexes // findSuggestions will take a string that the user has typed into a prompt // and return a slice of suggestions which match that string. @@ -288,12 +288,13 @@ type Modes struct { Diffing Diffing } -type guiStateMutexes struct { +type guiMutexes struct { RefreshingFilesMutex sync.Mutex RefreshingStatusMutex sync.Mutex FetchMutex sync.Mutex BranchCommitsMutex sync.Mutex LineByLinePanelMutex sync.Mutex + SubprocessMutex sync.Mutex } type guiState struct { @@ -476,6 +477,7 @@ func (gui *Gui) Run() error { if err != nil { return err } + gui.g = g // TODO: always use gui.g rather than passing g around everywhere defer g.Close() @@ -568,7 +570,25 @@ func (gui *Gui) RunAndHandleError() error { }) } -func (gui *Gui) runSubprocessWithSuspense(subprocess *exec.Cmd) error { +// returns whether command exited without error or not +func (gui *Gui) runSubprocessWithSuspenseAndRefresh(subprocess *exec.Cmd) error { + _, err := gui.runSubprocessWithSuspense(subprocess) + if err != nil { + return err + } + + if err := gui.refreshSidePanels(refreshOptions{mode: ASYNC}); err != nil { + return err + } + + return nil +} + +// returns whether command exited without error or not +func (gui *Gui) runSubprocessWithSuspense(subprocess *exec.Cmd) (bool, error) { + gui.Mutexes.SubprocessMutex.Lock() + defer gui.Mutexes.SubprocessMutex.Unlock() + if replaying() { // we do not yet support running subprocesses within integration tests. So if // we're replaying an integration test and we're inside this method, something @@ -577,21 +597,17 @@ func (gui *Gui) runSubprocessWithSuspense(subprocess *exec.Cmd) error { log.Fatal("opening subprocesses not yet supported in integration tests. Chances are that this test is running too fast and a subprocess is accidentally opened") } - if err := gocui.Screen.Suspend(); err != nil { - return gui.surfaceError(err) + if err := gui.g.Suspend(); err != nil { + return false, gui.surfaceError(err) } cmdErr := gui.runSubprocess(subprocess) - if err := gocui.Screen.Resume(); err != nil { - return gui.surfaceError(err) + if err := gui.g.Resume(); err != nil { + return false, gui.surfaceError(err) } - if err := gui.refreshSidePanels(refreshOptions{mode: ASYNC}); err != nil { - return err - } - - return gui.surfaceError(cmdErr) + return cmdErr == nil, gui.surfaceError(cmdErr) } func (gui *Gui) runSubprocess(subprocess *exec.Cmd) error { diff --git a/pkg/gui/rebase_options_panel.go b/pkg/gui/rebase_options_panel.go index 0fbe5d2c6..b1bcf1e7a 100644 --- a/pkg/gui/rebase_options_panel.go +++ b/pkg/gui/rebase_options_panel.go @@ -50,7 +50,7 @@ func (gui *Gui) genericMergeCommand(command string) error { if status == commands.REBASE_MODE_MERGING && command != "abort" && gui.Config.GetUserConfig().Git.Merging.ManualCommit { sub := gui.OSCommand.PrepareSubProcess("git", commandType, fmt.Sprintf("--%s", command)) if sub != nil { - return gui.runSubprocessWithSuspense(sub) + return gui.runSubprocessWithSuspenseAndRefresh(sub) } return nil } diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index 76e38ae6e..91aa18eb8 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -256,6 +256,7 @@ type TranslationSet struct { UndoingStatus string RedoingStatus string CheckingOutStatus string + CommittingStatus string CommitFiles string LcViewCommitFiles string CommitFilesTitle string @@ -891,6 +892,7 @@ func englishTranslationSet() TranslationSet { UndoingStatus: "undoing", RedoingStatus: "redoing", CheckingOutStatus: "checking out", + CommittingStatus: "committing", CommitFiles: "Commit files", LcViewCommitFiles: "view commit's files", CommitFilesTitle: "Commit Files", diff --git a/vendor/github.com/jesseduffield/gocui/gui.go b/vendor/github.com/jesseduffield/gocui/gui.go index c537a32b6..9b2d1ad25 100644 --- a/vendor/github.com/jesseduffield/gocui/gui.go +++ b/vendor/github.com/jesseduffield/gocui/gui.go @@ -13,6 +13,7 @@ import ( "sync" "time" + "github.com/gdamore/tcell/v2" "github.com/go-errors/errors" ) @@ -158,6 +159,10 @@ type Gui struct { SearchEscapeKey interface{} NextSearchMatchKey interface{} PrevSearchMatchKey interface{} + + screen tcell.Screen + suspendedMutex sync.Mutex + suspended bool } // NewGui returns a new Gui object with a given output mode. @@ -166,9 +171,9 @@ func NewGui(mode OutputMode, supportOverlaps bool, playMode PlayMode, headless b var err error if headless { - err = tcellInitSimulation() + err = g.tcellInitSimulation() } else { - err = tcellInit() + err = g.tcellInit() } if err != nil { return nil, err @@ -1000,6 +1005,10 @@ func (g *Gui) drawListFooter(v *View, fgColor, bgColor Attribute) error { // draw manages the cursor and calls the draw function of a view. func (g *Gui) draw(v *View) error { + if g.suspended { + return nil + } + if g.Cursor { if curview := g.currentView; curview != nil { vMaxX, vMaxY := curview.Size() @@ -1166,6 +1175,11 @@ func (g *Gui) StartTicking() { for { select { case <-ticker.C: + // I'm okay with having a data race here: there's no harm in letting one of these updates through + if g.suspended { + continue outer + } + for _, view := range g.Views() { if view.HasLoader { g.userEvents <- userEvent{func(g *Gui) error { return nil }} @@ -1290,3 +1304,29 @@ func (g *Gui) replayRecording() { log.Fatal("gocui should have already exited") } + +func (g *Gui) Suspend() error { + g.suspendedMutex.Lock() + defer g.suspendedMutex.Unlock() + + if g.suspended { + return errors.New("Already suspended") + } + + g.suspended = true + + return g.screen.Suspend() +} + +func (g *Gui) Resume() error { + g.suspendedMutex.Lock() + defer g.suspendedMutex.Unlock() + + if !g.suspended { + return errors.New("Cannot resume because we are not suspended") + } + + g.suspended = false + + return g.screen.Resume() +} diff --git a/vendor/github.com/jesseduffield/gocui/loader.go b/vendor/github.com/jesseduffield/gocui/loader.go index d6715ac6d..0aad5ae85 100644 --- a/vendor/github.com/jesseduffield/gocui/loader.go +++ b/vendor/github.com/jesseduffield/gocui/loader.go @@ -2,19 +2,6 @@ package gocui import "time" -func (g *Gui) loaderTick() { - go func() { - for range time.Tick(time.Millisecond * 50) { - for _, view := range g.Views() { - if view.HasLoader { - g.userEvents <- userEvent{func(g *Gui) error { return nil }} - break - } - } - } - }() -} - func (v *View) loaderLines() [][]cell { duplicate := make([][]cell, len(v.lines)) for i := range v.lines { diff --git a/vendor/github.com/jesseduffield/gocui/tcell_driver.go b/vendor/github.com/jesseduffield/gocui/tcell_driver.go index 3bc92b344..8d5045bce 100644 --- a/vendor/github.com/jesseduffield/gocui/tcell_driver.go +++ b/vendor/github.com/jesseduffield/gocui/tcell_driver.go @@ -21,23 +21,25 @@ type oldStyle struct { } // tcellInit initializes tcell screen for use. -func tcellInit() error { +func (g *Gui) tcellInit() error { if s, e := tcell.NewScreen(); e != nil { return e } else if e = s.Init(); e != nil { return e } else { + g.screen = s Screen = s return nil } } // tcellInitSimulation initializes tcell screen for use. -func tcellInitSimulation() error { +func (g *Gui) tcellInitSimulation() error { s := tcell.NewSimulationScreen("") if e := s.Init(); e != nil { return e } else { + g.screen = s Screen = s return nil } diff --git a/vendor/modules.txt b/vendor/modules.txt index 6ee82a59c..7c98c9239 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -149,7 +149,7 @@ github.com/jesseduffield/go-git/v5/utils/merkletrie/filesystem github.com/jesseduffield/go-git/v5/utils/merkletrie/index github.com/jesseduffield/go-git/v5/utils/merkletrie/internal/frame github.com/jesseduffield/go-git/v5/utils/merkletrie/noder -# github.com/jesseduffield/gocui v0.3.1-0.20210409121040-210802112d8a +# github.com/jesseduffield/gocui v0.3.1-0.20210410011117-a2bb4baca390 ## explicit github.com/jesseduffield/gocui # github.com/jesseduffield/termbox-go v0.0.0-20200823212418-a2289ed6aafe