diff --git a/pkg/commands/commits.go b/pkg/commands/commits.go index 3582131a6..df1a8a2c7 100644 --- a/pkg/commands/commits.go +++ b/pkg/commands/commits.go @@ -2,7 +2,6 @@ package commands import ( "fmt" - "os/exec" "strings" "github.com/jesseduffield/lazygit/pkg/commands/models" @@ -53,11 +52,6 @@ func (c *GitCommand) AmendHead() error { return c.OSCommand.RunCommand(c.AmendHeadCmdStr()) } -// 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" } diff --git a/pkg/commands/dummies.go b/pkg/commands/dummies.go index 933959fe7..6feb908e3 100644 --- a/pkg/commands/dummies.go +++ b/pkg/commands/dummies.go @@ -20,6 +20,5 @@ func NewDummyGitCommandWithOSCommand(osCommand *oscommands.OSCommand) *GitComman Tr: i18n.NewTranslationSet(utils.NewDummyLog()), Config: config.NewDummyAppConfig(), getGitConfigValue: func(string) (string, error) { return "", nil }, - removeFile: func(string) error { return nil }, } } diff --git a/pkg/commands/files.go b/pkg/commands/files.go index 9b4895710..4071c8c76 100644 --- a/pkg/commands/files.go +++ b/pkg/commands/files.go @@ -2,20 +2,14 @@ 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/stretchr/testify/assert" ) // CatFile obtains the content of a file @@ -145,7 +139,7 @@ func (c *GitCommand) DiscardAllFileChanges(file *models.File) error { } if file.Added { - return c.removeFile(file.Name) + return c.OSCommand.RemoveFile(file.Name) } return c.DiscardUnstagedFileChanges(file) } @@ -346,381 +340,3 @@ func (c *GitCommand) EditFileCmdStr(filename string) (string, error) { 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 index 62aa388cb..20c18a9a0 100644 --- a/pkg/commands/files_test.go +++ b/pkg/commands/files_test.go @@ -2,6 +2,7 @@ package commands import ( "fmt" + "io/ioutil" "os/exec" "runtime" "testing" @@ -323,7 +324,7 @@ func TestGitCommandDiscardAllFileChanges(t *testing.T) { var cmdsCalled *[][]string gitCmd := NewDummyGitCommand() gitCmd.OSCommand.Command, cmdsCalled = s.command() - gitCmd.removeFile = s.removeFile + gitCmd.OSCommand.SetRemoveFile(s.removeFile) s.test(cmdsCalled, gitCmd.DiscardAllFileChanges(s.file)) }) } @@ -465,3 +466,381 @@ func TestGitCommandCheckoutFile(t *testing.T) { }) } } + +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/git.go b/pkg/commands/git.go index 0c15b2788..2745c1935 100644 --- a/pkg/commands/git.go +++ b/pkg/commands/git.go @@ -33,7 +33,6 @@ type GitCommand struct { Tr *i18n.TranslationSet Config config.AppConfigurer getGitConfigValue func(string) (string, error) - removeFile func(string) error DotGitDir string onSuccessfulContinue func() error PatchManager *patch.PatchManager @@ -75,7 +74,6 @@ func NewGitCommand(log *logrus.Entry, osCommand *oscommands.OSCommand, tr *i18n. Repo: repo, Config: config, getGitConfigValue: getGitConfigValue, - removeFile: os.RemoveAll, DotGitDir: dotGitDir, PushToCurrent: pushToCurrent, } @@ -85,6 +83,14 @@ func NewGitCommand(log *logrus.Entry, osCommand *oscommands.OSCommand, tr *i18n. return gitCommand, nil } +func (c *GitCommand) WithSpan(span string) *GitCommand { + newGitCommand := &GitCommand{} + *newGitCommand = *c + newGitCommand.OSCommand = c.OSCommand.WithSpan(span) + newGitCommand.PatchManager.ApplyPatch = newGitCommand.ApplyPatch + return newGitCommand +} + func navigateToRepoRootDirectory(stat func(string) (os.FileInfo, error), chdir func(string) error) error { gitDir := env.GetGitDirEnv() if gitDir != "" { diff --git a/pkg/commands/oscommands/exec_live_default.go b/pkg/commands/oscommands/exec_live_default.go index 6f51ecedc..124792840 100644 --- a/pkg/commands/oscommands/exec_live_default.go +++ b/pkg/commands/oscommands/exec_live_default.go @@ -20,6 +20,7 @@ import ( // NOTE: If the return data is empty it won't written anything to stdin func RunCommandWithOutputLiveWrapper(c *OSCommand, command string, output func(string) string) error { c.Log.WithField("command", command).Info("RunCommand") + c.LogCommand(command, true) cmd := c.ExecutableFromString(command) cmd.Env = append(cmd.Env, "LANG=en_US.UTF-8", "LC_ALL=en_US.UTF-8") diff --git a/pkg/commands/oscommands/os.go b/pkg/commands/oscommands/os.go index 5b9b4dd91..2c85cfc5a 100644 --- a/pkg/commands/oscommands/os.go +++ b/pkg/commands/oscommands/os.go @@ -40,7 +40,44 @@ type OSCommand struct { Command func(string, ...string) *exec.Cmd BeforeExecuteCmd func(*exec.Cmd) Getenv func(string) string - onRunCommand func(string) + + // callback to run before running a command, i.e. for the purposes of logging + onRunCommand func(CmdLogEntry) + + // something like 'Staging File': allows us to group cmd logs under a single title + CmdLogSpan string + + removeFile func(string) error +} + +// TODO: make these fields private +type CmdLogEntry struct { + // e.g. 'git commit -m "haha"' + cmdStr string + // Span is something like 'Staging File'. Multiple commands can be grouped under the same + // span + span string + + // sometimes our command is direct like 'git commit', and sometimes it's a + // command to remove a file but through Go's standard library rather than the + // command line + commandLine bool +} + +func (e CmdLogEntry) GetCmdStr() string { + return e.cmdStr +} + +func (e CmdLogEntry) GetSpan() string { + return e.span +} + +func (e CmdLogEntry) GetCommandLine() bool { + return e.commandLine +} + +func NewCmdLogEntry(cmdStr string, span string, commandLine bool) CmdLogEntry { + return CmdLogEntry{cmdStr: cmdStr, span: span, commandLine: commandLine} } // NewOSCommand os command runner @@ -52,10 +89,30 @@ func NewOSCommand(log *logrus.Entry, config config.AppConfigurer) *OSCommand { Command: secureexec.Command, BeforeExecuteCmd: func(*exec.Cmd) {}, Getenv: os.Getenv, + removeFile: os.RemoveAll, } } -func (c *OSCommand) SetOnRunCommand(f func(string)) { +func (c *OSCommand) WithSpan(span string) *OSCommand { + newOSCommand := &OSCommand{} + *newOSCommand = *c + newOSCommand.CmdLogSpan = span + return newOSCommand +} + +func (c *OSCommand) LogExecCmd(cmd *exec.Cmd) { + c.LogCommand(strings.Join(cmd.Args, " "), true) +} + +func (c *OSCommand) LogCommand(cmdStr string, commandLine bool) { + c.Log.WithField("command", cmdStr).Info("RunCommand") + + if c.onRunCommand != nil && c.CmdLogSpan != "" { + c.onRunCommand(NewCmdLogEntry(cmdStr, c.CmdLogSpan, commandLine)) + } +} + +func (c *OSCommand) SetOnRunCommand(f func(CmdLogEntry)) { c.onRunCommand = f } @@ -65,6 +122,11 @@ func (c *OSCommand) SetCommand(cmd func(string, ...string) *exec.Cmd) { c.Command = cmd } +// To be used for testing only +func (c *OSCommand) SetRemoveFile(f func(string) error) { + c.removeFile = f +} + func (c *OSCommand) SetBeforeExecuteCmd(cmd func(*exec.Cmd)) { c.BeforeExecuteCmd = cmd } @@ -74,10 +136,7 @@ type RunCommandOptions struct { } func (c *OSCommand) RunCommandWithOutputWithOptions(command string, options RunCommandOptions) (string, error) { - c.Log.WithField("command", command).Info("RunCommand") - if c.onRunCommand != nil { - c.onRunCommand(command) - } + c.LogCommand(command, true) cmd := c.ExecutableFromString(command) cmd.Env = append(cmd.Env, "GIT_TERMINAL_PROMPT=0") // prevents git from prompting us for input which would freeze the program @@ -102,11 +161,8 @@ func (c *OSCommand) RunCommandWithOutput(formatString string, formatArgs ...inte if formatArgs != nil { command = fmt.Sprintf(formatString, formatArgs...) } - c.Log.WithField("command", command).Info("RunCommand") - if c.onRunCommand != nil { - c.onRunCommand(command) - } cmd := c.ExecutableFromString(command) + c.LogExecCmd(cmd) output, err := sanitisedCommandOutput(cmd.CombinedOutput()) if err != nil { c.Log.WithField("command", command).Error(output) @@ -114,20 +170,9 @@ func (c *OSCommand) RunCommandWithOutput(formatString string, formatArgs ...inte return output, err } -func (c *OSCommand) CatFile(filename string) (string, error) { - arr := append(c.Platform.CatCmd, filename) - cmdStr := strings.Join(arr, " ") - c.Log.WithField("command", cmdStr).Info("Cat") - cmd := c.Command(arr[0], arr[1:]...) - output, err := sanitisedCommandOutput(cmd.CombinedOutput()) - if err != nil { - c.Log.WithField("command", cmdStr).Error(output) - } - return output, err -} - // RunExecutableWithOutput runs an executable file and returns its output func (c *OSCommand) RunExecutableWithOutput(cmd *exec.Cmd) (string, error) { + c.LogExecCmd(cmd) c.BeforeExecuteCmd(cmd) return sanitisedCommandOutput(cmd.CombinedOutput()) } @@ -165,6 +210,18 @@ func (c *OSCommand) RunCommandWithOutputLive(command string, output func(string) return RunCommandWithOutputLiveWrapper(c, command, output) } +func (c *OSCommand) CatFile(filename string) (string, error) { + arr := append(c.Platform.CatCmd, filename) + cmdStr := strings.Join(arr, " ") + c.Log.WithField("command", cmdStr).Info("Cat") + cmd := c.Command(arr[0], arr[1:]...) + output, err := sanitisedCommandOutput(cmd.CombinedOutput()) + if err != nil { + c.Log.WithField("command", cmdStr).Error(output) + } + return output, err +} + // DetectUnamePass detect a username / password / passphrase question in a command // promptUserForCredential is a function that gets executed when this function detect you need to fillin a password or passphrase // The promptUserForCredential argument will be "username", "password" or "passphrase" and expects the user's password/passphrase or username back @@ -201,9 +258,9 @@ func (c *OSCommand) RunCommand(formatString string, formatArgs ...interface{}) e // RunShellCommand runs shell commands i.e. 'sh -c '. Good for when you // need access to the shell func (c *OSCommand) RunShellCommand(command string) error { - c.Log.WithField("command", command).Info("RunShellCommand") - cmd := c.Command(c.Platform.Shell, c.Platform.ShellArg, command) + c.LogExecCmd(cmd) + _, err := sanitisedCommandOutput(cmd.CombinedOutput()) return err @@ -236,6 +293,7 @@ func sanitisedCommandOutput(output []byte, err error) (string, error) { // OpenFile opens a file with the given func (c *OSCommand) OpenFile(filename string) error { + c.LogCommand(fmt.Sprintf("Opening file '%s'", filename), false) commandTemplate := c.Config.GetUserConfig().OS.OpenCommand templateValues := map[string]string{ "filename": c.Quote(filename), @@ -248,6 +306,7 @@ func (c *OSCommand) OpenFile(filename string) error { // OpenLink opens a file with the given func (c *OSCommand) OpenLink(link string) error { + c.LogCommand(fmt.Sprintf("Opening link '%s'", link), false) commandTemplate := c.Config.GetUserConfig().OS.OpenLinkCommand templateValues := map[string]string{ "link": c.Quote(link), @@ -265,6 +324,7 @@ func (c *OSCommand) PrepareSubProcess(cmdName string, commandArgs ...string) *ex if cmd != nil { cmd.Env = append(os.Environ(), "GIT_OPTIONAL_LOCKS=0") } + c.LogExecCmd(cmd) return cmd } @@ -290,6 +350,7 @@ func (c *OSCommand) Quote(message string) string { // AppendLineToFile adds a new line in file func (c *OSCommand) AppendLineToFile(filename, line string) error { + c.LogCommand(fmt.Sprintf("Appending '%s' to file '%s'", line, filename), false) f, err := os.OpenFile(filename, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600) if err != nil { return utils.WrapError(err) @@ -310,6 +371,7 @@ func (c *OSCommand) CreateTempFile(filename, content string) (string, error) { c.Log.Error(err) return "", utils.WrapError(err) } + c.LogCommand(fmt.Sprintf("Creating temp file '%s'", tmpfile.Name()), false) if _, err := tmpfile.WriteString(content); err != nil { c.Log.Error(err) @@ -325,6 +387,7 @@ func (c *OSCommand) CreateTempFile(filename, content string) (string, error) { // CreateFileWithContent creates a file with the given content func (c *OSCommand) CreateFileWithContent(path string, content string) error { + c.LogCommand(fmt.Sprintf("Creating file '%s'", path), false) if err := os.MkdirAll(filepath.Dir(path), os.ModePerm); err != nil { c.Log.Error(err) return err @@ -340,6 +403,7 @@ func (c *OSCommand) CreateFileWithContent(path string, content string) error { // Remove removes a file or directory at the specified path func (c *OSCommand) Remove(filename string) error { + c.LogCommand(fmt.Sprintf("Removing '%s'", filename), false) err := os.RemoveAll(filename) return utils.WrapError(err) } @@ -360,6 +424,7 @@ func (c *OSCommand) FileExists(path string) (bool, error) { // before running it func (c *OSCommand) RunPreparedCommand(cmd *exec.Cmd) error { c.BeforeExecuteCmd(cmd) + c.LogExecCmd(cmd) out, err := cmd.CombinedOutput() outString := string(out) c.Log.Info(outString) @@ -383,12 +448,16 @@ func (c *OSCommand) GetLazygitPath() string { // PipeCommands runs a heap of commands and pipes their inputs/outputs together like A | B | C func (c *OSCommand) PipeCommands(commandStrings ...string) error { - cmds := make([]*exec.Cmd, len(commandStrings)) - + logCmdStr := "" for i, str := range commandStrings { + if i > 0 { + logCmdStr += " | " + } + logCmdStr += str cmds[i] = c.ExecutableFromString(str) } + c.LogCommand(logCmdStr, true) for i := 0; i < len(cmds)-1; i++ { stdout, err := cmds[i].StdoutPipe() @@ -479,5 +548,12 @@ func RunLineOutputCmd(cmd *exec.Cmd, onLine func(line string) (bool, error)) err } func (c *OSCommand) CopyToClipboard(str string) error { + c.LogCommand(fmt.Sprintf("Copying '%s' to clipboard", utils.TruncateWithEllipsis(str, 40)), false) return clipboard.WriteAll(str) } + +func (c *OSCommand) RemoveFile(path string) error { + c.LogCommand(fmt.Sprintf("Deleting path '%s'", path), false) + + return c.removeFile(path) +} diff --git a/pkg/commands/patch_rebases.go b/pkg/commands/patch_rebases.go index 6f9e08679..a6901dc3f 100644 --- a/pkg/commands/patch_rebases.go +++ b/pkg/commands/patch_rebases.go @@ -137,7 +137,7 @@ func (c *GitCommand) MovePatchToSelectedCommit(commits []*models.Commit, sourceC return c.GenericMergeOrRebaseAction("rebase", "continue") } -func (c *GitCommand) PullPatchIntoIndex(commits []*models.Commit, commitIdx int, p *patch.PatchManager, stash bool) error { +func (c *GitCommand) MovePatchIntoIndex(commits []*models.Commit, commitIdx int, p *patch.PatchManager, stash bool) error { if stash { if err := c.StashSave(c.Tr.StashPrefix + commits[commitIdx].Sha); err != nil { return err diff --git a/pkg/commands/stash_entries.go b/pkg/commands/stash_entries.go index 48944c626..2707903d7 100644 --- a/pkg/commands/stash_entries.go +++ b/pkg/commands/stash_entries.go @@ -21,7 +21,7 @@ func (c *GitCommand) ShowStashEntryCmdStr(index int) string { // StashSaveStagedChanges stashes only the currently staged changes. This takes a few steps // shoutouts to Joe on https://stackoverflow.com/questions/14759748/stashing-only-staged-changes-in-git-is-it-possible func (c *GitCommand) StashSaveStagedChanges(message string) error { - + // wrap in 'writing', which uses a mutex if err := c.RunCommand("git stash --keep-index"); err != nil { return err } diff --git a/pkg/gui/arrangement.go b/pkg/gui/arrangement.go index b2a44f6eb..be2321a9a 100644 --- a/pkg/gui/arrangement.go +++ b/pkg/gui/arrangement.go @@ -146,7 +146,7 @@ func (gui *Gui) getWindowDimensions(informationStr string, appStatus string) map mainPanelsDirection = boxlayout.COLUMN } - cmdLogSize := 10 + cmdLogSize := 40 // TODO: make configurable root := &boxlayout.Box{ Direction: boxlayout.ROW, diff --git a/pkg/gui/commit_message_panel.go b/pkg/gui/commit_message_panel.go index 133174d77..21a5d2cfc 100644 --- a/pkg/gui/commit_message_panel.go +++ b/pkg/gui/commit_message_panel.go @@ -5,6 +5,7 @@ import ( "strings" "github.com/jesseduffield/gocui" + "github.com/jesseduffield/lazygit/pkg/commands/oscommands" "github.com/jesseduffield/lazygit/pkg/utils" ) @@ -19,7 +20,9 @@ func (gui *Gui) handleCommitConfirm() error { flags = "--no-verify" } - return gui.withGpgHandling(gui.GitCommand.CommitCmdStr(message, flags), gui.Tr.CommittingStatus, func() error { + cmdStr := gui.GitCommand.CommitCmdStr(message, flags) + gui.OnRunCommand(oscommands.NewCmdLogEntry(cmdStr, "Commit", true)) + return gui.withGpgHandling(cmdStr, gui.Tr.CommittingStatus, func() error { _ = gui.returnFromContext() gui.clearEditorView(gui.Views.CommitMessage) return nil diff --git a/pkg/gui/discard_changes_menu_panel.go b/pkg/gui/discard_changes_menu_panel.go index 71f9e6d96..853fd53d3 100644 --- a/pkg/gui/discard_changes_menu_panel.go +++ b/pkg/gui/discard_changes_menu_panel.go @@ -12,7 +12,7 @@ func (gui *Gui) handleCreateDiscardMenu() error { { displayString: gui.Tr.LcDiscardAllChanges, onPress: func() error { - if err := gui.GitCommand.DiscardAllDirChanges(node); err != nil { + if err := gui.GitCommand.WithSpan("Discard all changes in directory").DiscardAllDirChanges(node); err != nil { return gui.surfaceError(err) } return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []RefreshableView{FILES}}) @@ -24,7 +24,7 @@ func (gui *Gui) handleCreateDiscardMenu() error { menuItems = append(menuItems, &menuItem{ displayString: gui.Tr.LcDiscardUnstagedChanges, onPress: func() error { - if err := gui.GitCommand.DiscardUnstagedDirChanges(node); err != nil { + if err := gui.GitCommand.WithSpan("Discard unstaged changes in directory").DiscardUnstagedDirChanges(node); err != nil { return gui.surfaceError(err) } @@ -52,7 +52,8 @@ func (gui *Gui) handleCreateDiscardMenu() error { { displayString: gui.Tr.LcDiscardAllChanges, onPress: func() error { - if err := gui.GitCommand.DiscardAllFileChanges(file); err != nil { + gui.Log.Warn("HA?") + if err := gui.GitCommand.WithSpan("Discard all changes in file").DiscardAllFileChanges(file); err != nil { return gui.surfaceError(err) } return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []RefreshableView{FILES}}) @@ -64,7 +65,7 @@ func (gui *Gui) handleCreateDiscardMenu() error { menuItems = append(menuItems, &menuItem{ displayString: gui.Tr.LcDiscardUnstagedChanges, onPress: func() error { - if err := gui.GitCommand.DiscardUnstagedFileChanges(file); err != nil { + if err := gui.GitCommand.WithSpan("Discard all unstaged changes in file").DiscardUnstagedFileChanges(file); err != nil { return gui.surfaceError(err) } diff --git a/pkg/gui/files_panel.go b/pkg/gui/files_panel.go index f84d0c492..a09e8158f 100644 --- a/pkg/gui/files_panel.go +++ b/pkg/gui/files_panel.go @@ -8,6 +8,7 @@ import ( "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/commands" "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/gui/filetree" "github.com/jesseduffield/lazygit/pkg/utils" @@ -218,11 +219,11 @@ func (gui *Gui) handleFilePress() error { } if file.HasUnstagedChanges { - if err := gui.GitCommand.StageFile(file.Name); err != nil { + if err := gui.GitCommand.WithSpan("Stage file").StageFile(file.Name); err != nil { return gui.surfaceError(err) } } else { - if err := gui.GitCommand.UnStageFile(file.Names(), file.Tracked); err != nil { + if err := gui.GitCommand.WithSpan("Unstage file").UnStageFile(file.Names(), file.Tracked); err != nil { return gui.surfaceError(err) } } @@ -234,12 +235,12 @@ func (gui *Gui) handleFilePress() error { } if node.GetHasUnstagedChanges() { - if err := gui.GitCommand.StageFile(node.Path); err != nil { + if err := gui.GitCommand.WithSpan("Stage file").StageFile(node.Path); err != nil { return gui.surfaceError(err) } } else { // pretty sure it doesn't matter that we're always passing true here - if err := gui.GitCommand.UnStageFile([]string{node.Path}, true); err != nil { + if err := gui.GitCommand.WithSpan("Unstage file").UnStageFile([]string{node.Path}, true); err != nil { return gui.surfaceError(err) } } @@ -268,9 +269,9 @@ func (gui *Gui) focusAndSelectFile() error { func (gui *Gui) handleStageAll() error { var err error if gui.allFilesStaged() { - err = gui.GitCommand.UnstageAll() + err = gui.GitCommand.WithSpan("Unstage all files").UnstageAll() } else { - err = gui.GitCommand.StageAll() + err = gui.GitCommand.WithSpan("Stage all files").StageAll() } if err != nil { _ = gui.surfaceError(err) @@ -293,10 +294,12 @@ func (gui *Gui) handleIgnoreFile() error { return gui.createErrorPanel("Cannot ignore .gitignore") } + gitCommand := gui.GitCommand.WithSpan("Ignore file") + unstageFiles := func() error { return node.ForEachFile(func(file *models.File) error { if file.HasStagedChanges { - if err := gui.GitCommand.UnStageFile(file.Names(), file.Tracked); err != nil { + if err := gitCommand.UnStageFile(file.Names(), file.Tracked); err != nil { return err } } @@ -315,11 +318,11 @@ func (gui *Gui) handleIgnoreFile() error { return err } - if err := gui.GitCommand.RemoveTrackedFiles(node.GetPath()); err != nil { + if err := gitCommand.RemoveTrackedFiles(node.GetPath()); err != nil { return err } - if err := gui.GitCommand.Ignore(node.GetPath()); err != nil { + if err := gitCommand.Ignore(node.GetPath()); err != nil { return err } return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{FILES}}) @@ -331,7 +334,7 @@ func (gui *Gui) handleIgnoreFile() error { return err } - if err := gui.GitCommand.Ignore(node.GetPath()); err != nil { + if err := gitCommand.Ignore(node.GetPath()); err != nil { return gui.surfaceError(err) } @@ -364,7 +367,7 @@ func (gui *Gui) commitPrefixConfigForRepo() *config.CommitPrefixConfig { func (gui *Gui) prepareFilesForCommit() error { noStagedFiles := len(gui.stagedFiles()) == 0 if noStagedFiles && gui.Config.GetUserConfig().Gui.SkipNoStagedFilesWarning { - err := gui.GitCommand.StageAll() + err := gui.GitCommand.WithSpan("Stage all files").StageAll() if err != nil { return err } @@ -419,7 +422,7 @@ func (gui *Gui) promptToStageAllAndRetry(retry func() error) error { title: gui.Tr.NoFilesStagedTitle, prompt: gui.Tr.NoFilesStagedPrompt, handleConfirm: func() error { - if err := gui.GitCommand.StageAll(); err != nil { + if err := gui.GitCommand.WithSpan("Stage all files").StageAll(); err != nil { return gui.surfaceError(err) } if err := gui.refreshFilesAndSubmodules(); err != nil { @@ -448,7 +451,9 @@ func (gui *Gui) handleAmendCommitPress() error { title: strings.Title(gui.Tr.AmendLastCommit), prompt: gui.Tr.SureToAmend, handleConfirm: func() error { - return gui.withGpgHandling(gui.GitCommand.AmendHeadCmdStr(), gui.Tr.AmendingStatus, nil) + cmdStr := gui.GitCommand.AmendHeadCmdStr() + gui.OnRunCommand(oscommands.NewCmdLogEntry(cmdStr, "Amend commit", true)) + return gui.withGpgHandling(cmdStr, gui.Tr.AmendingStatus, nil) }, }) } @@ -465,7 +470,7 @@ func (gui *Gui) handleCommitEditorPress() error { } return gui.runSubprocessWithSuspenseAndRefresh( - gui.OSCommand.PrepareSubProcess("git", "commit"), + gui.OSCommand.WithSpan("Commit").PrepareSubProcess("git", "commit"), ) } @@ -476,7 +481,7 @@ func (gui *Gui) editFile(filename string) error { } return gui.runSubprocessWithSuspenseAndRefresh( - gui.OSCommand.PrepareShellSubProcess(cmdStr), + gui.OSCommand.WithSpan("Edit file").PrepareShellSubProcess(cmdStr), ) } @@ -654,6 +659,7 @@ func (gui *Gui) pullFiles(opts PullFilesOptions) error { mode := gui.Config.GetUserConfig().Git.Pull.Mode + // TODO: this doesn't look like a good idea. Why the goroutine? go utils.Safe(func() { _ = gui.pullWithMode(mode, opts) }) return nil @@ -663,7 +669,9 @@ func (gui *Gui) pullWithMode(mode string, opts PullFilesOptions) error { gui.Mutexes.FetchMutex.Lock() defer gui.Mutexes.FetchMutex.Unlock() - err := gui.GitCommand.Fetch( + gitCommand := gui.GitCommand.WithSpan("Pull") + + err := gitCommand.Fetch( commands.FetchOptions{ PromptUserForCredential: gui.promptUserForCredential, RemoteName: opts.RemoteName, @@ -677,13 +685,13 @@ func (gui *Gui) pullWithMode(mode string, opts PullFilesOptions) error { switch mode { case "rebase": - err := gui.GitCommand.RebaseBranch("FETCH_HEAD") + err := gitCommand.RebaseBranch("FETCH_HEAD") return gui.handleGenericMergeCommandResult(err) case "merge": - err := gui.GitCommand.Merge("FETCH_HEAD", commands.MergeOpts{}) + err := gitCommand.Merge("FETCH_HEAD", commands.MergeOpts{}) return gui.handleGenericMergeCommandResult(err) case "ff-only": - err := gui.GitCommand.Merge("FETCH_HEAD", commands.MergeOpts{FastForwardOnly: true}) + err := gitCommand.Merge("FETCH_HEAD", commands.MergeOpts{FastForwardOnly: true}) return gui.handleGenericMergeCommandResult(err) default: return gui.createErrorPanel(fmt.Sprintf("git pull mode '%s' unrecognised", mode)) @@ -696,7 +704,7 @@ func (gui *Gui) pushWithForceFlag(force bool, upstream string, args string) erro } go utils.Safe(func() { branchName := gui.getCheckedOutBranch().Name - err := gui.GitCommand.Push(branchName, force, upstream, args, gui.promptUserForCredential) + err := gui.GitCommand.WithSpan("Push").Push(branchName, force, upstream, args, gui.promptUserForCredential) if err != nil && !force && strings.Contains(err.Error(), "Updates were rejected") { forcePushDisabled := gui.Config.GetUserConfig().Git.DisableForcePushing if forcePushDisabled { @@ -785,7 +793,7 @@ func (gui *Gui) handleSwitchToMerge() error { } func (gui *Gui) openFile(filename string) error { - if err := gui.OSCommand.OpenFile(filename); err != nil { + if err := gui.OSCommand.WithSpan("Open file").OpenFile(filename); err != nil { return gui.surfaceError(err) } return nil @@ -804,6 +812,7 @@ func (gui *Gui) handleCustomCommand() error { return gui.prompt(promptOpts{ title: gui.Tr.CustomCommand, handleConfirm: func(command string) error { + gui.OnRunCommand(oscommands.NewCmdLogEntry(command, "Custom command", true)) return gui.runSubprocessWithSuspenseAndRefresh( gui.OSCommand.PrepareShellSubProcess(command), ) @@ -816,13 +825,13 @@ func (gui *Gui) handleCreateStashMenu() error { { displayString: gui.Tr.LcStashAllChanges, onPress: func() error { - return gui.handleStashSave(gui.GitCommand.StashSave) + return gui.handleStashSave(gui.GitCommand.WithSpan("Stash all changes").StashSave) }, }, { displayString: gui.Tr.LcStashStagedChanges, onPress: func() error { - return gui.handleStashSave(gui.GitCommand.StashSaveStagedChanges) + return gui.handleStashSave(gui.GitCommand.WithSpan("Stash staged changes").StashSaveStagedChanges) }, }, } diff --git a/pkg/gui/git_flow.go b/pkg/gui/git_flow.go index 4549e6639..701eda37c 100644 --- a/pkg/gui/git_flow.go +++ b/pkg/gui/git_flow.go @@ -56,7 +56,7 @@ func (gui *Gui) handleCreateGitFlowMenu() error { title: title, handleConfirm: func(name string) error { return gui.runSubprocessWithSuspenseAndRefresh( - gui.OSCommand.PrepareSubProcess("git", "flow", branchType, "start", name), + gui.OSCommand.WithSpan("Git Flow").PrepareSubProcess("git", "flow", branchType, "start", name), ) }, }) diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index 5ba0064db..dbbb25b8a 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -111,7 +111,8 @@ type Gui struct { PauseBackgroundThreads bool // Log of the commands that get run, to be displayed to the user. - CmdLog []string + CmdLog []string + OnRunCommand func(entry oscommands.CmdLogEntry) } type listPanelState struct { @@ -470,15 +471,30 @@ func NewGui(log *logrus.Entry, gitCommand *commands.GitCommand, oSCommand *oscom gui.watchFilesForChanges() - oSCommand.SetOnRunCommand(func(cmdStr string) { - gui.Log.Warn("HHMM") - gui.CmdLog = append(gui.CmdLog, cmdStr) - if gui.Views.CmdLog != nil { - fmt.Fprintln(gui.Views.CmdLog, cmdStr) - gui.Log.Warn("HHMM") - gui.Log.Warn(cmdStr) + onRunCommand := func() func(entry oscommands.CmdLogEntry) { + currentSpan := "" + + return func(entry oscommands.CmdLogEntry) { + if gui.Views.CmdLog == nil { + return + } + + if entry.GetSpan() != currentSpan { + fmt.Fprintln(gui.Views.CmdLog, utils.ColoredString(entry.GetSpan(), color.FgYellow)) + currentSpan = entry.GetSpan() + } + + clrAttr := theme.DefaultTextColor + if !entry.GetCommandLine() { + clrAttr = color.FgMagenta + } + gui.CmdLog = append(gui.CmdLog, entry.GetCmdStr()) + fmt.Fprintln(gui.Views.CmdLog, " "+utils.ColoredString(entry.GetCmdStr(), clrAttr)) } - }) + }() + + oSCommand.SetOnRunCommand(onRunCommand) + gui.OnRunCommand = onRunCommand return gui, nil } diff --git a/pkg/gui/patch_options_panel.go b/pkg/gui/patch_options_panel.go index 90f72fc37..1d31b7e60 100644 --- a/pkg/gui/patch_options_panel.go +++ b/pkg/gui/patch_options_panel.go @@ -33,11 +33,11 @@ func (gui *Gui) handleCreatePatchOptionsMenu() error { onPress: gui.handleDeletePatchFromCommit, }, { - displayString: "pull patch out into index", - onPress: gui.handlePullPatchIntoWorkingTree, + displayString: "move patch out into index", + onPress: gui.handleMovePatchIntoWorkingTree, }, { - displayString: "pull patch into new commit", + displayString: "move patch into new commit", onPress: gui.handlePullPatchIntoNewCommit, }, }...) @@ -98,7 +98,7 @@ func (gui *Gui) handleDeletePatchFromCommit() error { return gui.WithWaitingStatus(gui.Tr.RebasingStatus, func() error { commitIndex := gui.getPatchCommitIndex() - err := gui.GitCommand.DeletePatchesFromCommit(gui.State.Commits, commitIndex, gui.GitCommand.PatchManager) + err := gui.GitCommand.WithSpan("Remove patch from commit").DeletePatchesFromCommit(gui.State.Commits, commitIndex, gui.GitCommand.PatchManager) return gui.handleGenericMergeCommandResult(err) }) } @@ -114,12 +114,12 @@ func (gui *Gui) handleMovePatchToSelectedCommit() error { return gui.WithWaitingStatus(gui.Tr.RebasingStatus, func() error { commitIndex := gui.getPatchCommitIndex() - err := gui.GitCommand.MovePatchToSelectedCommit(gui.State.Commits, commitIndex, gui.State.Panels.Commits.SelectedLineIdx, gui.GitCommand.PatchManager) + err := gui.GitCommand.WithSpan("Move patch to selected commit").MovePatchToSelectedCommit(gui.State.Commits, commitIndex, gui.State.Panels.Commits.SelectedLineIdx, gui.GitCommand.PatchManager) return gui.handleGenericMergeCommandResult(err) }) } -func (gui *Gui) handlePullPatchIntoWorkingTree() error { +func (gui *Gui) handleMovePatchIntoWorkingTree() error { if ok, err := gui.validateNormalWorkingTreeState(); !ok { return err } @@ -131,7 +131,7 @@ func (gui *Gui) handlePullPatchIntoWorkingTree() error { pull := func(stash bool) error { return gui.WithWaitingStatus(gui.Tr.RebasingStatus, func() error { commitIndex := gui.getPatchCommitIndex() - err := gui.GitCommand.PullPatchIntoIndex(gui.State.Commits, commitIndex, gui.GitCommand.PatchManager, stash) + err := gui.GitCommand.WithSpan("Move patch into index").MovePatchIntoIndex(gui.State.Commits, commitIndex, gui.GitCommand.PatchManager, stash) return gui.handleGenericMergeCommandResult(err) }) } @@ -160,7 +160,7 @@ func (gui *Gui) handlePullPatchIntoNewCommit() error { return gui.WithWaitingStatus(gui.Tr.RebasingStatus, func() error { commitIndex := gui.getPatchCommitIndex() - err := gui.GitCommand.PullPatchIntoNewCommit(gui.State.Commits, commitIndex, gui.GitCommand.PatchManager) + err := gui.GitCommand.WithSpan("Move patch into new commit").PullPatchIntoNewCommit(gui.State.Commits, commitIndex, gui.GitCommand.PatchManager) return gui.handleGenericMergeCommandResult(err) }) } @@ -170,7 +170,11 @@ func (gui *Gui) handleApplyPatch(reverse bool) error { return err } - if err := gui.GitCommand.PatchManager.ApplyPatches(reverse); err != nil { + span := "Apply patch" + if reverse { + span = "Apply patch in reverse" + } + if err := gui.GitCommand.WithSpan(span).PatchManager.ApplyPatches(reverse); err != nil { return gui.surfaceError(err) } return gui.refreshSidePanels(refreshOptions{mode: ASYNC}) diff --git a/pkg/gui/rebase_options_panel.go b/pkg/gui/rebase_options_panel.go index b1bcf1e7a..c49b4f38f 100644 --- a/pkg/gui/rebase_options_panel.go +++ b/pkg/gui/rebase_options_panel.go @@ -43,18 +43,20 @@ func (gui *Gui) genericMergeCommand(command string) error { return gui.createErrorPanel(gui.Tr.NotMergingOrRebasing) } + gitCommand := gui.GitCommand.WithSpan(fmt.Sprintf("Merge: %s", command)) + commandType := strings.Replace(status, "ing", "e", 1) // we should end up with a command like 'git merge --continue' // it's impossible for a rebase to require a commit so we'll use a subprocess only if it's a merge if status == commands.REBASE_MODE_MERGING && command != "abort" && gui.Config.GetUserConfig().Git.Merging.ManualCommit { - sub := gui.OSCommand.PrepareSubProcess("git", commandType, fmt.Sprintf("--%s", command)) + sub := gitCommand.OSCommand.PrepareSubProcess("git", commandType, fmt.Sprintf("--%s", command)) if sub != nil { return gui.runSubprocessWithSuspenseAndRefresh(sub) } return nil } - result := gui.GitCommand.GenericMergeOrRebaseAction(commandType, command) + result := gitCommand.GenericMergeOrRebaseAction(commandType, command) if err := gui.handleGenericMergeCommandResult(result); err != nil { return err } diff --git a/pkg/gui/staging_panel.go b/pkg/gui/staging_panel.go index af50d1c2a..612806f94 100644 --- a/pkg/gui/staging_panel.go +++ b/pkg/gui/staging_panel.go @@ -142,7 +142,7 @@ func (gui *Gui) applySelection(reverse bool, state *lBlPanelState) error { if !reverse || state.SecondaryFocused { applyFlags = append(applyFlags, "cached") } - err := gui.GitCommand.ApplyPatch(patch, applyFlags...) + err := gui.GitCommand.WithSpan("Apply patch").ApplyPatch(patch, applyFlags...) if err != nil { return gui.surfaceError(err) } diff --git a/pkg/gui/stash_panel.go b/pkg/gui/stash_panel.go index 8cf2e28f5..a87ae2681 100644 --- a/pkg/gui/stash_panel.go +++ b/pkg/gui/stash_panel.go @@ -106,7 +106,7 @@ func (gui *Gui) stashDo(method string) error { return gui.createErrorPanel(errorMessage) } - if err := gui.GitCommand.StashDo(stashEntry.Index, method); err != nil { + if err := gui.GitCommand.WithSpan("Stash").StashDo(stashEntry.Index, method); err != nil { return gui.surfaceError(err) } return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{STASH, FILES}}) diff --git a/pkg/gui/submodules_panel.go b/pkg/gui/submodules_panel.go index 5be2bd63f..a9d1d2eca 100644 --- a/pkg/gui/submodules_panel.go +++ b/pkg/gui/submodules_panel.go @@ -81,7 +81,7 @@ func (gui *Gui) removeSubmodule(submodule *models.SubmoduleConfig) error { title: gui.Tr.RemoveSubmodule, prompt: fmt.Sprintf(gui.Tr.RemoveSubmodulePrompt, submodule.Name), handleConfirm: func() error { - if err := gui.GitCommand.SubmoduleDelete(submodule); err != nil { + if err := gui.GitCommand.WithSpan("Remove submodule").SubmoduleDelete(submodule); err != nil { return gui.surfaceError(err) } @@ -107,17 +107,19 @@ func (gui *Gui) fileForSubmodule(submodule *models.SubmoduleConfig) *models.File } func (gui *Gui) resetSubmodule(submodule *models.SubmoduleConfig) error { + gitCommand := gui.GitCommand.WithSpan("Reset submodule") + file := gui.fileForSubmodule(submodule) if file != nil { - if err := gui.GitCommand.UnStageFile(file.Names(), file.Tracked); err != nil { + if err := gitCommand.UnStageFile(file.Names(), file.Tracked); err != nil { return gui.surfaceError(err) } } - if err := gui.GitCommand.SubmoduleStash(submodule); err != nil { + if err := gitCommand.SubmoduleStash(submodule); err != nil { return gui.surfaceError(err) } - if err := gui.GitCommand.SubmoduleReset(submodule); err != nil { + if err := gitCommand.SubmoduleReset(submodule); err != nil { return gui.surfaceError(err) } @@ -140,7 +142,7 @@ func (gui *Gui) handleAddSubmodule() error { initialContent: submoduleName, handleConfirm: func(submodulePath string) error { return gui.WithWaitingStatus(gui.Tr.LcAddingSubmoduleStatus, func() error { - err := gui.GitCommand.SubmoduleAdd(submoduleName, submodulePath, submoduleUrl) + err := gui.GitCommand.WithSpan("Add submodule").SubmoduleAdd(submoduleName, submodulePath, submoduleUrl) gui.handleCredentialsPopup(err) return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{SUBMODULES}}) @@ -160,7 +162,7 @@ func (gui *Gui) handleEditSubmoduleUrl(submodule *models.SubmoduleConfig) error initialContent: submodule.Url, handleConfirm: func(newUrl string) error { return gui.WithWaitingStatus(gui.Tr.LcUpdatingSubmoduleUrlStatus, func() error { - err := gui.GitCommand.SubmoduleUpdateUrl(submodule.Name, submodule.Path, newUrl) + err := gui.GitCommand.WithSpan("Update submodule URL").SubmoduleUpdateUrl(submodule.Name, submodule.Path, newUrl) gui.handleCredentialsPopup(err) return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{SUBMODULES}}) @@ -171,7 +173,7 @@ func (gui *Gui) handleEditSubmoduleUrl(submodule *models.SubmoduleConfig) error func (gui *Gui) handleSubmoduleInit(submodule *models.SubmoduleConfig) error { return gui.WithWaitingStatus(gui.Tr.LcInitializingSubmoduleStatus, func() error { - err := gui.GitCommand.SubmoduleInit(submodule.Path) + err := gui.GitCommand.WithSpan("Initialise submodule").SubmoduleInit(submodule.Path) gui.handleCredentialsPopup(err) return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{SUBMODULES}}) @@ -214,7 +216,7 @@ func (gui *Gui) handleBulkSubmoduleActionsMenu() error { displayStrings: []string{gui.Tr.LcBulkInitSubmodules, utils.ColoredString(gui.GitCommand.SubmoduleBulkInitCmdStr(), color.FgGreen)}, onPress: func() error { return gui.WithWaitingStatus(gui.Tr.LcRunningCommand, func() error { - if err := gui.OSCommand.RunCommand(gui.GitCommand.SubmoduleBulkInitCmdStr()); err != nil { + if err := gui.OSCommand.WithSpan("Bulk initialise submodules").RunCommand(gui.GitCommand.SubmoduleBulkInitCmdStr()); err != nil { return gui.surfaceError(err) } @@ -226,7 +228,7 @@ func (gui *Gui) handleBulkSubmoduleActionsMenu() error { displayStrings: []string{gui.Tr.LcBulkUpdateSubmodules, utils.ColoredString(gui.GitCommand.SubmoduleBulkUpdateCmdStr(), color.FgYellow)}, onPress: func() error { return gui.WithWaitingStatus(gui.Tr.LcRunningCommand, func() error { - if err := gui.OSCommand.RunCommand(gui.GitCommand.SubmoduleBulkUpdateCmdStr()); err != nil { + if err := gui.OSCommand.WithSpan("Bulk update submodules").RunCommand(gui.GitCommand.SubmoduleBulkUpdateCmdStr()); err != nil { return gui.surfaceError(err) } @@ -238,7 +240,7 @@ func (gui *Gui) handleBulkSubmoduleActionsMenu() error { displayStrings: []string{gui.Tr.LcSubmoduleStashAndReset, utils.ColoredString(fmt.Sprintf("git stash in each submodule && %s", gui.GitCommand.SubmoduleForceBulkUpdateCmdStr()), color.FgRed)}, onPress: func() error { return gui.WithWaitingStatus(gui.Tr.LcRunningCommand, func() error { - if err := gui.GitCommand.ResetSubmodules(gui.State.Submodules); err != nil { + if err := gui.GitCommand.WithSpan("Bulk stash and reset submodules").ResetSubmodules(gui.State.Submodules); err != nil { return gui.surfaceError(err) } @@ -250,7 +252,7 @@ func (gui *Gui) handleBulkSubmoduleActionsMenu() error { displayStrings: []string{gui.Tr.LcBulkDeinitSubmodules, utils.ColoredString(gui.GitCommand.SubmoduleBulkDeinitCmdStr(), color.FgRed)}, onPress: func() error { return gui.WithWaitingStatus(gui.Tr.LcRunningCommand, func() error { - if err := gui.OSCommand.RunCommand(gui.GitCommand.SubmoduleBulkDeinitCmdStr()); err != nil { + if err := gui.OSCommand.WithSpan("Bulk deinitialise submodules").RunCommand(gui.GitCommand.SubmoduleBulkDeinitCmdStr()); err != nil { return gui.surfaceError(err) } @@ -265,7 +267,7 @@ func (gui *Gui) handleBulkSubmoduleActionsMenu() error { func (gui *Gui) handleUpdateSubmodule(submodule *models.SubmoduleConfig) error { return gui.WithWaitingStatus(gui.Tr.LcUpdatingSubmoduleStatus, func() error { - err := gui.GitCommand.SubmoduleUpdate(submodule.Path) + err := gui.GitCommand.WithSpan("Update submodule").SubmoduleUpdate(submodule.Path) gui.handleCredentialsPopup(err) return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{SUBMODULES}})