diff --git a/pkg/commands/git.go b/pkg/commands/git.go index f72280d64..fb4ddab79 100644 --- a/pkg/commands/git.go +++ b/pkg/commands/git.go @@ -176,8 +176,8 @@ func stashEntryFromLine(line string, index int) *StashEntry { } // GetStashEntryDiff stash diff -func (c *GitCommand) GetStashEntryDiff(index int) (string, error) { - return c.OSCommand.RunCommandWithOutput("git stash show -p --color stash@{%d}", index) +func (c *GitCommand) ShowStashEntryCmdStr(index int) string { + return fmt.Sprintf("git stash show -p --color stash@{%d}", index) } // GetStatusFiles git status files @@ -538,7 +538,8 @@ func (c *GitCommand) PrepareCommitAmendSubProcess() *exec.Cmd { // Currently it limits the result to 100 commits, but when we get async stuff // working we can do lazy loading func (c *GitCommand) GetBranchGraph(branchName string) (string, error) { - return c.OSCommand.RunCommandWithOutput("git log --graph --color --abbrev-commit --decorate --date=relative --pretty=medium -100 %s", branchName) + cmdStr := c.GetBranchGraphCmdStr(branchName) + return c.OSCommand.RunCommandWithOutput(cmdStr) } func (c *GitCommand) GetUpstreamForBranch(branchName string) (string, error) { @@ -551,41 +552,12 @@ func (c *GitCommand) Ignore(filename string) error { return c.OSCommand.AppendLineToFile(".gitignore", filename) } -// Show shows the diff of a commit -func (c *GitCommand) Show(sha string) (string, error) { - show, err := c.OSCommand.RunCommandWithOutput("git show --color --no-renames %s", sha) - if err != nil { - return "", err - } +func (c *GitCommand) ShowCmdStr(sha string) string { + return fmt.Sprintf("git show --color --no-renames %s", sha) +} - // if this is a merge commit, we need to go a step further and get the diff between the two branches we merged - revList, err := c.OSCommand.RunCommandWithOutput("git rev-list -1 --merges %s^...%s", sha, sha) - if err != nil { - // turns out we get an error here when it's the first commit. We'll just return the original show - return show, nil - } - if len(revList) == 0 { - return show, nil - } - - // we want to pull out 1a6a69a and 3b51d7c from this: - // commit ccc771d8b13d5b0d4635db4463556366470fd4f6 - // Merge: 1a6a69a 3b51d7c - lines := utils.SplitLines(show) - if len(lines) < 2 { - return show, nil - } - - secondLineWords := strings.Split(lines[1], " ") - if len(secondLineWords) < 3 { - return show, nil - } - - mergeDiff, err := c.OSCommand.RunCommandWithOutput("git diff --color %s...%s", secondLineWords[1], secondLineWords[2]) - if err != nil { - return "", err - } - return show + mergeDiff, nil +func (c *GitCommand) GetBranchGraphCmdStr(branchName string) string { + return fmt.Sprintf("git log --graph --color --abbrev-commit --decorate --date=relative --pretty=medium %s", branchName) } // GetRemoteURL returns current repo remote url @@ -606,6 +578,12 @@ func (c *GitCommand) CheckRemoteBranchExists(branch *Branch) bool { // Diff returns the diff of a file func (c *GitCommand) Diff(file *File, plain bool, cached bool) string { + // for now we assume an error means the file was deleted + s, _ := c.OSCommand.RunCommandWithOutput(c.DiffCmdStr(file, plain, cached)) + return s +} + +func (c *GitCommand) DiffCmdStr(file *File, plain bool, cached bool) string { cachedArg := "" trackedArg := "--" colorArg := "--color" @@ -621,9 +599,7 @@ func (c *GitCommand) Diff(file *File, plain bool, cached bool) string { colorArg = "" } - // for now we assume an error means the file was deleted - s, _ := c.OSCommand.RunCommandWithOutput("git diff %s %s %s %s", colorArg, cachedArg, trackedArg, fileName) - return s + return fmt.Sprintf("git diff %s %s %s %s", colorArg, cachedArg, trackedArg, fileName) } func (c *GitCommand) ApplyPatch(patch string, flags ...string) error { @@ -908,11 +884,17 @@ func (c *GitCommand) GetCommitFiles(commitSha string, patchManager *PatchManager // ShowCommitFile get the diff of specified commit file func (c *GitCommand) ShowCommitFile(commitSha, fileName string, plain bool) (string, error) { + cmdStr := c.ShowCommitFileCmdStr(commitSha, fileName, plain) + return c.OSCommand.RunCommandWithOutput(cmdStr) +} + +func (c *GitCommand) ShowCommitFileCmdStr(commitSha, fileName string, plain bool) string { colorArg := "--color" if plain { colorArg = "" } - return c.OSCommand.RunCommandWithOutput("git show --no-renames %s %s -- %s", colorArg, commitSha, fileName) + + return fmt.Sprintf("git show --no-renames %s %s -- %s", colorArg, commitSha, fileName) } // CheckoutFile checks out the file for the given commit @@ -1098,10 +1080,6 @@ func (c *GitCommand) CreateLightweightTag(tagName string, commitSha string) erro return c.OSCommand.RunCommand("git tag %s %s", tagName, commitSha) } -func (c *GitCommand) ShowTag(tagName string) (string, error) { - return c.OSCommand.RunCommandWithOutput("git tag -n99 %s", tagName) -} - func (c *GitCommand) DeleteTag(tagName string) error { return c.OSCommand.RunCommand("git tag -d %s", tagName) } diff --git a/pkg/commands/git_test.go b/pkg/commands/git_test.go index 0429165de..ceaadc576 100644 --- a/pkg/commands/git_test.go +++ b/pkg/commands/git_test.go @@ -318,21 +318,6 @@ func TestGitCommandGetStashEntries(t *testing.T) { } } -// TestGitCommandGetStashEntryDiff is a function. -func TestGitCommandGetStashEntryDiff(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", "show", "-p", "--color", "stash@{1}"}, args) - - return exec.Command("echo") - } - - _, err := gitCmd.GetStashEntryDiff(1) - - assert.NoError(t, err) -} - // TestGitCommandGetStatusFiles is a function. func TestGitCommandGetStatusFiles(t *testing.T) { type scenario struct { @@ -1411,66 +1396,6 @@ func TestGitCommandDiscardAllFileChanges(t *testing.T) { } } -// TestGitCommandShow is a function. -func TestGitCommandShow(t *testing.T) { - type scenario struct { - testName string - arg string - command func(string, ...string) *exec.Cmd - test func(string, error) - } - - scenarios := []scenario{ - { - "regular commit", - "456abcde", - test.CreateMockCommand(t, []*test.CommandSwapper{ - { - Expect: "git show --color --no-renames 456abcde", - Replace: "echo \"commit ccc771d8b13d5b0d4635db4463556366470fd4f6\nblah\"", - }, - { - Expect: "git rev-list -1 --merges 456abcde^...456abcde", - Replace: "echo", - }, - }), - func(result string, err error) { - assert.NoError(t, err) - assert.Equal(t, "commit ccc771d8b13d5b0d4635db4463556366470fd4f6\nblah\n", result) - }, - }, - { - "merge commit", - "456abcde", - test.CreateMockCommand(t, []*test.CommandSwapper{ - { - Expect: "git show --color --no-renames 456abcde", - Replace: "echo \"commit ccc771d8b13d5b0d4635db4463556366470fd4f6\nMerge: 1a6a69a 3b51d7c\"", - }, - { - Expect: "git rev-list -1 --merges 456abcde^...456abcde", - Replace: "echo aa30e006433628ba9281652952b34d8aacda9c01", - }, - { - Expect: "git diff --color 1a6a69a...3b51d7c", - Replace: "echo blah", - }, - }), - func(result string, err error) { - assert.NoError(t, err) - assert.Equal(t, "commit ccc771d8b13d5b0d4635db4463556366470fd4f6\nMerge: 1a6a69a 3b51d7c\nblah\n", result) - }, - }, - } - - gitCmd := NewDummyGitCommand() - - for _, s := range scenarios { - gitCmd.OSCommand.command = s.command - s.test(gitCmd.Show(s.arg)) - } -} - // TestGitCommandCheckout is a function. func TestGitCommandCheckout(t *testing.T) { type scenario struct { @@ -1523,7 +1448,7 @@ 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", "--abbrev-commit", "--decorate", "--date=relative", "--pretty=medium", "-100", "test"}, args) + assert.EqualValues(t, []string{"log", "--graph", "--color", "--abbrev-commit", "--decorate", "--date=relative", "--pretty=medium", "test"}, args) return exec.Command("echo") } diff --git a/pkg/gui/branches_panel.go b/pkg/gui/branches_panel.go index 1bf408026..ff77c9cb4 100644 --- a/pkg/gui/branches_panel.go +++ b/pkg/gui/branches_panel.go @@ -4,7 +4,6 @@ import ( "fmt" "strings" - "github.com/fatih/color" "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/commands" "github.com/jesseduffield/lazygit/pkg/utils" @@ -37,40 +36,45 @@ func (gui *Gui) handleBranchSelect(g *gocui.Gui, v *gocui.View) error { // This really shouldn't happen: there should always be a master branch if len(gui.State.Branches) == 0 { - return gui.renderString(g, "main", gui.Tr.SLocalize("NoBranchesThisRepo")) + return gui.newStringTask("main", gui.Tr.SLocalize("NoBranchesThisRepo")) } branch := gui.getSelectedBranch() if err := gui.focusPoint(0, gui.State.Panels.Branches.SelectedLine, len(gui.State.Branches), v); err != nil { return err } - go func() { - _ = gui.RenderSelectedBranchUpstreamDifferences() - }() - go func() { - upstream, _ := gui.GitCommand.GetUpstreamForBranch(branch.Name) - if strings.Contains(upstream, "no upstream configured for branch") || strings.Contains(upstream, "unknown revision or path not in the working tree") { - upstream = gui.Tr.SLocalize("notTrackingRemote") - } - graph, err := gui.GitCommand.GetBranchGraph(branch.Name) - if err != nil && strings.HasPrefix(graph, "fatal: ambiguous argument") { - graph = gui.Tr.SLocalize("NoTrackingThisBranch") - } - _ = gui.renderString(g, "main", fmt.Sprintf("%s → %s\n\n%s", utils.ColoredString(branch.Name, color.FgGreen), utils.ColoredString(upstream, color.FgRed), graph)) - }() + if err := gui.RenderSelectedBranchUpstreamDifferences(); err != nil { + return err + } + + cmd := gui.OSCommand.ExecutableFromString( + gui.GitCommand.GetBranchGraphCmdStr(branch.Name), + ) + if err := gui.newCmdTask("main", cmd); err != nil { + gui.Log.Error(err) + } return nil } func (gui *Gui) RenderSelectedBranchUpstreamDifferences() error { - // here we tell the selected branch that it is selected. - // this is necessary for showing stats on a branch that is selected, because - // the displaystring function doesn't have access to gui state to tell if it's selected - for i, branch := range gui.State.Branches { - branch.Selected = i == gui.State.Panels.Branches.SelectedLine - } + return gui.newTask("branches", func(stop chan struct{}) error { + // here we tell the selected branch that it is selected. + // this is necessary for showing stats on a branch that is selected, because + // the displaystring function doesn't have access to gui state to tell if it's selected + for i, branch := range gui.State.Branches { + branch.Selected = i == gui.State.Panels.Branches.SelectedLine + } - branch := gui.getSelectedBranch() - branch.Pushables, branch.Pullables = gui.GitCommand.GetBranchUpstreamDifferenceCount(branch.Name) - return gui.renderListPanel(gui.getBranchesView(), gui.State.Branches) + branch := gui.getSelectedBranch() + branch.Pushables, branch.Pullables = gui.GitCommand.GetBranchUpstreamDifferenceCount(branch.Name) + + select { + case <-stop: + return nil + default: + } + + return gui.renderListPanel(gui.getBranchesView(), gui.State.Branches) + }) } // gui.refreshStatus is called at the end of this because that's when we can diff --git a/pkg/gui/commit_files_panel.go b/pkg/gui/commit_files_panel.go index 1dee4a3cd..42afc6c94 100644 --- a/pkg/gui/commit_files_panel.go +++ b/pkg/gui/commit_files_panel.go @@ -43,11 +43,15 @@ func (gui *Gui) handleCommitFileSelect(g *gocui.Gui, v *gocui.View) error { if err := gui.focusPoint(0, gui.State.Panels.CommitFiles.SelectedLine, len(gui.State.CommitFiles), v); err != nil { return err } - commitText, err := gui.GitCommand.ShowCommitFile(commitFile.Sha, commitFile.Name, false) - if err != nil { - return err + + cmd := gui.OSCommand.ExecutableFromString( + gui.GitCommand.ShowCommitFileCmdStr(commitFile.Sha, commitFile.Name, false), + ) + if err := gui.newCmdTask("main", cmd); err != nil { + gui.Log.Error(err) } - return gui.renderString(g, "main", commitText) + + return nil } func (gui *Gui) handleSwitchToCommitsPanel(g *gocui.Gui, v *gocui.View) error { diff --git a/pkg/gui/commits_panel.go b/pkg/gui/commits_panel.go index 32672a40b..abc6b47ec 100644 --- a/pkg/gui/commits_panel.go +++ b/pkg/gui/commits_panel.go @@ -53,7 +53,7 @@ func (gui *Gui) handleCommitSelect(g *gocui.Gui, v *gocui.View) error { commit := gui.getSelectedCommit(g) if commit == nil { - return gui.renderString(g, "main", gui.Tr.SLocalize("NoCommitsThisBranch")) + return gui.newStringTask("main", gui.Tr.SLocalize("NoCommitsThisBranch")) } if err := gui.focusPoint(0, gui.State.Panels.Commits.SelectedLine, len(gui.State.Commits), v); err != nil { @@ -65,11 +65,14 @@ func (gui *Gui) handleCommitSelect(g *gocui.Gui, v *gocui.View) error { return nil } - commitText, err := gui.GitCommand.Show(commit.Sha) - if err != nil { - return err + cmd := gui.OSCommand.ExecutableFromString( + gui.GitCommand.ShowCmdStr(commit.Sha), + ) + if err := gui.newCmdTask("main", cmd); err != nil { + gui.Log.Error(err) } - return gui.renderString(g, "main", commitText) + + return nil } func (gui *Gui) refreshCommits(g *gocui.Gui) error { @@ -463,7 +466,7 @@ func (gui *Gui) handleToggleDiffCommit(g *gocui.Gui, v *gocui.View) error { // get selected commit commit := gui.getSelectedCommit(g) if commit == nil { - return gui.renderString(g, "main", gui.Tr.SLocalize("NoCommitsThisBranch")) + return gui.newStringTask("main", gui.Tr.SLocalize("NoCommitsThisBranch")) } // if already selected commit delete @@ -486,7 +489,7 @@ func (gui *Gui) handleToggleDiffCommit(g *gocui.Gui, v *gocui.View) error { return gui.createErrorPanel(gui.g, err.Error()) } - return gui.renderString(g, "main", commitText) + return gui.newStringTask("main", commitText) } return nil diff --git a/pkg/gui/files_panel.go b/pkg/gui/files_panel.go index 38622efa1..47e7bae1f 100644 --- a/pkg/gui/files_panel.go +++ b/pkg/gui/files_panel.go @@ -32,7 +32,7 @@ func (gui *Gui) selectFile(alreadySelected bool) error { if err != gui.Errors.ErrNoFiles { return err } - return gui.renderString(gui.g, "main", gui.Tr.SLocalize("NoChangedFiles")) + return gui.newStringTask("main", gui.Tr.SLocalize("NoChangedFiles")) } if err := gui.focusPoint(0, gui.State.Panels.Files.SelectedLine, len(gui.State.Files), gui.getFilesView()); err != nil { @@ -45,37 +45,40 @@ func (gui *Gui) selectFile(alreadySelected bool) error { return gui.refreshMergePanel() } - content := gui.GitCommand.Diff(file, false, false) - contentCached := gui.GitCommand.Diff(file, false, true) - leftContent := content + if !alreadySelected { + if err := gui.resetOrigin(gui.getMainView()); err != nil { + return err + } + if err := gui.resetOrigin(gui.getSecondaryView()); err != nil { + return err + } + } + if file.HasStagedChanges && file.HasUnstagedChanges { gui.State.SplitMainPanel = true gui.getMainView().Title = gui.Tr.SLocalize("UnstagedChanges") gui.getSecondaryView().Title = gui.Tr.SLocalize("StagedChanges") + cmdStr := gui.GitCommand.DiffCmdStr(file, false, true) + cmd := gui.OSCommand.ExecutableFromString(cmdStr) + if err := gui.newCmdTask("secondary", cmd); err != nil { + return err + } } else { gui.State.SplitMainPanel = false if file.HasUnstagedChanges { - leftContent = content gui.getMainView().Title = gui.Tr.SLocalize("UnstagedChanges") } else { - leftContent = contentCached gui.getMainView().Title = gui.Tr.SLocalize("StagedChanges") } } - if alreadySelected { - gui.g.Update(func(*gocui.Gui) error { - if err := gui.setViewContent(gui.g, gui.getSecondaryView(), contentCached); err != nil { - return err - } - return gui.setViewContent(gui.g, gui.getMainView(), leftContent) - }) - return nil - } - if err := gui.renderString(gui.g, "secondary", contentCached); err != nil { + cmdStr := gui.GitCommand.DiffCmdStr(file, false, !file.HasUnstagedChanges && file.HasStagedChanges) + cmd := gui.OSCommand.ExecutableFromString(cmdStr) + if err := gui.newCmdTask("main", cmd); err != nil { return err } - return gui.renderString(gui.g, "main", leftContent) + + return nil } func (gui *Gui) refreshFiles() error { @@ -369,15 +372,15 @@ func (gui *Gui) catSelectedFile(g *gocui.Gui) (string, error) { if err != gui.Errors.ErrNoFiles { return "", err } - return "", gui.renderString(g, "main", gui.Tr.SLocalize("NoFilesDisplay")) + return "", gui.newStringTask("main", gui.Tr.SLocalize("NoFilesDisplay")) } if item.Type != "file" { - return "", gui.renderString(g, "main", gui.Tr.SLocalize("NotAFile")) + return "", gui.newStringTask("main", gui.Tr.SLocalize("NotAFile")) } cat, err := gui.GitCommand.CatFile(item.Name) if err != nil { gui.Log.Error(err) - return "", gui.renderString(g, "main", err.Error()) + return "", gui.newStringTask("main", err.Error()) } return cat, nil } diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index 2bdb66e0f..d748b4d72 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -25,6 +25,7 @@ import ( "github.com/jesseduffield/lazygit/pkg/commands" "github.com/jesseduffield/lazygit/pkg/config" "github.com/jesseduffield/lazygit/pkg/i18n" + "github.com/jesseduffield/lazygit/pkg/tasks" "github.com/jesseduffield/lazygit/pkg/theme" "github.com/jesseduffield/lazygit/pkg/updates" "github.com/jesseduffield/lazygit/pkg/utils" @@ -68,20 +69,21 @@ type Teml i18n.Teml // Gui wraps the gocui Gui object which handles rendering and events type Gui struct { - g *gocui.Gui - Log *logrus.Entry - GitCommand *commands.GitCommand - OSCommand *commands.OSCommand - SubProcess *exec.Cmd - State guiState - Config config.AppConfigurer - Tr *i18n.Localizer - Errors SentinelErrors - Updater *updates.Updater - statusManager *statusManager - credentials credentials - waitForIntro sync.WaitGroup - fileWatcher *fileWatcher + g *gocui.Gui + Log *logrus.Entry + GitCommand *commands.GitCommand + OSCommand *commands.OSCommand + SubProcess *exec.Cmd + State guiState + Config config.AppConfigurer + Tr *i18n.Localizer + Errors SentinelErrors + Updater *updates.Updater + statusManager *statusManager + credentials credentials + waitForIntro sync.WaitGroup + fileWatcher *fileWatcher + viewBufferManagerMap map[string]*tasks.ViewBufferManager } // for now the staging panel state, unlike the other panel states, is going to be @@ -229,14 +231,15 @@ func NewGui(log *logrus.Entry, gitCommand *commands.GitCommand, oSCommand *comma } gui := &Gui{ - Log: log, - GitCommand: gitCommand, - OSCommand: oSCommand, - State: initialState, - Config: config, - Tr: tr, - Updater: updater, - statusManager: &statusManager{}, + Log: log, + GitCommand: gitCommand, + OSCommand: oSCommand, + State: initialState, + Config: config, + Tr: tr, + Updater: updater, + statusManager: &statusManager{}, + viewBufferManagerMap: map[string]*tasks.ViewBufferManager{}, } gui.watchFilesForChanges() @@ -261,8 +264,14 @@ func (gui *Gui) scrollDownView(viewName string) error { _, sy := mainView.Size() y += sy } + scrollHeight := gui.Config.GetUserConfig().GetInt("gui.scrollHeight") if y < len(mainView.BufferLines()) { - return mainView.SetOrigin(ox, oy+gui.Config.GetUserConfig().GetInt("gui.scrollHeight")) + if err := mainView.SetOrigin(ox, oy+scrollHeight); err != nil { + return err + } + } + if manager, ok := gui.viewBufferManagerMap[viewName]; ok { + manager.ReadLines(scrollHeight) } return nil } @@ -465,7 +474,23 @@ func (gui *Gui) layout(g *gocui.Gui) error { secondary = "main" } - v, err := g.SetView(main, leftSideWidth+panelSpacing, 0, panelSplitX, height-2, gocui.LEFT) + // reading more lines into main view buffers upon resize + mainHeight := height - 2 + prevMainView, err := gui.g.View("main") + if err == nil { + _, prevMainHeight := prevMainView.Size() + heightDiff := mainHeight - 1 - prevMainHeight + if heightDiff > 0 { + if manager, ok := gui.viewBufferManagerMap["main"]; ok { + manager.ReadLines(heightDiff) + } + if manager, ok := gui.viewBufferManagerMap["secondary"]; ok { + manager.ReadLines(heightDiff) + } + } + } + + v, err := g.SetView(main, leftSideWidth+panelSpacing, 0, panelSplitX, mainHeight, gocui.LEFT) if err != nil { if err.Error() != "unknown view" { return err @@ -479,7 +504,7 @@ func (gui *Gui) layout(g *gocui.Gui) error { if !gui.State.SplitMainPanel { hiddenViewOffset = 9999 } - secondaryView, err := g.SetView(secondary, panelSplitX+1+hiddenViewOffset, hiddenViewOffset, width-1+hiddenViewOffset, height-2+hiddenViewOffset, gocui.LEFT) + secondaryView, err := g.SetView(secondary, panelSplitX+1+hiddenViewOffset, hiddenViewOffset, width-1+hiddenViewOffset, mainHeight+hiddenViewOffset, gocui.LEFT) if err != nil { if err.Error() != "unknown view" { return err @@ -843,6 +868,11 @@ func (gui *Gui) Run() error { func (gui *Gui) RunWithSubprocesses() error { for { if err := gui.Run(); err != nil { + for _, manager := range gui.viewBufferManagerMap { + manager.Close() + } + gui.viewBufferManagerMap = map[string]*tasks.ViewBufferManager{} + if err == gocui.ErrQuit { if !gui.State.RetainOriginalDir { if err := gui.recordCurrentDirectory(); err != nil { diff --git a/pkg/gui/merge_panel.go b/pkg/gui/merge_panel.go index 25238ce4f..4b86cf6c0 100644 --- a/pkg/gui/merge_panel.go +++ b/pkg/gui/merge_panel.go @@ -212,7 +212,7 @@ func (gui *Gui) refreshMergePanel() error { if err != nil { return err } - if err := gui.renderString(gui.g, "main", content); err != nil { + if err := gui.newStringTask("main", content); err != nil { return err } if err := gui.scrollToConflict(gui.g); err != nil { diff --git a/pkg/gui/reflog_panel.go b/pkg/gui/reflog_panel.go index 33577c073..e766070fb 100644 --- a/pkg/gui/reflog_panel.go +++ b/pkg/gui/reflog_panel.go @@ -31,17 +31,20 @@ func (gui *Gui) handleReflogCommitSelect(g *gocui.Gui, v *gocui.View) error { commit := gui.getSelectedReflogCommit() if commit == nil { - return gui.renderString(g, "main", "No reflog history") + return gui.newStringTask("main", "No reflog history") } if err := gui.focusPoint(0, gui.State.Panels.ReflogCommits.SelectedLine, len(gui.State.ReflogCommits), v); err != nil { return err } - commitText, err := gui.GitCommand.Show(commit.Sha) - if err != nil { - return err + cmd := gui.OSCommand.ExecutableFromString( + gui.GitCommand.ShowCmdStr(commit.Sha), + ) + if err := gui.newCmdTask("main", cmd); err != nil { + gui.Log.Error(err) } - return gui.renderString(g, "main", commitText) + + return nil } func (gui *Gui) refreshReflogCommits() error { diff --git a/pkg/gui/remote_branches_panel.go b/pkg/gui/remote_branches_panel.go index 4b8658df7..fbe299fd9 100644 --- a/pkg/gui/remote_branches_panel.go +++ b/pkg/gui/remote_branches_panel.go @@ -2,12 +2,9 @@ package gui import ( "fmt" - "strings" - "github.com/fatih/color" "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/commands" - "github.com/jesseduffield/lazygit/pkg/utils" ) // list panel functions @@ -37,7 +34,7 @@ func (gui *Gui) handleRemoteBranchSelect(g *gocui.Gui, v *gocui.View) error { remote := gui.getSelectedRemote() remoteBranch := gui.getSelectedRemoteBranch() if remoteBranch == nil { - return gui.renderString(g, "main", "No branches for this remote") + return gui.newStringTask("main", "No branches for this remote") } gui.focusPoint(0, gui.State.Panels.Menu.SelectedLine, gui.State.MenuItemCount, v) @@ -45,13 +42,14 @@ func (gui *Gui) handleRemoteBranchSelect(g *gocui.Gui, v *gocui.View) error { return err } - go func() { - graph, err := gui.GitCommand.GetBranchGraph(fmt.Sprintf("%s/%s", remote.Name, remoteBranch.Name)) - if err != nil && strings.HasPrefix(graph, "fatal: ambiguous argument") { - graph = gui.Tr.SLocalize("NoTrackingThisBranch") - } - _ = gui.renderString(g, "main", fmt.Sprintf("%s/%s\n\n%s", utils.ColoredString(remote.Name, color.FgRed), utils.ColoredString(remoteBranch.Name, color.FgGreen), graph)) - }() + branchName := fmt.Sprintf("%s/%s", remote.Name, remoteBranch.Name) + + cmd := gui.OSCommand.ExecutableFromString( + gui.GitCommand.GetBranchGraphCmdStr(branchName), + ) + if err := gui.newCmdTask("main", cmd); err != nil { + gui.Log.Error(err) + } return nil } diff --git a/pkg/gui/remotes_panel.go b/pkg/gui/remotes_panel.go index a646462d9..ca3e3aae1 100644 --- a/pkg/gui/remotes_panel.go +++ b/pkg/gui/remotes_panel.go @@ -36,13 +36,13 @@ func (gui *Gui) handleRemoteSelect(g *gocui.Gui, v *gocui.View) error { remote := gui.getSelectedRemote() if remote == nil { - return gui.renderString(g, "main", "No remotes") + return gui.newStringTask("main", "No remotes") } if err := gui.focusPoint(0, gui.State.Panels.Remotes.SelectedLine, len(gui.State.Remotes), v); err != nil { return err } - return gui.renderString(g, "main", fmt.Sprintf("%s\nUrls:\n%s", utils.ColoredString(remote.Name, color.FgGreen), strings.Join(remote.Urls, "\n"))) + return gui.newStringTask("main", fmt.Sprintf("%s\nUrls:\n%s", utils.ColoredString(remote.Name, color.FgGreen), strings.Join(remote.Urls, "\n"))) } func (gui *Gui) refreshRemotes() error { diff --git a/pkg/gui/stash_panel.go b/pkg/gui/stash_panel.go index 8ccdc9880..23ecaba60 100644 --- a/pkg/gui/stash_panel.go +++ b/pkg/gui/stash_panel.go @@ -34,16 +34,19 @@ func (gui *Gui) handleStashEntrySelect(g *gocui.Gui, v *gocui.View) error { stashEntry := gui.getSelectedStashEntry(v) if stashEntry == nil { - return gui.renderString(g, "main", gui.Tr.SLocalize("NoStashEntries")) + return gui.newStringTask("main", gui.Tr.SLocalize("NoStashEntries")) } if err := gui.focusPoint(0, gui.State.Panels.Stash.SelectedLine, len(gui.State.StashEntries), v); err != nil { return err } - go func() { - // doing this asynchronously cos it can take time - diff, _ := gui.GitCommand.GetStashEntryDiff(stashEntry.Index) - _ = gui.renderString(g, "main", diff) - }() + + cmd := gui.OSCommand.ExecutableFromString( + gui.GitCommand.ShowStashEntryCmdStr(stashEntry.Index), + ) + if err := gui.newCmdTask("main", cmd); err != nil { + gui.Log.Error(err) + } + return nil } diff --git a/pkg/gui/status_panel.go b/pkg/gui/status_panel.go index c351f24e8..8eb6a92f9 100644 --- a/pkg/gui/status_panel.go +++ b/pkg/gui/status_panel.go @@ -111,7 +111,7 @@ func (gui *Gui) handleStatusSelect(g *gocui.Gui, v *gocui.View) error { magenta.Sprint("Become a sponsor (github is matching all donations for 12 months): https://github.com/sponsors/jesseduffield"), // caffeine ain't free }, "\n\n") - return gui.renderString(g, "main", dashboardString) + return gui.newStringTask("main", dashboardString) } func (gui *Gui) handleOpenConfig(g *gocui.Gui, v *gocui.View) error { diff --git a/pkg/gui/tags_panel.go b/pkg/gui/tags_panel.go index 62777e237..8b74a3527 100644 --- a/pkg/gui/tags_panel.go +++ b/pkg/gui/tags_panel.go @@ -1,8 +1,6 @@ package gui import ( - "fmt" - "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/commands" ) @@ -33,25 +31,18 @@ func (gui *Gui) handleTagSelect(g *gocui.Gui, v *gocui.View) error { tag := gui.getSelectedTag() if tag == nil { - return gui.renderString(g, "main", "No tags") + return gui.newStringTask("main", "No tags") } if err := gui.focusPoint(0, gui.State.Panels.Tags.SelectedLine, len(gui.State.Tags), v); err != nil { return err } - go func() { - show, err := gui.GitCommand.ShowTag(tag.Name) - if err != nil { - show = "" - } - - graph, err := gui.GitCommand.GetBranchGraph(tag.Name) - if err != nil { - graph = "No graph for tag " + tag.Name - } - - _ = gui.renderString(g, "main", fmt.Sprintf("%s\n%s", show, graph)) - }() + cmd := gui.OSCommand.ExecutableFromString( + gui.GitCommand.GetBranchGraphCmdStr(tag.Name), + ) + if err := gui.newCmdTask("main", cmd); err != nil { + gui.Log.Error(err) + } return nil } diff --git a/pkg/gui/tasks_adapter.go b/pkg/gui/tasks_adapter.go new file mode 100644 index 000000000..22ebb9da9 --- /dev/null +++ b/pkg/gui/tasks_adapter.go @@ -0,0 +1,78 @@ +package gui + +import ( + "os/exec" + + "github.com/jesseduffield/gocui" + "github.com/jesseduffield/lazygit/pkg/tasks" +) + +func (gui *Gui) newCmdTask(viewName string, cmd *exec.Cmd) error { + view, err := gui.g.View(viewName) + if err != nil { + return nil // swallowing for now + } + + _, height := view.Size() + _, oy := view.Origin() + + manager := gui.getManager(view) + + if err := manager.NewTask(manager.NewCmdTask(cmd, height+oy+10)); err != nil { + return err + } + + return nil +} + +func (gui *Gui) newTask(viewName string, f func(chan struct{}) error) error { + view, err := gui.g.View(viewName) + if err != nil { + return nil // swallowing for now + } + + manager := gui.getManager(view) + + if err := manager.NewTask(f); err != nil { + return err + } + + return nil +} + +func (gui *Gui) newStringTask(viewName string, str string) error { + view, err := gui.g.View(viewName) + if err != nil { + return nil // swallowing for now + } + + manager := gui.getManager(view) + + f := func(stop chan struct{}) error { + return gui.renderString(gui.g, viewName, str) + } + + if err := manager.NewTask(f); err != nil { + return err + } + + return nil +} + +func (gui *Gui) getManager(view *gocui.View) *tasks.ViewBufferManager { + manager, ok := gui.viewBufferManagerMap[view.Name()] + if !ok { + manager = tasks.NewViewBufferManager( + gui.Log, + view, + func() { + view.Clear() + }, + func() { + gui.g.Update(func(*gocui.Gui) error { return nil }) + }) + gui.viewBufferManagerMap[view.Name()] = manager + } + + return manager +} diff --git a/pkg/tasks/tasks.go b/pkg/tasks/tasks.go new file mode 100644 index 000000000..2468ac958 --- /dev/null +++ b/pkg/tasks/tasks.go @@ -0,0 +1,226 @@ +package tasks + +import ( + "bufio" + "fmt" + "io" + "os/exec" + "sync" + "time" + + "github.com/sirupsen/logrus" +) + +type Task struct { + stop chan struct{} + stopped bool + stopMutex sync.Mutex + notifyStopped chan struct{} + Log *logrus.Entry + f func(chan struct{}) error +} + +type ViewBufferManager struct { + writer io.Writer + waitingTask *Task + currentTask *Task + waitingMutex sync.Mutex + taskIDMutex sync.Mutex + Log *logrus.Entry + newTaskId int + readLines chan int + + // beforeStart is the function that is called before starting a new task + beforeStart func() + refreshView func() +} + +func NewViewBufferManager(log *logrus.Entry, writer io.Writer, beforeStart func(), refreshView func()) *ViewBufferManager { + return &ViewBufferManager{Log: log, writer: writer, beforeStart: beforeStart, refreshView: refreshView, readLines: make(chan int, 1024)} +} + +func (m *ViewBufferManager) ReadLines(n int) { + go func() { + m.readLines <- n + }() +} + +func (m *ViewBufferManager) NewCmdTask(cmd *exec.Cmd, linesToRead int) func(chan struct{}) error { + return func(stop chan struct{}) error { + r, err := cmd.StdoutPipe() + if err != nil { + return err + } + cmd.Stderr = cmd.Stdout + + if err := cmd.Start(); err != nil { + return err + } + + go func() { + <-stop + if cmd.ProcessState.ExitCode() == -1 { + if err := kill(cmd); err != nil { + m.Log.Warn(err) + } + } + }() + + // not sure if it's the right move to redefine this or not + m.readLines = make(chan int, 1024) + + done := make(chan struct{}) + + go func() { + scanner := bufio.NewScanner(r) + scanner.Split(bufio.ScanLines) + + loaded := false + + go func() { + select { + case <-time.Tick(time.Millisecond * 100): + if !loaded { + m.beforeStart() + m.writer.Write([]byte("loading...")) + m.refreshView() + } + case <-stop: + return + } + }() + + outer: + for { + select { + case linesToRead := <-m.readLines: + for i := 0; i < linesToRead; i++ { + ok := scanner.Scan() + if !loaded { + m.beforeStart() + loaded = true + } + + select { + case <-stop: + break outer + default: + } + if !ok { + m.refreshView() + break outer + } + m.writer.Write(append(scanner.Bytes(), []byte("\n")...)) + } + m.refreshView() + case <-stop: + break outer + } + } + + if err := cmd.Wait(); err != nil { + m.Log.Warn(err) + } + + close(done) + }() + + m.readLines <- linesToRead + + <-done + + return nil + } +} + +// Close closes the task manager, killing whatever task may currently be running +func (t *ViewBufferManager) Close() { + if t.currentTask == nil { + return + } + + c := make(chan struct{}, 1) + + go func() { + t.currentTask.Stop() + c <- struct{}{} + }() + + select { + case <-c: + return + case <-time.After(3 * time.Second): + fmt.Println("cannot kill child process") + } +} + +// different kinds of tasks: +// 1) command based, where the manager can be asked to read more lines, but the command can be killed +// 2) string based, where the manager can also be asked to read more lines + +func (m *ViewBufferManager) NewTask(f func(stop chan struct{}) error) error { + go func() { + m.taskIDMutex.Lock() + m.newTaskId++ + taskID := m.newTaskId + m.Log.Infof("starting task %d", taskID) + m.taskIDMutex.Unlock() + + m.waitingMutex.Lock() + defer m.waitingMutex.Unlock() + if taskID < m.newTaskId { + return + } + + stop := make(chan struct{}) + notifyStopped := make(chan struct{}) + + if m.currentTask != nil { + m.Log.Info("asking task to stop") + m.currentTask.Stop() + m.Log.Info("task stopped") + } + + m.currentTask = &Task{ + stop: stop, + notifyStopped: notifyStopped, + Log: m.Log, + f: f, + } + + go func() { + if err := f(stop); err != nil { + m.Log.Error(err) // might need an onError callback + } + + m.Log.Infof("returning from task %d", taskID) + close(notifyStopped) + }() + }() + + return nil +} + +func (t *Task) Stop() { + t.stopMutex.Lock() + defer t.stopMutex.Unlock() + if t.stopped { + return + } + close(t.stop) + t.Log.Info("closed stop channel, waiting for notifyStopped message") + <-t.notifyStopped + t.Log.Info("received notifystopped message") + t.stopped = true + return +} + +// kill kills a process +func kill(cmd *exec.Cmd) error { + if cmd.Process == nil { + // somebody got to it before we were able to, poor bastard + return nil + } + + return cmd.Process.Kill() +}