diff --git a/pkg/app/app.go b/pkg/app/app.go index 65acd2e35..74152b08b 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -52,6 +52,11 @@ func newLogger(config config.AppConfigurer) *logrus.Entry { } else { log = newProductionLogger(config) } + + // highly recommended: tail -f development.log | humanlog + // https://github.com/aybabtme/humanlog + log.Formatter = &logrus.JSONFormatter{} + if config.GetUserConfig().GetString("reporting") == "on" { // this isn't really a secret token: it only has permission to push new rollbar items hook := rollrus.NewHook("23432119147a4367abf7c0de2aa99a2d", environment) diff --git a/pkg/commands/branch.go b/pkg/commands/branch.go index 19553a26b..49655c41a 100644 --- a/pkg/commands/branch.go +++ b/pkg/commands/branch.go @@ -1,6 +1,7 @@ package commands import ( + "fmt" "strings" "github.com/fatih/color" @@ -10,13 +11,21 @@ import ( // Branch : A git branch // duplicating this for now type Branch struct { - Name string - Recency string + Name string + Recency string + Pushables string + Pullables string + Selected bool } // GetDisplayStrings returns the dispaly string of branch func (b *Branch) GetDisplayStrings() []string { - return []string{b.Recency, utils.ColoredString(b.Name, b.GetColor())} + displayName := utils.ColoredString(b.Name, b.GetColor()) + if b.Selected && b.Pushables != "" && b.Pullables != "" { + displayName = fmt.Sprintf("↑%s↓%s %s", b.Pushables, b.Pullables, displayName) + } + + return []string{b.Recency, displayName} } // GetColor branch color diff --git a/pkg/commands/git.go b/pkg/commands/git.go index 584558d7e..d9230c4eb 100644 --- a/pkg/commands/git.go +++ b/pkg/commands/git.go @@ -154,7 +154,6 @@ func (c *GitCommand) GetStatusFiles() []*File { } files = append(files, file) } - c.Log.Info(files) // TODO: use a dumper-esque log here return files } @@ -208,19 +207,33 @@ func includesInt(list []int, a int) bool { return false } -// ResetHard does the equivalent of `git reset --hard HEAD` -func (c *GitCommand) ResetHard() error { - return c.Worktree.Reset(&gogit.ResetOptions{Mode: gogit.HardReset}) +// ResetAndClean removes all unstaged changes and removes all untracked files +func (c *GitCommand) ResetAndClean() error { + if err := c.OSCommand.RunCommand("git reset --hard HEAD"); err != nil { + return err + } + + return c.OSCommand.RunCommand("git clean -fd") } -// UpstreamDifferenceCount checks how many pushables/pullables there are for the +func (c *GitCommand) GetCurrentBranchUpstreamDifferenceCount() (string, string) { + return c.GetCommitDifferences("HEAD", "@{u}") +} + +func (c *GitCommand) GetBranchUpstreamDifferenceCount(branchName string) (string, string) { + upstream := "origin" // hardcoded for now + return c.GetCommitDifferences(branchName, fmt.Sprintf("%s/%s", upstream, branchName)) +} + +// GetCommitDifferences checks how many pushables/pullables there are for the // current branch -func (c *GitCommand) UpstreamDifferenceCount() (string, string) { - pushableCount, err := c.OSCommand.RunCommandWithOutput("git rev-list @{u}..HEAD --count") +func (c *GitCommand) GetCommitDifferences(from, to string) (string, string) { + command := "git rev-list %s..%s --count" + pushableCount, err := c.OSCommand.RunCommandWithOutput(fmt.Sprintf(command, to, from)) if err != nil { return "?", "?" } - pullableCount, err := c.OSCommand.RunCommandWithOutput("git rev-list HEAD..@{u} --count") + pullableCount, err := c.OSCommand.RunCommandWithOutput(fmt.Sprintf(command, from, to)) if err != nil { return "?", "?" } @@ -609,3 +622,8 @@ func (c *GitCommand) ApplyPatch(patch string) (string, error) { return c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git apply --cached %s", filename)) } + +func (c *GitCommand) FastForward(branchName string) error { + upstream := "origin" // hardcoding for now + return c.OSCommand.RunCommand(fmt.Sprintf("git fetch %s %s:%s", upstream, branchName, branchName)) +} diff --git a/pkg/commands/git_test.go b/pkg/commands/git_test.go index da6a90c98..7d0a503d7 100644 --- a/pkg/commands/git_test.go +++ b/pkg/commands/git_test.go @@ -557,8 +557,8 @@ func TestGitCommandMergeStatusFiles(t *testing.T) { } } -// TestGitCommandUpstreamDifferentCount is a function. -func TestGitCommandUpstreamDifferentCount(t *testing.T) { +// TestGitCommandGetCommitDifferences is a function. +func TestGitCommandGetCommitDifferences(t *testing.T) { type scenario struct { testName string command func(string, ...string) *exec.Cmd @@ -610,7 +610,7 @@ func TestGitCommandUpstreamDifferentCount(t *testing.T) { t.Run(s.testName, func(t *testing.T) { gitCmd := newDummyGitCommand() gitCmd.OSCommand.command = s.command - s.test(gitCmd.UpstreamDifferenceCount()) + s.test(gitCmd.GetCommitDifferences("HEAD", "@{u}")) }) } } diff --git a/pkg/gui/branches_panel.go b/pkg/gui/branches_panel.go index 059f0af50..e54d6e8c1 100644 --- a/pkg/gui/branches_panel.go +++ b/pkg/gui/branches_panel.go @@ -7,25 +7,116 @@ import ( "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/commands" "github.com/jesseduffield/lazygit/pkg/git" - "github.com/jesseduffield/lazygit/pkg/utils" ) +// list panel functions + +func (gui *Gui) getSelectedBranch() *commands.Branch { + selectedLine := gui.State.Panels.Branches.SelectedLine + if selectedLine == -1 { + return nil + } + + return gui.State.Branches[selectedLine] +} + +// may want to standardise how these select methods work +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")) + } + branch := gui.getSelectedBranch() + if err := gui.focusPoint(0, gui.State.Panels.Branches.SelectedLine, v); err != nil { + return err + } + go func() { + _ = gui.RenderSelectedBranchUpstreamDifferences() + }() + go func() { + 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", graph) + }() + 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 + } + + branch := gui.getSelectedBranch() + branch.Pushables, branch.Pullables = gui.GitCommand.GetBranchUpstreamDifferenceCount(branch.Name) + return gui.renderListPanel(gui.getBranchesView(gui.g), gui.State.Branches) +} + +// gui.refreshStatus is called at the end of this because that's when we can +// be sure there is a state.Branches array to pick the current branch from +func (gui *Gui) refreshBranches(g *gocui.Gui) error { + g.Update(func(g *gocui.Gui) error { + builder, err := git.NewBranchListBuilder(gui.Log, gui.GitCommand) + if err != nil { + return err + } + gui.State.Branches = builder.Build() + + gui.refreshSelectedLine(&gui.State.Panels.Branches.SelectedLine, len(gui.State.Branches)) + if err := gui.resetOrigin(gui.getBranchesView(gui.g)); err != nil { + return err + } + if err := gui.RenderSelectedBranchUpstreamDifferences(); err != nil { + return err + } + + return gui.refreshStatus(g) + }) + return nil +} + +func (gui *Gui) handleBranchesNextLine(g *gocui.Gui, v *gocui.View) error { + panelState := gui.State.Panels.Branches + gui.changeSelectedLine(&panelState.SelectedLine, len(gui.State.Branches), false) + return gui.handleBranchSelect(gui.g, v) +} + +func (gui *Gui) handleBranchesPrevLine(g *gocui.Gui, v *gocui.View) error { + panelState := gui.State.Panels.Branches + gui.changeSelectedLine(&panelState.SelectedLine, len(gui.State.Branches), true) + + return gui.handleBranchSelect(gui.g, v) +} + +// specific functions + func (gui *Gui) handleBranchPress(g *gocui.Gui, v *gocui.View) error { - index := gui.getItemPosition(gui.getBranchesView(g)) - if index == 0 { + if gui.State.Panels.Branches.SelectedLine == -1 { + return nil + } + if gui.State.Panels.Branches.SelectedLine == 0 { return gui.createErrorPanel(g, gui.Tr.SLocalize("AlreadyCheckedOutBranch")) } - branch := gui.getSelectedBranch(gui.getBranchesView(g)) + branch := gui.getSelectedBranch() if err := gui.GitCommand.Checkout(branch.Name, false); err != nil { - gui.createErrorPanel(g, err.Error()) + if err := gui.createErrorPanel(g, err.Error()); err != nil { + return err + } + } else { + gui.State.Panels.Branches.SelectedLine = 0 } + return gui.refreshSidePanels(g) } func (gui *Gui) handleCreatePullRequestPress(g *gocui.Gui, v *gocui.View) error { - branch := gui.getSelectedBranch(gui.getBranchesView(g)) pullRequest := commands.NewPullRequest(gui.GitCommand) + branch := gui.getSelectedBranch() if err := pullRequest.Create(branch); err != nil { return gui.createErrorPanel(g, err.Error()) } @@ -60,7 +151,7 @@ func (gui *Gui) handleGitFetch(g *gocui.Gui, v *gocui.View) error { } func (gui *Gui) handleForceCheckout(g *gocui.Gui, v *gocui.View) error { - branch := gui.getSelectedBranch(v) + branch := gui.getSelectedBranch() message := gui.Tr.SLocalize("SureForceCheckout") title := gui.Tr.SLocalize("ForceCheckoutBranch") return gui.createConfirmationPanel(g, v, title, message, func(g *gocui.Gui, v *gocui.View) error { @@ -108,8 +199,11 @@ func (gui *Gui) handleForceDeleteBranch(g *gocui.Gui, v *gocui.View) error { } func (gui *Gui) deleteBranch(g *gocui.Gui, v *gocui.View, force bool) error { + selectedBranch := gui.getSelectedBranch() + if selectedBranch == nil { + return nil + } checkedOutBranch := gui.State.Branches[0] - selectedBranch := gui.getSelectedBranch(v) if checkedOutBranch.Name == selectedBranch.Name { return gui.createErrorPanel(g, gui.Tr.SLocalize("CantDeleteCheckOutBranch")) } @@ -144,7 +238,7 @@ func (gui *Gui) deleteNamedBranch(g *gocui.Gui, v *gocui.View, selectedBranch *c func (gui *Gui) handleMerge(g *gocui.Gui, v *gocui.View) error { checkedOutBranch := gui.State.Branches[0] - selectedBranch := gui.getSelectedBranch(v) + selectedBranch := gui.getSelectedBranch() defer gui.refreshSidePanels(g) if checkedOutBranch.Name == selectedBranch.Name { return gui.createErrorPanel(g, gui.Tr.SLocalize("CantMergeBranchIntoItself")) @@ -155,59 +249,36 @@ func (gui *Gui) handleMerge(g *gocui.Gui, v *gocui.View) error { return nil } -func (gui *Gui) getSelectedBranch(v *gocui.View) *commands.Branch { - lineNumber := gui.getItemPosition(v) - return gui.State.Branches[lineNumber] -} - -func (gui *Gui) renderBranchesOptions(g *gocui.Gui) error { - return gui.renderGlobalOptions(g) -} - -// may want to standardise how these select methods work -func (gui *Gui) handleBranchSelect(g *gocui.Gui, v *gocui.View) error { - if err := gui.renderBranchesOptions(g); err != nil { - return err +func (gui *Gui) handleFastForward(g *gocui.Gui, v *gocui.View) error { + branch := gui.getSelectedBranch() + if branch == nil { + return nil } - // 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")) + if branch.Pushables == "" { + return nil } + if branch.Pushables == "?" { + return gui.createErrorPanel(gui.g, "Cannot fast-forward a branch with no upstream") + } + if branch.Pushables != "0" { + return gui.createErrorPanel(gui.g, "Cannot fast-forward a branch with commits to push") + } + upstream := "origin" // hardcoding for now + message := gui.Tr.TemplateLocalize( + "Fetching", + Teml{ + "from": fmt.Sprintf("%s/%s", upstream, branch.Name), + "to": branch.Name, + }, + ) go func() { - branch := gui.getSelectedBranch(v) - diff, err := gui.GitCommand.GetBranchGraph(branch.Name) - if err != nil && strings.HasPrefix(diff, "fatal: ambiguous argument") { - diff = gui.Tr.SLocalize("NoTrackingThisBranch") + _ = gui.createMessagePanel(gui.g, v, "", message) + if err := gui.GitCommand.FastForward(branch.Name); err != nil { + _ = gui.createErrorPanel(gui.g, err.Error()) + } else { + _ = gui.closeConfirmationPrompt(gui.g) + _ = gui.RenderSelectedBranchUpstreamDifferences() } - gui.renderString(g, "main", diff) }() return nil } - -// gui.refreshStatus is called at the end of this because that's when we can -// be sure there is a state.Branches array to pick the current branch from -func (gui *Gui) refreshBranches(g *gocui.Gui) error { - g.Update(func(g *gocui.Gui) error { - v, err := g.View("branches") - if err != nil { - panic(err) - } - builder, err := git.NewBranchListBuilder(gui.Log, gui.GitCommand) - if err != nil { - return err - } - gui.State.Branches = builder.Build() - - v.Clear() - list, err := utils.RenderList(gui.State.Branches) - if err != nil { - return err - } - - fmt.Fprint(v, list) - - gui.resetOrigin(v) - return gui.refreshStatus(g) - }) - return nil -} diff --git a/pkg/gui/commit_message_panel.go b/pkg/gui/commit_message_panel.go index 14e3ab795..85497ef87 100644 --- a/pkg/gui/commit_message_panel.go +++ b/pkg/gui/commit_message_panel.go @@ -23,12 +23,12 @@ func (gui *Gui) handleCommitConfirm(g *gocui.Gui, v *gocui.View) error { gui.SubProcess = sub return gui.Errors.ErrSubProcess } - gui.refreshFiles(g) v.Clear() - v.SetCursor(0, 0) - g.SetViewOnBottom("commitMessage") - gui.switchFocus(g, v, gui.getFilesView(g)) - return gui.refreshCommits(g) + _ = v.SetCursor(0, 0) + _ = v.SetOrigin(0, 0) + _, _ = g.SetViewOnBottom("commitMessage") + _ = gui.switchFocus(g, v, gui.getFilesView(g)) + return gui.refreshSidePanels(g) } func (gui *Gui) handleCommitClose(g *gocui.Gui, v *gocui.View) error { diff --git a/pkg/gui/commits_panel.go b/pkg/gui/commits_panel.go index ee7f191a7..4ff79960d 100644 --- a/pkg/gui/commits_panel.go +++ b/pkg/gui/commits_panel.go @@ -9,29 +9,54 @@ import ( "github.com/jesseduffield/lazygit/pkg/utils" ) +// list panel functions + +func (gui *Gui) getSelectedCommit(g *gocui.Gui) *commands.Commit { + selectedLine := gui.State.Panels.Commits.SelectedLine + if selectedLine == -1 { + return nil + } + + return gui.State.Commits[selectedLine] +} + +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")) + } + + if err := gui.focusPoint(0, gui.State.Panels.Commits.SelectedLine, v); err != nil { + return err + } + commitText, err := gui.GitCommand.Show(commit.Sha) + if err != nil { + return err + } + return gui.renderString(g, "main", commitText) +} + func (gui *Gui) refreshCommits(g *gocui.Gui) error { g.Update(func(*gocui.Gui) error { commits, err := gui.GitCommand.GetCommits() if err != nil { return err } - gui.State.Commits = commits - v, err := g.View("commits") - if err != nil { - return err - } - v.Clear() + gui.refreshSelectedLine(&gui.State.Panels.Commits.SelectedLine, len(gui.State.Commits)) list, err := utils.RenderList(gui.State.Commits) if err != nil { return err } + + v := gui.getCommitsView(gui.g) + v.Clear() fmt.Fprint(v, list) gui.refreshStatus(g) - if g.CurrentView().Name() == "commits" { + if v == g.CurrentView() { gui.handleCommitSelect(g, v) } return nil @@ -39,11 +64,27 @@ func (gui *Gui) refreshCommits(g *gocui.Gui) error { return nil } +func (gui *Gui) handleCommitsNextLine(g *gocui.Gui, v *gocui.View) error { + panelState := gui.State.Panels.Commits + gui.changeSelectedLine(&panelState.SelectedLine, len(gui.State.Commits), false) + + return gui.handleCommitSelect(gui.g, v) +} + +func (gui *Gui) handleCommitsPrevLine(g *gocui.Gui, v *gocui.View) error { + panelState := gui.State.Panels.Commits + gui.changeSelectedLine(&panelState.SelectedLine, len(gui.State.Commits), true) + + return gui.handleCommitSelect(gui.g, v) +} + +// specific functions + func (gui *Gui) handleResetToCommit(g *gocui.Gui, commitView *gocui.View) error { return gui.createConfirmationPanel(g, commitView, gui.Tr.SLocalize("ResetToCommit"), gui.Tr.SLocalize("SureResetThisCommit"), func(g *gocui.Gui, v *gocui.View) error { - commit, err := gui.getSelectedCommit(g) - if err != nil { - panic(err) + commit := gui.getSelectedCommit(g) + if commit == nil { + panic(errors.New(gui.Tr.SLocalize("NoCommitsThisBranch"))) } if err := gui.GitCommand.ResetToCommit(commit.Sha); err != nil { return gui.createErrorPanel(g, err.Error()) @@ -55,42 +96,21 @@ func (gui *Gui) handleResetToCommit(g *gocui.Gui, commitView *gocui.View) error panic(err) } gui.resetOrigin(commitView) - return gui.handleCommitSelect(g, nil) + gui.State.Panels.Commits.SelectedLine = 0 + return gui.handleCommitSelect(g, commitView) }, nil) } -func (gui *Gui) renderCommitsOptions(g *gocui.Gui) error { - return gui.renderGlobalOptions(g) -} - -func (gui *Gui) handleCommitSelect(g *gocui.Gui, v *gocui.View) error { - if err := gui.renderCommitsOptions(g); err != nil { - return err - } - commit, err := gui.getSelectedCommit(g) - if err != nil { - if err.Error() != gui.Tr.SLocalize("NoCommitsThisBranch") { - return err - } - return gui.renderString(g, "main", gui.Tr.SLocalize("NoCommitsThisBranch")) - } - commitText, err := gui.GitCommand.Show(commit.Sha) - if err != nil { - return err - } - return gui.renderString(g, "main", commitText) -} - func (gui *Gui) handleCommitSquashDown(g *gocui.Gui, v *gocui.View) error { - if gui.getItemPosition(v) != 0 { + if gui.State.Panels.Commits.SelectedLine != 0 { return gui.createErrorPanel(g, gui.Tr.SLocalize("OnlySquashTopmostCommit")) } - if len(gui.State.Commits) == 1 { + if len(gui.State.Commits) <= 1 { return gui.createErrorPanel(g, gui.Tr.SLocalize("YouNoCommitsToSquash")) } - commit, err := gui.getSelectedCommit(g) - if err != nil { - return err + commit := gui.getSelectedCommit(g) + if commit == nil { + return errors.New(gui.Tr.SLocalize("NoCommitsThisBranch")) } if err := gui.GitCommand.SquashPreviousTwoCommits(commit.Name); err != nil { return gui.createErrorPanel(g, err.Error()) @@ -113,16 +133,16 @@ func (gui *Gui) anyUnStagedChanges(files []*commands.File) bool { } func (gui *Gui) handleCommitFixup(g *gocui.Gui, v *gocui.View) error { - if len(gui.State.Commits) == 1 { + if len(gui.State.Commits) <= 1 { return gui.createErrorPanel(g, gui.Tr.SLocalize("YouNoCommitsToSquash")) } if gui.anyUnStagedChanges(gui.State.Files) { return gui.createErrorPanel(g, gui.Tr.SLocalize("CantFixupWhileUnstagedChanges")) } branch := gui.State.Branches[0] - commit, err := gui.getSelectedCommit(g) - if err != nil { - return err + commit := gui.getSelectedCommit(g) + if commit == nil { + return gui.createErrorPanel(g, gui.Tr.SLocalize("NoCommitsThisBranch")) } message := gui.Tr.SLocalize("SureFixupThisCommit") gui.createConfirmationPanel(g, v, gui.Tr.SLocalize("Fixup"), message, func(g *gocui.Gui, v *gocui.View) error { @@ -138,7 +158,7 @@ func (gui *Gui) handleCommitFixup(g *gocui.Gui, v *gocui.View) error { } func (gui *Gui) handleRenameCommit(g *gocui.Gui, v *gocui.View) error { - if gui.getItemPosition(gui.getCommitsView(g)) != 0 { + if gui.State.Panels.Commits.SelectedLine != 0 { return gui.createErrorPanel(g, gui.Tr.SLocalize("OnlyRenameTopCommit")) } return gui.createPromptPanel(g, v, gui.Tr.SLocalize("renameCommit"), func(g *gocui.Gui, v *gocui.View) error { @@ -153,7 +173,7 @@ func (gui *Gui) handleRenameCommit(g *gocui.Gui, v *gocui.View) error { } func (gui *Gui) handleRenameCommitEditor(g *gocui.Gui, v *gocui.View) error { - if gui.getItemPosition(gui.getCommitsView(g)) != 0 { + if gui.State.Panels.Commits.SelectedLine != 0 { return gui.createErrorPanel(g, gui.Tr.SLocalize("OnlyRenameTopCommit")) } @@ -164,19 +184,3 @@ func (gui *Gui) handleRenameCommitEditor(g *gocui.Gui, v *gocui.View) error { return nil } - -func (gui *Gui) getSelectedCommit(g *gocui.Gui) (*commands.Commit, error) { - v, err := g.View("commits") - if err != nil { - panic(err) - } - if len(gui.State.Commits) == 0 { - return &commands.Commit{}, errors.New(gui.Tr.SLocalize("NoCommitsThisBranch")) - } - lineNumber := gui.getItemPosition(v) - if lineNumber > len(gui.State.Commits)-1 { - gui.Log.Info(gui.Tr.SLocalize("PotentialErrInGetselectedCommit"), gui.State.Commits, lineNumber) - return gui.State.Commits[len(gui.State.Commits)-1], nil - } - return gui.State.Commits[lineNumber], nil -} diff --git a/pkg/gui/confirmation_panel.go b/pkg/gui/confirmation_panel.go index 75349777c..521bd827d 100644 --- a/pkg/gui/confirmation_panel.go +++ b/pkg/gui/confirmation_panel.go @@ -28,7 +28,7 @@ func (gui *Gui) wrappedConfirmationFunction(function func(*gocui.Gui, *gocui.Vie func (gui *Gui) closeConfirmationPrompt(g *gocui.Gui) error { view, err := g.View("confirmation") if err != nil { - panic(err) + return nil // if it's already been closed we can just return } if err := gui.returnFocus(g, view); err != nil { panic(err) @@ -77,11 +77,10 @@ func (gui *Gui) prepareConfirmationPanel(currentView *gocui.View, title, prompt confirmationView.Wrap = true confirmationView.FgColor = gocui.ColorWhite } - confirmationView.Clear() - - if err := gui.switchFocus(gui.g, currentView, confirmationView); err != nil { - return nil, err - } + gui.g.Update(func(g *gocui.Gui) error { + confirmationView.Clear() + return gui.switchFocus(gui.g, currentView, confirmationView) + }) return confirmationView, nil } diff --git a/pkg/gui/files_panel.go b/pkg/gui/files_panel.go index cd8df1859..7942e421b 100644 --- a/pkg/gui/files_panel.go +++ b/pkg/gui/files_panel.go @@ -15,6 +15,79 @@ import ( "github.com/jesseduffield/lazygit/pkg/utils" ) +// list panel functions + +func (gui *Gui) getSelectedFile(g *gocui.Gui) (*commands.File, error) { + selectedLine := gui.State.Panels.Files.SelectedLine + if selectedLine == -1 { + return &commands.File{}, gui.Errors.ErrNoFiles + } + + return gui.State.Files[selectedLine], nil +} + +func (gui *Gui) handleFileSelect(g *gocui.Gui, v *gocui.View) error { + file, err := gui.getSelectedFile(g) + if err != nil { + if err != gui.Errors.ErrNoFiles { + return err + } + return gui.renderString(g, "main", gui.Tr.SLocalize("NoChangedFiles")) + } + + if file.HasMergeConflicts { + return gui.refreshMergePanel(g) + } + + if err := gui.focusPoint(0, gui.State.Panels.Files.SelectedLine, v); err != nil { + return err + } + + content := gui.GitCommand.Diff(file, false) + return gui.renderString(g, "main", content) +} + +func (gui *Gui) refreshFiles(g *gocui.Gui) error { + filesView, err := g.View("files") + if err != nil { + return err + } + gui.refreshStateFiles() + + gui.g.Update(func(g *gocui.Gui) error { + + filesView.Clear() + list, err := utils.RenderList(gui.State.Files) + if err != nil { + return err + } + fmt.Fprint(filesView, list) + + if filesView == g.CurrentView() { + return gui.handleFileSelect(g, filesView) + } + return nil + }) + + return nil +} + +func (gui *Gui) handleFilesNextLine(g *gocui.Gui, v *gocui.View) error { + panelState := gui.State.Panels.Files + gui.changeSelectedLine(&panelState.SelectedLine, len(gui.State.Files), false) + + return gui.handleFileSelect(gui.g, v) +} + +func (gui *Gui) handleFilesPrevLine(g *gocui.Gui, v *gocui.View) error { + panelState := gui.State.Panels.Files + gui.changeSelectedLine(&panelState.SelectedLine, len(gui.State.Files), true) + + return gui.handleFileSelect(gui.g, v) +} + +// specific functions + func (gui *Gui) stagedFiles() []*commands.File { files := gui.State.Files result := make([]*commands.File, 0) @@ -139,18 +212,6 @@ func (gui *Gui) handleAddPatch(g *gocui.Gui, v *gocui.View) error { return gui.Errors.ErrSubProcess } -func (gui *Gui) getSelectedFile(g *gocui.Gui) (*commands.File, error) { - if len(gui.State.Files) == 0 { - return &commands.File{}, gui.Errors.ErrNoFiles - } - filesView, err := g.View("files") - if err != nil { - panic(err) - } - lineNumber := gui.getItemPosition(filesView) - return gui.State.Files[lineNumber], nil -} - func (gui *Gui) handleFileRemove(g *gocui.Gui, v *gocui.View) error { file, err := gui.getSelectedFile(g) if err != nil { @@ -194,30 +255,6 @@ func (gui *Gui) handleIgnoreFile(g *gocui.Gui, v *gocui.View) error { return gui.refreshFiles(g) } -func (gui *Gui) renderfilesOptions(g *gocui.Gui, file *commands.File) error { - return gui.renderGlobalOptions(g) -} - -func (gui *Gui) handleFileSelect(g *gocui.Gui, v *gocui.View) error { - file, err := gui.getSelectedFile(g) - if err != nil { - if err != gui.Errors.ErrNoFiles { - return err - } - gui.renderString(g, "main", gui.Tr.SLocalize("NoChangedFiles")) - return gui.renderfilesOptions(g, nil) - } - if err := gui.renderfilesOptions(g, file); err != nil { - return err - } - if file.HasMergeConflicts { - return gui.refreshMergePanel(g) - } - - content := gui.GitCommand.Diff(file, false) - return gui.renderString(g, "main", content) -} - func (gui *Gui) handleCommitPress(g *gocui.Gui, filesView *gocui.View) error { if len(gui.stagedFiles()) == 0 && !gui.State.HasMergeConflicts { return gui.createErrorPanel(g, gui.Tr.SLocalize("NoStagedFilesToCommit")) @@ -309,6 +346,7 @@ func (gui *Gui) refreshStateFiles() { // get files to stage files := gui.GitCommand.GetStatusFiles() gui.State.Files = gui.GitCommand.MergeStatusFiles(gui.State.Files, files) + gui.refreshSelectedLine(&gui.State.Panels.Files.SelectedLine, len(gui.State.Files)) gui.updateHasMergeConflictStatus() } @@ -340,27 +378,6 @@ func (gui *Gui) catSelectedFile(g *gocui.Gui) (string, error) { return cat, nil } -func (gui *Gui) refreshFiles(g *gocui.Gui) error { - filesView, err := g.View("files") - if err != nil { - return err - } - gui.refreshStateFiles() - - filesView.Clear() - list, err := utils.RenderList(gui.State.Files) - if err != nil { - return err - } - fmt.Fprint(filesView, list) - - gui.correctCursor(filesView) - if filesView == g.CurrentView() { - gui.handleFileSelect(g, filesView) - } - return nil -} - func (gui *Gui) pullFiles(g *gocui.Gui, v *gocui.View) error { if err := gui.createMessagePanel(gui.g, v, "", gui.Tr.SLocalize("PullWait")); err != nil { return err @@ -424,7 +441,7 @@ func (gui *Gui) pushWithForceFlag(g *gocui.Gui, v *gocui.View, force bool) error func (gui *Gui) pushFiles(g *gocui.Gui, v *gocui.View) error { // if we have pullables we'll ask if the user wants to force push - _, pullables := gui.GitCommand.UpstreamDifferenceCount() + _, pullables := gui.GitCommand.GetCurrentBranchUpstreamDifferenceCount() if pullables == "?" || pullables == "0" { return gui.pushWithForceFlag(g, v, false) } @@ -462,9 +479,9 @@ func (gui *Gui) handleAbortMerge(g *gocui.Gui, v *gocui.View) error { return gui.refreshFiles(g) } -func (gui *Gui) handleResetHard(g *gocui.Gui, v *gocui.View) error { +func (gui *Gui) handleResetAndClean(g *gocui.Gui, v *gocui.View) error { return gui.createConfirmationPanel(g, v, gui.Tr.SLocalize("ClearFilePanel"), gui.Tr.SLocalize("SureResetHardHead"), func(g *gocui.Gui, v *gocui.View) error { - if err := gui.GitCommand.ResetHard(); err != nil { + if err := gui.GitCommand.ResetAndClean(); err != nil { gui.createErrorPanel(g, err.Error()) } return gui.refreshFiles(g) diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index 70e61c8d5..d47913c32 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -76,11 +76,43 @@ type Gui struct { introAgree sync.WaitGroup } -type stagingState struct { - StageableLines []int - HunkStarts []int - CurrentLineIndex int - Diff string +// for now the staging panel state, unlike the other panel states, is going to be +// non-mutative, so that we don't accidentally end up +// with mismatches of data. We might change this in the future +type stagingPanelState struct { + SelectedLine int + StageableLines []int + HunkStarts []int + Diff string +} + +type filePanelState struct { + SelectedLine int +} + +type branchPanelState struct { + SelectedLine int +} + +type commitPanelState struct { + SelectedLine int +} + +type stashPanelState struct { + SelectedLine int +} + +type menuPanelState struct { + SelectedLine int +} + +type panelStates struct { + Files *filePanelState + Staging *stagingPanelState + Branches *branchPanelState + Commits *commitPanelState + Stash *stashPanelState + Menu *menuPanelState } type guiState struct { @@ -96,7 +128,7 @@ type guiState struct { EditHistory *stack.Stack Platform commands.Platform Updating bool - StagingState *stagingState + Panels *panelStates } // NewGui builds a new gui handler @@ -112,6 +144,13 @@ func NewGui(log *logrus.Entry, gitCommand *commands.GitCommand, oSCommand *comma Conflicts: make([]commands.Conflict, 0), EditHistory: stack.New(), Platform: *oSCommand.Platform, + Panels: &panelStates{ + Files: &filePanelState{SelectedLine: -1}, + Branches: &branchPanelState{SelectedLine: 0}, + Commits: &commitPanelState{SelectedLine: -1}, + Stash: &stashPanelState{SelectedLine: -1}, + Menu: &menuPanelState{SelectedLine: 0}, + }, } gui := &Gui{ @@ -197,9 +236,11 @@ func (gui *Gui) layout(g *gocui.Gui) error { } v.Title = gui.Tr.SLocalize("NotEnoughSpace") v.Wrap = true - g.SetCurrentView(v.Name()) + g.SetViewOnTop("limit") } return nil + } else { + _, _ = g.SetViewOnBottom("limit") } g.DeleteView("limit") @@ -251,12 +292,13 @@ func (gui *Gui) layout(g *gocui.Gui) error { v.FgColor = gocui.ColorWhite } - if v, err := g.SetView("branches", 0, filesBranchesBoundary+panelSpacing, leftSideWidth, commitsBranchesBoundary, gocui.TOP|gocui.BOTTOM); err != nil { + branchesView, err := g.SetView("branches", 0, filesBranchesBoundary+panelSpacing, leftSideWidth, commitsBranchesBoundary, gocui.TOP|gocui.BOTTOM) + if err != nil { if err != gocui.ErrUnknownView { return err } - v.Title = gui.Tr.SLocalize("BranchesTitle") - v.FgColor = gocui.ColorWhite + branchesView.Title = gui.Tr.SLocalize("BranchesTitle") + branchesView.FgColor = gocui.ColorWhite } if v, err := g.SetView("commits", 0, commitsBranchesBoundary+panelSpacing, leftSideWidth, commitsStashBoundary, gocui.TOP|gocui.BOTTOM); err != nil { @@ -346,11 +388,14 @@ func (gui *Gui) layout(g *gocui.Gui) error { return err } - gui.handleFileSelect(g, filesView) - gui.refreshFiles(g) - gui.refreshBranches(g) - gui.refreshCommits(g) - gui.refreshStashEntries(g) + if _, err := gui.g.SetCurrentView(filesView.Name()); err != nil { + return err + } + + if err := gui.refreshSidePanels(gui.g); err != nil { + return err + } + if err := gui.switchFocus(g, nil, filesView); err != nil { return err } @@ -362,6 +407,22 @@ func (gui *Gui) layout(g *gocui.Gui) error { } } + listViews := map[*gocui.View]int{ + filesView: gui.State.Panels.Files.SelectedLine, + branchesView: gui.State.Panels.Branches.SelectedLine, + } + for view, selectedLine := range listViews { + // check if the selected line is now out of view and if so refocus it + if err := gui.focusPoint(0, selectedLine, view); err != nil { + return err + } + } + + // here is a good place log some stuff + // if you download humanlog and do tail -f development.log | humanlog + // this will let you see these branches as prettified json + // gui.Log.Info(utils.AsJson(gui.State.Branches[0:4])) + return gui.resizeCurrentPopupPanel(g) } @@ -416,8 +477,8 @@ func (gui *Gui) renderAppStatus(g *gocui.Gui) error { return nil } -func (gui *Gui) renderGlobalOptions(g *gocui.Gui) error { - return gui.renderOptionsMap(g, map[string]string{ +func (gui *Gui) renderGlobalOptions() error { + return gui.renderOptionsMap(map[string]string{ "PgUp/PgDn": gui.Tr.SLocalize("scroll"), "← → ↑ ↓": gui.Tr.SLocalize("navigate"), "esc/q": gui.Tr.SLocalize("close"), diff --git a/pkg/gui/keybindings.go b/pkg/gui/keybindings.go index 28647b245..978f6dcdf 100644 --- a/pkg/gui/keybindings.go +++ b/pkg/gui/keybindings.go @@ -210,7 +210,7 @@ func (gui *Gui) GetKeybindings() []*Binding { ViewName: "files", Key: 'D', Modifier: gocui.ModNone, - Handler: gui.handleResetHard, + Handler: gui.handleResetAndClean, Description: gui.Tr.SLocalize("resetHard"), }, { ViewName: "files", @@ -328,6 +328,12 @@ func (gui *Gui) GetKeybindings() []*Binding { Modifier: gocui.ModNone, Handler: gui.handleMerge, Description: gui.Tr.SLocalize("mergeIntoCurrentBranch"), + }, { + ViewName: "branches", + Key: 'f', + Modifier: gocui.ModNone, + Handler: gui.handleFastForward, + Description: gui.Tr.SLocalize("FastForward"), }, { ViewName: "commits", Key: 's', @@ -469,19 +475,33 @@ func (gui *Gui) GetKeybindings() []*Binding { }, } - // Would make these keybindings global but that interferes with editing - // input in the confirmation panel - for _, viewName := range []string{"status", "files", "branches", "commits", "stash", "menu"} { + for _, viewName := range []string{"status", "branches", "files", "commits", "stash", "menu"} { bindings = append(bindings, []*Binding{ {ViewName: viewName, Key: gocui.KeyTab, Modifier: gocui.ModNone, Handler: gui.nextView}, {ViewName: viewName, Key: gocui.KeyArrowLeft, Modifier: gocui.ModNone, Handler: gui.previousView}, {ViewName: viewName, Key: gocui.KeyArrowRight, Modifier: gocui.ModNone, Handler: gui.nextView}, - {ViewName: viewName, Key: gocui.KeyArrowUp, Modifier: gocui.ModNone, Handler: gui.cursorUp}, - {ViewName: viewName, Key: gocui.KeyArrowDown, Modifier: gocui.ModNone, Handler: gui.cursorDown}, {ViewName: viewName, Key: 'h', Modifier: gocui.ModNone, Handler: gui.previousView}, {ViewName: viewName, Key: 'l', Modifier: gocui.ModNone, Handler: gui.nextView}, - {ViewName: viewName, Key: 'k', Modifier: gocui.ModNone, Handler: gui.cursorUp}, - {ViewName: viewName, Key: 'j', Modifier: gocui.ModNone, Handler: gui.cursorDown}, + }...) + } + + listPanelMap := map[string]struct { + prevLine func(*gocui.Gui, *gocui.View) error + nextLine func(*gocui.Gui, *gocui.View) error + }{ + "menu": {prevLine: gui.handleMenuPrevLine, nextLine: gui.handleMenuNextLine}, + "files": {prevLine: gui.handleFilesPrevLine, nextLine: gui.handleFilesNextLine}, + "branches": {prevLine: gui.handleBranchesPrevLine, nextLine: gui.handleBranchesNextLine}, + "commits": {prevLine: gui.handleCommitsPrevLine, nextLine: gui.handleCommitsNextLine}, + "stash": {prevLine: gui.handleStashPrevLine, nextLine: gui.handleStashNextLine}, + } + + for viewName, functions := range listPanelMap { + bindings = append(bindings, []*Binding{ + {ViewName: viewName, Key: 'k', Modifier: gocui.ModNone, Handler: functions.prevLine}, + {ViewName: viewName, Key: gocui.KeyArrowUp, Modifier: gocui.ModNone, Handler: functions.prevLine}, + {ViewName: viewName, Key: 'j', Modifier: gocui.ModNone, Handler: functions.nextLine}, + {ViewName: viewName, Key: gocui.KeyArrowDown, Modifier: gocui.ModNone, Handler: functions.nextLine}, }...) } diff --git a/pkg/gui/menu_panel.go b/pkg/gui/menu_panel.go index 753e8f84d..8ce60f6be 100644 --- a/pkg/gui/menu_panel.go +++ b/pkg/gui/menu_panel.go @@ -8,19 +8,35 @@ import ( "github.com/jesseduffield/lazygit/pkg/utils" ) +// list panel functions + func (gui *Gui) handleMenuSelect(g *gocui.Gui, v *gocui.View) error { - // doing nothing for now - // but it is needed for switch in newLineFocused - return nil + return gui.focusPoint(0, gui.State.Panels.Menu.SelectedLine, v) } -func (gui *Gui) renderMenuOptions(g *gocui.Gui) error { +func (gui *Gui) handleMenuNextLine(g *gocui.Gui, v *gocui.View) error { + panelState := gui.State.Panels.Menu + gui.changeSelectedLine(&panelState.SelectedLine, v.LinesHeight(), false) + + return gui.handleMenuSelect(g, v) +} + +func (gui *Gui) handleMenuPrevLine(g *gocui.Gui, v *gocui.View) error { + panelState := gui.State.Panels.Menu + gui.changeSelectedLine(&panelState.SelectedLine, v.LinesHeight(), true) + + return gui.handleMenuSelect(g, v) +} + +// specific functions + +func (gui *Gui) renderMenuOptions() error { optionsMap := map[string]string{ "esc/q": gui.Tr.SLocalize("close"), "↑ ↓": gui.Tr.SLocalize("navigate"), "space": gui.Tr.SLocalize("execute"), } - return gui.renderOptionsMap(g, optionsMap) + return gui.renderOptionsMap(optionsMap) } func (gui *Gui) handleMenuClose(g *gocui.Gui, v *gocui.View) error { @@ -46,14 +62,11 @@ func (gui *Gui) createMenu(items interface{}, handlePress func(int) error) error menuView.FgColor = gocui.ColorWhite menuView.Clear() fmt.Fprint(menuView, list) - - if err := gui.renderMenuOptions(gui.g); err != nil { - return err - } + gui.State.Panels.Menu.SelectedLine = 0 wrappedHandlePress := func(g *gocui.Gui, v *gocui.View) error { - lineNumber := gui.getItemPosition(v) - return handlePress(lineNumber) + selectedLine := gui.State.Panels.Menu.SelectedLine + return handlePress(selectedLine) } if err := gui.g.SetKeybinding("menu", gocui.KeySpace, gocui.ModNone, wrappedHandlePress); err != nil { diff --git a/pkg/gui/merge_panel.go b/pkg/gui/merge_panel.go index e3da82f3c..2592275bf 100644 --- a/pkg/gui/merge_panel.go +++ b/pkg/gui/merge_panel.go @@ -194,9 +194,6 @@ func (gui *Gui) refreshMergePanel(g *gocui.Gui) error { gui.State.ConflictIndex = len(gui.State.Conflicts) - 1 } hasFocus := gui.currentViewName(g) == "main" - if hasFocus { - gui.renderMergeOptions(g) - } content, err := gui.coloredConflictFile(cat, gui.State.Conflicts, gui.State.ConflictIndex, gui.State.ConflictTop, hasFocus) if err != nil { return err @@ -233,8 +230,8 @@ func (gui *Gui) switchToMerging(g *gocui.Gui) error { return gui.refreshMergePanel(g) } -func (gui *Gui) renderMergeOptions(g *gocui.Gui) error { - return gui.renderOptionsMap(g, map[string]string{ +func (gui *Gui) renderMergeOptions() error { + return gui.renderOptionsMap(map[string]string{ "↑ ↓": gui.Tr.SLocalize("selectHunk"), "← →": gui.Tr.SLocalize("navigateConflicts"), "space": gui.Tr.SLocalize("pickHunk"), diff --git a/pkg/gui/staging_panel.go b/pkg/gui/staging_panel.go index cba7d7638..1408cfb45 100644 --- a/pkg/gui/staging_panel.go +++ b/pkg/gui/staging_panel.go @@ -40,23 +40,23 @@ func (gui *Gui) refreshStagingPanel() error { return nil } - var currentLineIndex int - if gui.State.StagingState != nil { + var selectedLine int + if gui.State.Panels.Staging != nil { end := len(stageableLines) - 1 - if end < gui.State.StagingState.CurrentLineIndex { - currentLineIndex = end + if end < gui.State.Panels.Staging.SelectedLine { + selectedLine = end } else { - currentLineIndex = gui.State.StagingState.CurrentLineIndex + selectedLine = gui.State.Panels.Staging.SelectedLine } } else { - currentLineIndex = 0 + selectedLine = 0 } - gui.State.StagingState = &stagingState{ - StageableLines: stageableLines, - HunkStarts: hunkStarts, - CurrentLineIndex: currentLineIndex, - Diff: diff, + gui.State.Panels.Staging = &stagingPanelState{ + StageableLines: stageableLines, + HunkStarts: hunkStarts, + SelectedLine: selectedLine, + Diff: diff, } if len(stageableLines) == 0 { @@ -74,7 +74,7 @@ func (gui *Gui) handleStagingEscape(g *gocui.Gui, v *gocui.View) error { return err } - gui.State.StagingState = nil + gui.State.Panels.Staging = nil return gui.switchFocus(gui.g, nil, gui.getFilesView(gui.g)) } @@ -96,9 +96,9 @@ func (gui *Gui) handleStagingNextHunk(g *gocui.Gui, v *gocui.View) error { } func (gui *Gui) handleCycleHunk(prev bool) error { - state := gui.State.StagingState + state := gui.State.Panels.Staging lineNumbers := state.StageableLines - currentLine := lineNumbers[state.CurrentLineIndex] + currentLine := lineNumbers[state.SelectedLine] currentHunkIndex := utils.PrevIndex(state.HunkStarts, currentLine) var newHunkIndex int if prev { @@ -115,22 +115,22 @@ func (gui *Gui) handleCycleHunk(prev bool) error { } } - state.CurrentLineIndex = utils.NextIndex(lineNumbers, state.HunkStarts[newHunkIndex]) + state.SelectedLine = utils.NextIndex(lineNumbers, state.HunkStarts[newHunkIndex]) return gui.focusLineAndHunk() } func (gui *Gui) handleCycleLine(prev bool) error { - state := gui.State.StagingState + state := gui.State.Panels.Staging lineNumbers := state.StageableLines - currentLine := lineNumbers[state.CurrentLineIndex] + currentLine := lineNumbers[state.SelectedLine] var newIndex int if prev { newIndex = utils.PrevIndex(lineNumbers, currentLine) } else { newIndex = utils.NextIndex(lineNumbers, currentLine) } - state.CurrentLineIndex = newIndex + state.SelectedLine = newIndex return gui.focusLineAndHunk() } @@ -139,9 +139,9 @@ func (gui *Gui) handleCycleLine(prev bool) error { // selected line and size of the hunk func (gui *Gui) focusLineAndHunk() error { stagingView := gui.getStagingView(gui.g) - state := gui.State.StagingState + state := gui.State.Panels.Staging - lineNumber := state.StageableLines[state.CurrentLineIndex] + lineNumber := state.StageableLines[state.SelectedLine] // we want the bottom line of the view buffer to ideally be the bottom line // of the hunk, but if the hunk is too big we'll just go three lines beyond @@ -170,23 +170,7 @@ func (gui *Gui) focusLineAndHunk() error { bottomLine = lineNumber + 3 } - return gui.focusLine(lineNumber, bottomLine, stagingView) -} - -// focusLine takes a lineNumber to focus, and a bottomLine to ensure we can see -func (gui *Gui) focusLine(lineNumber int, bottomLine int, v *gocui.View) error { - _, height := v.Size() - overScroll := bottomLine - height + 1 - if overScroll < 0 { - overScroll = 0 - } - if err := v.SetOrigin(0, overScroll); err != nil { - return err - } - if err := v.SetCursor(0, lineNumber-overScroll); err != nil { - return err - } - return nil + return gui.generalFocusLine(lineNumber, bottomLine, stagingView) } func (gui *Gui) handleStageHunk(g *gocui.Gui, v *gocui.View) error { @@ -198,13 +182,13 @@ func (gui *Gui) handleStageLine(g *gocui.Gui, v *gocui.View) error { } func (gui *Gui) handleStageLineOrHunk(hunk bool) error { - state := gui.State.StagingState + state := gui.State.Panels.Staging p, err := git.NewPatchModifier(gui.Log) if err != nil { return err } - currentLine := state.StageableLines[state.CurrentLineIndex] + currentLine := state.StageableLines[state.SelectedLine] var patch string if hunk { patch, err = p.ModifyPatchForHunk(state.Diff, state.HunkStarts, currentLine) diff --git a/pkg/gui/stash_panel.go b/pkg/gui/stash_panel.go index 62b4efda7..306d61771 100644 --- a/pkg/gui/stash_panel.go +++ b/pkg/gui/stash_panel.go @@ -8,19 +8,46 @@ import ( "github.com/jesseduffield/lazygit/pkg/utils" ) +// list panel functions + +func (gui *Gui) getSelectedStashEntry(v *gocui.View) *commands.StashEntry { + selectedLine := gui.State.Panels.Stash.SelectedLine + if selectedLine == -1 { + return nil + } + + return gui.State.StashEntries[selectedLine] +} + +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")) + } + if err := gui.focusPoint(0, gui.State.Panels.Stash.SelectedLine, 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) + }() + return nil +} + func (gui *Gui) refreshStashEntries(g *gocui.Gui) error { g.Update(func(g *gocui.Gui) error { - v, err := g.View("stash") - if err != nil { - panic(err) - } gui.State.StashEntries = gui.GitCommand.GetStashEntries() - v.Clear() + gui.refreshSelectedLine(&gui.State.Panels.Stash.SelectedLine, len(gui.State.StashEntries)) + list, err := utils.RenderList(gui.State.StashEntries) if err != nil { return err } + + v := gui.getStashView(gui.g) + v.Clear() fmt.Fprint(v, list) return gui.resetOrigin(v) @@ -28,34 +55,21 @@ func (gui *Gui) refreshStashEntries(g *gocui.Gui) error { return nil } -func (gui *Gui) getSelectedStashEntry(v *gocui.View) *commands.StashEntry { - if len(gui.State.StashEntries) == 0 { - return nil - } - stashView, _ := gui.g.View("stash") - lineNumber := gui.getItemPosition(stashView) - return gui.State.StashEntries[lineNumber] +func (gui *Gui) handleStashNextLine(g *gocui.Gui, v *gocui.View) error { + panelState := gui.State.Panels.Stash + gui.changeSelectedLine(&panelState.SelectedLine, len(gui.State.StashEntries), false) + + return gui.handleStashEntrySelect(gui.g, v) } -func (gui *Gui) renderStashOptions(g *gocui.Gui) error { - return gui.renderGlobalOptions(g) +func (gui *Gui) handleStashPrevLine(g *gocui.Gui, v *gocui.View) error { + panelState := gui.State.Panels.Stash + gui.changeSelectedLine(&panelState.SelectedLine, len(gui.State.StashEntries), true) + + return gui.handleStashEntrySelect(gui.g, v) } -func (gui *Gui) handleStashEntrySelect(g *gocui.Gui, v *gocui.View) error { - if err := gui.renderStashOptions(g); err != nil { - return err - } - go func() { - stashEntry := gui.getSelectedStashEntry(v) - if stashEntry == nil { - gui.renderString(g, "main", gui.Tr.SLocalize("NoStashEntries")) - return - } - diff, _ := gui.GitCommand.GetStashEntryDiff(stashEntry.Index) - gui.renderString(g, "main", diff) - }() - return nil -} +// specific functions func (gui *Gui) handleStashApply(g *gocui.Gui, v *gocui.View) error { return gui.stashDo(g, v, "apply") diff --git a/pkg/gui/status_panel.go b/pkg/gui/status_panel.go index aeae19c50..8d3b34dfe 100644 --- a/pkg/gui/status_panel.go +++ b/pkg/gui/status_panel.go @@ -19,7 +19,7 @@ func (gui *Gui) refreshStatus(g *gocui.Gui) error { // contents end up cleared g.Update(func(*gocui.Gui) error { v.Clear() - pushables, pullables := gui.GitCommand.UpstreamDifferenceCount() + pushables, pullables := gui.GitCommand.GetCurrentBranchUpstreamDifferenceCount() fmt.Fprint(v, "↑"+pushables+"↓"+pullables) branches := gui.State.Branches if err := gui.updateHasMergeConflictStatus(); err != nil { @@ -42,16 +42,14 @@ func (gui *Gui) refreshStatus(g *gocui.Gui) error { return nil } -func (gui *Gui) renderStatusOptions(g *gocui.Gui) error { - return gui.renderGlobalOptions(g) -} - func (gui *Gui) handleCheckForUpdate(g *gocui.Gui, v *gocui.View) error { gui.Updater.CheckForNewUpdate(gui.onUserUpdateCheckFinish, true) return gui.createMessagePanel(gui.g, v, "", gui.Tr.SLocalize("CheckingForUpdates")) } func (gui *Gui) handleStatusSelect(g *gocui.Gui, v *gocui.View) error { + blue := color.New(color.FgBlue) + dashboardString := strings.Join( []string{ lazygitTitle(), @@ -60,13 +58,10 @@ func (gui *Gui) handleStatusSelect(g *gocui.Gui, v *gocui.View) error { "Config Options: https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md", "Tutorial: https://youtu.be/VDXvbHZYeKY", "Raise an Issue: https://github.com/jesseduffield/lazygit/issues", - "Buy Jesse a coffee: https://donorbox.org/lazygit", + blue.Sprint("Buy Jesse a coffee: https://donorbox.org/lazygit"), // caffeine ain't free }, "\n\n") - if err := gui.renderString(g, "main", dashboardString); err != nil { - return err - } - return gui.renderStatusOptions(g) + return gui.renderString(g, "main", dashboardString) } func (gui *Gui) handleOpenConfig(g *gocui.Gui, v *gocui.View) error { diff --git a/pkg/gui/view_helpers.go b/pkg/gui/view_helpers.go index 9c5d1684b..8ed764c6f 100644 --- a/pkg/gui/view_helpers.go +++ b/pkg/gui/view_helpers.go @@ -13,10 +13,16 @@ import ( var cyclableViews = []string{"status", "files", "branches", "commits", "stash"} func (gui *Gui) refreshSidePanels(g *gocui.Gui) error { - gui.refreshBranches(g) - gui.refreshFiles(g) - gui.refreshCommits(g) - return nil + if err := gui.refreshBranches(g); err != nil { + return err + } + if err := gui.refreshFiles(g); err != nil { + return err + } + if err := gui.refreshCommits(g); err != nil { + return err + } + return gui.refreshStashEntries(g) } func (gui *Gui) nextView(g *gocui.Gui, v *gocui.View) error { @@ -78,9 +84,6 @@ func (gui *Gui) previousView(g *gocui.Gui, v *gocui.View) error { } func (gui *Gui) newLineFocused(g *gocui.Gui, v *gocui.View) error { - mainView, _ := g.View("main") - mainView.SetOrigin(0, 0) - switch v.Name() { case "menu": return gui.handleMenuSelect(g, v) @@ -90,6 +93,10 @@ func (gui *Gui) newLineFocused(g *gocui.Gui, v *gocui.View) error { return gui.handleFileSelect(g, v) case "branches": return gui.handleBranchSelect(g, v) + case "commits": + return gui.handleCommitSelect(g, v) + case "stash": + return gui.handleStashEntrySelect(g, v) case "confirmation": return nil case "commitMessage": @@ -101,10 +108,6 @@ func (gui *Gui) newLineFocused(g *gocui.Gui, v *gocui.View) error { gui.refreshMergePanel(g) v.Highlight = false return nil - case "commits": - return gui.handleCommitSelect(g, v) - case "stash": - return gui.handleStashEntrySelect(g, v) case "staging": return nil // return gui.handleStagingSelect(g, v) @@ -164,62 +167,11 @@ func (gui *Gui) switchFocus(g *gocui.Gui, oldView, newView *gocui.View) error { g.Cursor = newView.Editable - return gui.newLineFocused(g, newView) -} - -func (gui *Gui) getItemPosition(v *gocui.View) int { - gui.correctCursor(v) - _, cy := v.Cursor() - _, oy := v.Origin() - return oy + cy -} - -func (gui *Gui) cursorUp(g *gocui.Gui, v *gocui.View) error { - // swallowing cursor movements in main - if v == nil || v.Name() == "main" { - return nil - } - - ox, oy := v.Origin() - cx, cy := v.Cursor() - if err := v.SetCursor(cx, cy-1); err != nil && oy > 0 { - if err := v.SetOrigin(ox, oy-1); err != nil { - return err - } - } - - gui.newLineFocused(g, v) - return nil -} - -func (gui *Gui) cursorDown(g *gocui.Gui, v *gocui.View) error { - // swallowing cursor movements in main - if v == nil || v.Name() == "main" { - return nil - } - cx, cy := v.Cursor() - ox, oy := v.Origin() - ly := v.LinesHeight() - 1 - _, height := v.Size() - maxY := height - 1 - - // if we are at the end we just return - if cy+oy == ly { - return nil - } - - var err error - if cy < maxY { - err = v.SetCursor(cx, cy+1) - } else { - err = v.SetOrigin(ox, oy+1) - } - if err != nil { + if err := gui.renderPanelOptions(); err != nil { return err } - gui.newLineFocused(g, v) - return nil + return gui.newLineFocused(g, newView) } func (gui *Gui) resetOrigin(v *gocui.View) error { @@ -230,21 +182,42 @@ func (gui *Gui) resetOrigin(v *gocui.View) error { } // if the cursor down past the last item, move it to the last line -func (gui *Gui) correctCursor(v *gocui.View) error { - cx, cy := v.Cursor() - ox, oy := v.Origin() - _, height := v.Size() - maxY := height - 1 - ly := v.LinesHeight() - 1 - if oy+cy <= ly { +func (gui *Gui) focusPoint(cx int, cy int, v *gocui.View) error { + if cy < 0 { return nil } - newCy := utils.Min(ly, maxY) - if err := v.SetCursor(cx, newCy); err != nil { - return err - } - if err := v.SetOrigin(ox, ly-newCy); err != nil { - return err + ox, oy := v.Origin() + _, height := v.Size() + ly := height - 1 + + // if line is above origin, move origin and set cursor to zero + // if line is below origin + height, move origin and set cursor to max + // otherwise set cursor to value - origin + if ly > v.LinesHeight() { + if err := v.SetCursor(cx, cy); err != nil { + return err + } + if err := v.SetOrigin(ox, 0); err != nil { + return err + } + } else if cy < oy { + if err := v.SetCursor(cx, 0); err != nil { + return err + } + if err := v.SetOrigin(ox, cy); err != nil { + return err + } + } else if cy > oy+ly { + if err := v.SetCursor(cx, ly); err != nil { + return err + } + if err := v.SetOrigin(ox, cy-ly); err != nil { + return err + } + } else { + if err := v.SetCursor(cx, cy-oy); err != nil { + return err + } } return nil } @@ -258,6 +231,9 @@ func (gui *Gui) renderString(g *gocui.Gui, viewName, s string) error { return nil } v.Clear() + if err := v.SetOrigin(0, 0); err != nil { + return err + } output := string(bom.Clean([]byte(s))) output = utils.NormalizeLinefeeds(output) fmt.Fprint(v, output) @@ -275,8 +251,8 @@ func (gui *Gui) optionsMapToString(optionsMap map[string]string) string { return strings.Join(optionsArray, ", ") } -func (gui *Gui) renderOptionsMap(g *gocui.Gui, optionsMap map[string]string) error { - return gui.renderString(g, "options", gui.optionsMapToString(optionsMap)) +func (gui *Gui) renderOptionsMap(optionsMap map[string]string) error { + return gui.renderString(gui.g, "options", gui.optionsMapToString(optionsMap)) } // TODO: refactor properly @@ -306,6 +282,16 @@ func (gui *Gui) getStagingView(g *gocui.Gui) *gocui.View { return v } +func (gui *Gui) getMainView(g *gocui.Gui) *gocui.View { + v, _ := g.View("main") + return v +} + +func (gui *Gui) getStashView(g *gocui.Gui) *gocui.View { + v, _ := g.View("stash") + return v +} + func (gui *Gui) trimmedContent(v *gocui.View) string { return strings.TrimSpace(v.Buffer()) } @@ -336,3 +322,68 @@ func (gui *Gui) resizePopupPanel(g *gocui.Gui, v *gocui.View) error { _, err := g.SetView(v.Name(), x0, y0, x1, y1, 0) return err } + +// generalFocusLine takes a lineNumber to focus, and a bottomLine to ensure we can see +func (gui *Gui) generalFocusLine(lineNumber int, bottomLine int, v *gocui.View) error { + _, height := v.Size() + overScroll := bottomLine - height + 1 + if overScroll < 0 { + overScroll = 0 + } + if err := v.SetOrigin(0, overScroll); err != nil { + return err + } + if err := v.SetCursor(0, lineNumber-overScroll); err != nil { + return err + } + return nil +} + +func (gui *Gui) changeSelectedLine(line *int, total int, up bool) { + if up { + if *line == -1 || *line == 0 { + return + } + + *line -= 1 + } else { + if *line == -1 || *line == total-1 { + return + } + + *line += 1 + } +} + +func (gui *Gui) refreshSelectedLine(line *int, total int) { + if *line == -1 && total > 0 { + *line = 0 + } else if total-1 < *line { + *line = total - 1 + } +} + +func (gui *Gui) renderListPanel(v *gocui.View, items interface{}) error { + gui.g.Update(func(g *gocui.Gui) error { + list, err := utils.RenderList(items) + if err != nil { + return gui.createErrorPanel(gui.g, err.Error()) + } + v.Clear() + fmt.Fprint(v, list) + return nil + }) + return nil +} + +func (gui *Gui) renderPanelOptions() error { + currentView := gui.g.CurrentView() + switch currentView.Name() { + case "menu": + return gui.renderMenuOptions() + case "main": + return gui.renderMergeOptions() + default: + return gui.renderGlobalOptions() + } +} diff --git a/pkg/i18n/dutch.go b/pkg/i18n/dutch.go index f62b60b90..bd5a30e6e 100644 --- a/pkg/i18n/dutch.go +++ b/pkg/i18n/dutch.go @@ -150,7 +150,7 @@ func addDutch(i18nObject *i18n.Bundle) error { Other: "Dit bestand heeft geen merge conflicten", }, &i18n.Message{ ID: "SureResetHardHead", - Other: "Weet je het zeker dat je `reset --hard HEAD` wil uitvoeren? Het kan dat je hierdoor bestanden verliest", + Other: "Weet je het zeker dat je `reset --hard HEAD` en `clean -fd` wil uitvoeren? Het kan dat je hierdoor bestanden verliest", }, &i18n.Message{ ID: "SureTo", Other: "Weet je het zeker dat je {{.fileName}} wilt {{.deleteVerb}} (je veranderingen zullen worden verwijderd)", @@ -396,7 +396,7 @@ func addDutch(i18nObject *i18n.Bundle) error { Other: `refresh bestanden`, }, &i18n.Message{ ID: "resetHard", - Other: `harde reset`, + Other: `harde reset and verwijderen ongevolgde bestanden`, }, &i18n.Message{ ID: "mergeIntoCurrentBranch", Other: `merge in met huidige checked out branch`, diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index 451b7bc0e..5806c8c03 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -158,7 +158,7 @@ func addEnglish(i18nObject *i18n.Bundle) error { Other: "This file has no merge conflicts", }, &i18n.Message{ ID: "SureResetHardHead", - Other: "Are you sure you want `reset --hard HEAD`? You may lose changes", + Other: "Are you sure you want `reset --hard HEAD` and `clean -fd`? You may lose changes", }, &i18n.Message{ ID: "SureTo", Other: "Are you sure you want to {{.deleteVerb}} {{.fileName}} (you will lose your changes)?", @@ -404,7 +404,7 @@ func addEnglish(i18nObject *i18n.Bundle) error { Other: `refresh files`, }, &i18n.Message{ ID: "resetHard", - Other: `reset hard`, + Other: `reset hard and remove untracked files`, }, &i18n.Message{ ID: "mergeIntoCurrentBranch", Other: `merge into currently checked out branch`, @@ -456,6 +456,12 @@ func addEnglish(i18nObject *i18n.Bundle) error { }, &i18n.Message{ ID: "CantFindHunk", Other: `Could not find hunk`, + }, &i18n.Message{ + ID: "FastForward", + Other: `fast-forward this branch from its upstream`, + }, &i18n.Message{ + ID: "Fetching", + Other: "fetching and fast-forwarding {{.from}} -> {{.to}} ...", }, ) } diff --git a/pkg/i18n/polish.go b/pkg/i18n/polish.go index 8470045e0..c8ff0c186 100644 --- a/pkg/i18n/polish.go +++ b/pkg/i18n/polish.go @@ -139,7 +139,7 @@ func addPolish(i18nObject *i18n.Bundle) error { Other: "Ten plik nie powoduje konfliktów scalania", }, &i18n.Message{ ID: "SureResetHardHead", - Other: "Jesteś pewny, że chcesz wykonać `reset --hard HEAD`? Możesz stracić wprowadzone zmiany", + Other: "Jesteś pewny, że chcesz wykonać `reset --hard HEAD` i `clean -fd`? Możesz stracić wprowadzone zmiany", }, &i18n.Message{ ID: "SureTo", Other: "Jesteś pewny, że chcesz {{.deleteVerb}} {{.fileName}} (stracisz swoje wprowadzone zmiany)?", @@ -382,7 +382,7 @@ func addPolish(i18nObject *i18n.Bundle) error { Other: `odśwież pliki`, }, &i18n.Message{ ID: "resetHard", - Other: `zresetuj twardo`, + Other: `zresetuj twardo i usuń niepotwierdzone pliki`, }, &i18n.Message{ ID: "mergeIntoCurrentBranch", Other: `scal do obecnej gałęzi`, diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index e2a5337e3..390f85f70 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -1,6 +1,7 @@ package utils import ( + "encoding/json" "errors" "fmt" "log" @@ -235,3 +236,8 @@ func PrevIndex(numbers []int, currentNumber int) int { } return end } + +func AsJson(i interface{}) string { + bytes, _ := json.MarshalIndent(i, "", " ") + return string(bytes) +} diff --git a/pkg/utils/utils_test.go b/pkg/utils/utils_test.go index f03bc087a..f7545f5e9 100644 --- a/pkg/utils/utils_test.go +++ b/pkg/utils/utils_test.go @@ -517,3 +517,14 @@ func TestPrevIndex(t *testing.T) { }) } } + +func TestAsJson(t *testing.T) { + type myStruct struct { + a string + } + + output := AsJson(&myStruct{a: "foo"}) + + // no idea why this is returning empty hashes but it's works in the app ¯\_(ツ)_/¯ + assert.EqualValues(t, "{}", output) +}