From 66512ca253b73c173196080ea0929e49a521b379 Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Wed, 5 Dec 2018 19:49:07 +1100 Subject: [PATCH 1/5] use porcelain git rather than go-git for reset --hard HEAD because go-git takes over 5 seconds --- pkg/commands/git.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/commands/git.go b/pkg/commands/git.go index 1d8e5a10d..e06b36d2e 100644 --- a/pkg/commands/git.go +++ b/pkg/commands/git.go @@ -208,9 +208,9 @@ func includesInt(list []int, a int) bool { return false } -// ResetHard does the equivalent of `git reset --hard HEAD` +// ResetHard clears the working tree to match HEAD func (c *GitCommand) ResetHard() error { - return c.Worktree.Reset(&gogit.ResetOptions{Mode: gogit.HardReset}) + return c.OSCommand.RunCommand("git reset --hard HEAD") } // UpstreamDifferenceCount checks how many pushables/pullables there are for the From 3b51d7cd0026f0b4da13bf6ffcd4635b86c2a704 Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Wed, 5 Dec 2018 20:06:47 +1100 Subject: [PATCH 2/5] clean as well as reset (I'm hoping this is a good design decision) --- pkg/commands/git.go | 10 +++++++--- pkg/gui/files_panel.go | 4 ++-- pkg/gui/keybindings.go | 2 +- pkg/i18n/dutch.go | 4 ++-- pkg/i18n/english.go | 4 ++-- pkg/i18n/polish.go | 4 ++-- 6 files changed, 16 insertions(+), 12 deletions(-) diff --git a/pkg/commands/git.go b/pkg/commands/git.go index e06b36d2e..7d820c599 100644 --- a/pkg/commands/git.go +++ b/pkg/commands/git.go @@ -208,9 +208,13 @@ func includesInt(list []int, a int) bool { return false } -// ResetHard clears the working tree to match HEAD -func (c *GitCommand) ResetHard() error { - return c.OSCommand.RunCommand("git reset --hard HEAD") +// 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 diff --git a/pkg/gui/files_panel.go b/pkg/gui/files_panel.go index a16ef4909..5eb8f63f1 100644 --- a/pkg/gui/files_panel.go +++ b/pkg/gui/files_panel.go @@ -433,9 +433,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/keybindings.go b/pkg/gui/keybindings.go index 4158bedb7..1a63d009c 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", diff --git a/pkg/i18n/dutch.go b/pkg/i18n/dutch.go index f49457ec4..f63b83477 100644 --- a/pkg/i18n/dutch.go +++ b/pkg/i18n/dutch.go @@ -138,7 +138,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)", @@ -384,7 +384,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 a3ce14d2e..97b6addf6 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -146,7 +146,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)?", @@ -392,7 +392,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`, diff --git a/pkg/i18n/polish.go b/pkg/i18n/polish.go index 3e1ce657d..52e110b37 100644 --- a/pkg/i18n/polish.go +++ b/pkg/i18n/polish.go @@ -127,7 +127,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)?", @@ -370,7 +370,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`, From 99a8b1ae8bb93565df8acbe168749278dc768979 Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Tue, 4 Dec 2018 19:50:11 +1100 Subject: [PATCH 3/5] making a start on unidirectional data binding to fix these UI bugs --- pkg/app/app.go | 2 + pkg/commands/git.go | 11 ++++ pkg/gui/branches_panel.go | 42 ++++++++++----- pkg/gui/commits_panel.go | 77 +++++++++++++++------------- pkg/gui/files_panel.go | 65 +++++++++++++---------- pkg/gui/gui.go | 71 +++++++++++++++++++++----- pkg/gui/keybindings.go | 36 ++++++++++--- pkg/gui/staging_panel.go | 62 +++++++++------------- pkg/gui/stash_panel.go | 7 --- pkg/gui/status_panel.go | 9 +--- pkg/gui/view_helpers.go | 105 ++++++++++++++++++++++++++++++++++---- pkg/utils/utils.go | 6 +++ 12 files changed, 333 insertions(+), 160 deletions(-) diff --git a/pkg/app/app.go b/pkg/app/app.go index 65acd2e35..b43f9568c 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -52,6 +52,8 @@ func newLogger(config config.AppConfigurer) *logrus.Entry { } else { log = newProductionLogger(config) } + 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/git.go b/pkg/commands/git.go index 7d820c599..e185b0fbd 100644 --- a/pkg/commands/git.go +++ b/pkg/commands/git.go @@ -130,6 +130,17 @@ func (c *GitCommand) GetStashEntryDiff(index int) (string, error) { // GetStatusFiles git status files func (c *GitCommand) GetStatusFiles() []*File { + + // files := []*File{} + // for i := 0; i < 100; i++ { + // files = append(files, &File{ + // Name: strconv.Itoa(i), + // DisplayString: strconv.Itoa(i), + // Type: "file", + // }) + // } + // return files + statusOutput, _ := c.GitStatus() statusStrings := utils.SplitLines(statusOutput) files := []*File{} diff --git a/pkg/gui/branches_panel.go b/pkg/gui/branches_panel.go index dbf4b007a..e1504bd69 100644 --- a/pkg/gui/branches_panel.go +++ b/pkg/gui/branches_panel.go @@ -131,30 +131,30 @@ func (gui *Gui) handleMerge(g *gocui.Gui, v *gocui.View) error { } func (gui *Gui) getSelectedBranch(v *gocui.View) *commands.Branch { - lineNumber := gui.getItemPosition(v) - return gui.State.Branches[lineNumber] -} + selectedLine := gui.State.Panels.Branches.SelectedLine + if selectedLine == -1 { + return nil + } -func (gui *Gui) renderBranchesOptions(g *gocui.Gui) error { - return gui.renderGlobalOptions(g) + 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 { - if err := gui.renderBranchesOptions(g); err != nil { - return err - } // 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(v) + if err := gui.focusPoint(0, gui.State.Panels.Branches.SelectedLine, v); err != nil { + return err + } 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") + 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", diff) + _ = gui.renderString(g, "main", graph) }() return nil } @@ -173,6 +173,8 @@ func (gui *Gui) refreshBranches(g *gocui.Gui) error { } gui.State.Branches = builder.Build() + gui.refreshSelectedLine(&gui.State.Panels.Branches.SelectedLine, len(gui.State.Branches)) + v.Clear() list, err := utils.RenderList(gui.State.Branches) if err != nil { @@ -186,3 +188,17 @@ func (gui *Gui) refreshBranches(g *gocui.Gui) error { }) 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) +} diff --git a/pkg/gui/commits_panel.go b/pkg/gui/commits_panel.go index ee7f191a7..be291091c 100644 --- a/pkg/gui/commits_panel.go +++ b/pkg/gui/commits_panel.go @@ -22,6 +22,8 @@ func (gui *Gui) refreshCommits(g *gocui.Gui) error { return err } + gui.refreshSelectedLine(&gui.State.Panels.Commits.SelectedLine, len(gui.State.Commits)) + v.Clear() list, err := utils.RenderList(gui.State.Commits) @@ -41,9 +43,9 @@ func (gui *Gui) refreshCommits(g *gocui.Gui) error { 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()) @@ -59,21 +61,15 @@ func (gui *Gui) handleResetToCommit(g *gocui.Gui, commitView *gocui.View) error }, 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 - } + 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 @@ -85,12 +81,12 @@ func (gui *Gui) handleCommitSquashDown(g *gocui.Gui, v *gocui.View) error { if gui.getItemPosition(v) != 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 +109,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 { @@ -165,18 +161,27 @@ 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) +func (gui *Gui) getSelectedCommit(g *gocui.Gui) *commands.Commit { + selectedLine := gui.State.Panels.Commits.SelectedLine + if selectedLine == -1 { + return nil } - 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 + + return gui.State.Commits[selectedLine] +} + +func (gui *Gui) handleCommitsNextLine(g *gocui.Gui, v *gocui.View) error { + gui.Log.Info(utils.AsJson(gui.State.Panels)) + 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 { + gui.Log.Info(utils.AsJson(gui.State.Panels)) + panelState := gui.State.Panels.Commits + gui.changeSelectedLine(&panelState.SelectedLine, len(gui.State.Commits), true) + + return gui.handleCommitSelect(gui.g, v) } diff --git a/pkg/gui/files_panel.go b/pkg/gui/files_panel.go index 5eb8f63f1..13ce48d10 100644 --- a/pkg/gui/files_panel.go +++ b/pkg/gui/files_panel.go @@ -140,15 +140,12 @@ func (gui *Gui) handleAddPatch(g *gocui.Gui, v *gocui.View) error { } func (gui *Gui) getSelectedFile(g *gocui.Gui) (*commands.File, error) { - if len(gui.State.Files) == 0 { + selectedLine := gui.State.Panels.Files.SelectedLine + if selectedLine == -1 { 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 + + return gui.State.Files[selectedLine], nil } func (gui *Gui) handleFileRemove(g *gocui.Gui, v *gocui.View) error { @@ -194,26 +191,23 @@ 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 + 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) } @@ -309,6 +303,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,6 +335,20 @@ func (gui *Gui) catSelectedFile(g *gocui.Gui) (string, error) { return cat, 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) +} + func (gui *Gui) refreshFiles(g *gocui.Gui) error { filesView, err := g.View("files") if err != nil { @@ -347,17 +356,21 @@ func (gui *Gui) refreshFiles(g *gocui.Gui) error { } gui.refreshStateFiles() - filesView.Clear() - list, err := utils.RenderList(gui.State.Files) - if err != nil { - return err - } - fmt.Fprint(filesView, list) + 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() { + gui.handleFileSelect(g, filesView) + } + return nil + }) - gui.correctCursor(filesView) - if filesView == g.CurrentView() { - gui.handleFileSelect(g, filesView) - } return nil } diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index 9f25121d5..1af3a5e1f 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -72,11 +72,35 @@ type Gui struct { statusManager *statusManager } -type stagingState struct { - StageableLines []int - HunkStarts []int - CurrentLineIndex int - Diff string +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 panelStates struct { + Files *filePanelState + Staging *stagingPanelState + Branches *branchPanelState + Commits *commitPanelState + Stash *stashPanelState } type guiState struct { @@ -92,7 +116,7 @@ type guiState struct { EditHistory *stack.Stack Platform commands.Platform Updating bool - StagingState *stagingState + Panels *panelStates } // NewGui builds a new gui handler @@ -108,6 +132,12 @@ 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}, + }, } gui := &Gui{ @@ -193,9 +223,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") @@ -247,12 +279,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 { @@ -325,11 +358,14 @@ func (gui *Gui) layout(g *gocui.Gui) error { return err } - gui.handleFileSelect(g, filesView) + gui.g.SetCurrentView(filesView.Name()) gui.refreshFiles(g) gui.refreshBranches(g) gui.refreshCommits(g) gui.refreshStashEntries(g) + if err := gui.renderGlobalOptions(g); err != nil { + return err + } if err := gui.switchFocus(g, nil, filesView); err != nil { return err } @@ -341,6 +377,17 @@ 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 + } + } + return gui.resizeCurrentPopupPanel(g) } @@ -411,7 +458,7 @@ func (gui *Gui) Run() error { } gui.goEvery(g, time.Second*60, gui.fetch) - gui.goEvery(g, time.Second*10, gui.refreshFiles) + // gui.goEvery(g, time.Second*2, gui.refreshFiles) // TODO: comment back in gui.goEvery(g, time.Millisecond*50, gui.updateLoader) gui.goEvery(g, time.Millisecond*50, gui.renderAppStatus) diff --git a/pkg/gui/keybindings.go b/pkg/gui/keybindings.go index 1a63d009c..c8c3d642f 100644 --- a/pkg/gui/keybindings.go +++ b/pkg/gui/keybindings.go @@ -219,7 +219,12 @@ func (gui *Gui) GetKeybindings() []*Binding { Handler: gui.handleSwitchToStagingPanel, Description: gui.Tr.SLocalize("StageLines"), KeyReadable: "enter", - }, { + }, + {ViewName: "files", Key: 'k', Modifier: gocui.ModNone, Handler: gui.handleFilesPrevLine}, + {ViewName: "files", Key: gocui.KeyArrowUp, Modifier: gocui.ModNone, Handler: gui.handleFilesPrevLine}, + {ViewName: "files", Key: 'j', Modifier: gocui.ModNone, Handler: gui.handleFilesNextLine}, + {ViewName: "files", Key: gocui.KeyArrowDown, Modifier: gocui.ModNone, Handler: gui.handleFilesNextLine}, + { ViewName: "main", Key: gocui.KeyEsc, Modifier: gocui.ModNone, @@ -322,7 +327,12 @@ func (gui *Gui) GetKeybindings() []*Binding { Modifier: gocui.ModNone, Handler: gui.handleMerge, Description: gui.Tr.SLocalize("mergeIntoCurrentBranch"), - }, { + }, + {ViewName: "branches", Key: 'k', Modifier: gocui.ModNone, Handler: gui.handleBranchesPrevLine}, + {ViewName: "branches", Key: gocui.KeyArrowUp, Modifier: gocui.ModNone, Handler: gui.handleBranchesPrevLine}, + {ViewName: "branches", Key: 'j', Modifier: gocui.ModNone, Handler: gui.handleBranchesNextLine}, + {ViewName: "branches", Key: gocui.KeyArrowDown, Modifier: gocui.ModNone, Handler: gui.handleBranchesNextLine}, + { ViewName: "commits", Key: 's', Modifier: gocui.ModNone, @@ -352,7 +362,12 @@ func (gui *Gui) GetKeybindings() []*Binding { Modifier: gocui.ModNone, Handler: gui.handleCommitFixup, Description: gui.Tr.SLocalize("fixupCommit"), - }, { + }, + {ViewName: "commits", Key: 'k', Modifier: gocui.ModNone, Handler: gui.handleCommitsPrevLine}, + {ViewName: "commits", Key: gocui.KeyArrowUp, Modifier: gocui.ModNone, Handler: gui.handleCommitsPrevLine}, + {ViewName: "commits", Key: 'j', Modifier: gocui.ModNone, Handler: gui.handleCommitsNextLine}, + {ViewName: "commits", Key: gocui.KeyArrowDown, Modifier: gocui.ModNone, Handler: gui.handleCommitsNextLine}, + { ViewName: "stash", Key: gocui.KeySpace, Modifier: gocui.ModNone, @@ -455,17 +470,22 @@ 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", "commits", "stash", "menu"} { + bindings = append(bindings, []*Binding{ + {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: 'k', Modifier: gocui.ModNone, Handler: gui.cursorUp}, + {ViewName: viewName, Key: 'j', Modifier: gocui.ModNone, Handler: gui.cursorDown}, + }...) + } + + 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}, }...) } 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..196a33a08 100644 --- a/pkg/gui/stash_panel.go +++ b/pkg/gui/stash_panel.go @@ -37,14 +37,7 @@ func (gui *Gui) getSelectedStashEntry(v *gocui.View) *commands.StashEntry { return gui.State.StashEntries[lineNumber] } -func (gui *Gui) renderStashOptions(g *gocui.Gui) error { - return gui.renderGlobalOptions(g) -} - 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 { diff --git a/pkg/gui/status_panel.go b/pkg/gui/status_panel.go index aeae19c50..15f3f27d1 100644 --- a/pkg/gui/status_panel.go +++ b/pkg/gui/status_panel.go @@ -42,10 +42,6 @@ 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")) @@ -63,10 +59,7 @@ func (gui *Gui) handleStatusSelect(g *gocui.Gui, v *gocui.View) error { "Buy Jesse a coffee: https://donorbox.org/lazygit", }, "\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 525b05f97..4c5002efe 100644 --- a/pkg/gui/view_helpers.go +++ b/pkg/gui/view_helpers.go @@ -230,19 +230,46 @@ 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 { + return gui.focusPoint(cx, cy, v) +} + +// if the cursor down past the last item, move it to the last line +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 } @@ -334,3 +361,59 @@ func (gui *Gui) resizePopupPanel(g *gocui.Gui, v *gocui.View) error { _, err := g.SetView(v.Name(), x0, y0, x1, y1, 0) return err } + +// focusLine focuses and selects the given line +func (gui *Gui) focusLine(lineNumber int, v *gocui.View) error { + _, height := v.Size() + overScroll := lineNumber - 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 +} + +// 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 + } +} 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) +} From ca3afa2a3905365ec79624e0c9b08865e9ab27ae Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Thu, 6 Dec 2018 22:18:17 +1100 Subject: [PATCH 4/5] standardising how list panels deal with cursor movement --- pkg/gui/branches_panel.go | 253 +++++++++++++++++--------------- pkg/gui/commit_message_panel.go | 4 +- pkg/gui/commits_panel.go | 102 +++++++------ pkg/gui/files_panel.go | 145 +++++++++--------- pkg/gui/gui.go | 21 ++- pkg/gui/keybindings.go | 24 ++- pkg/gui/menu_panel.go | 31 +++- pkg/gui/stash_panel.go | 67 ++++++--- pkg/gui/view_helpers.go | 88 +++-------- 9 files changed, 373 insertions(+), 362 deletions(-) diff --git a/pkg/gui/branches_panel.go b/pkg/gui/branches_panel.go index e1504bd69..809deaa7d 100644 --- a/pkg/gui/branches_panel.go +++ b/pkg/gui/branches_panel.go @@ -10,127 +10,9 @@ import ( "github.com/jesseduffield/lazygit/pkg/utils" ) -func (gui *Gui) handleBranchPress(g *gocui.Gui, v *gocui.View) error { - index := gui.getItemPosition(gui.getBranchesView(g)) - if index == 0 { - return gui.createErrorPanel(g, gui.Tr.SLocalize("AlreadyCheckedOutBranch")) - } - branch := gui.getSelectedBranch(gui.getBranchesView(g)) - if err := gui.GitCommand.Checkout(branch.Name, false); err != nil { - gui.createErrorPanel(g, err.Error()) - } - return gui.refreshSidePanels(g) -} +// list panel functions -func (gui *Gui) handleCreatePullRequestPress(g *gocui.Gui, v *gocui.View) error { - branch := gui.getSelectedBranch(gui.getBranchesView(g)) - pullRequest := commands.NewPullRequest(gui.GitCommand) - - if err := pullRequest.Create(branch); err != nil { - return gui.createErrorPanel(g, err.Error()) - } - - return nil -} - -func (gui *Gui) handleForceCheckout(g *gocui.Gui, v *gocui.View) error { - branch := gui.getSelectedBranch(v) - 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 { - if err := gui.GitCommand.Checkout(branch.Name, true); err != nil { - gui.createErrorPanel(g, err.Error()) - } - return gui.refreshSidePanels(g) - }, nil) -} - -func (gui *Gui) handleCheckoutByName(g *gocui.Gui, v *gocui.View) error { - gui.createPromptPanel(g, v, gui.Tr.SLocalize("BranchName")+":", func(g *gocui.Gui, v *gocui.View) error { - if err := gui.GitCommand.Checkout(gui.trimmedContent(v), false); err != nil { - return gui.createErrorPanel(g, err.Error()) - } - return gui.refreshSidePanels(g) - }) - return nil -} - -func (gui *Gui) handleNewBranch(g *gocui.Gui, v *gocui.View) error { - branch := gui.State.Branches[0] - message := gui.Tr.TemplateLocalize( - "NewBranchNameBranchOff", - Teml{ - "branchName": branch.Name, - }, - ) - gui.createPromptPanel(g, v, message, func(g *gocui.Gui, v *gocui.View) error { - if err := gui.GitCommand.NewBranch(gui.trimmedContent(v)); err != nil { - return gui.createErrorPanel(g, err.Error()) - } - gui.refreshSidePanels(g) - return gui.handleBranchSelect(g, v) - }) - return nil -} - -func (gui *Gui) handleDeleteBranch(g *gocui.Gui, v *gocui.View) error { - return gui.deleteBranch(g, v, false) -} - -func (gui *Gui) handleForceDeleteBranch(g *gocui.Gui, v *gocui.View) error { - return gui.deleteBranch(g, v, true) -} - -func (gui *Gui) deleteBranch(g *gocui.Gui, v *gocui.View, force bool) error { - checkedOutBranch := gui.State.Branches[0] - selectedBranch := gui.getSelectedBranch(v) - if checkedOutBranch.Name == selectedBranch.Name { - return gui.createErrorPanel(g, gui.Tr.SLocalize("CantDeleteCheckOutBranch")) - } - return gui.deleteNamedBranch(g, v, selectedBranch, force) -} - -func (gui *Gui) deleteNamedBranch(g *gocui.Gui, v *gocui.View, selectedBranch *commands.Branch, force bool) error { - title := gui.Tr.SLocalize("DeleteBranch") - var messageId string - if force { - messageId = "ForceDeleteBranchMessage" - } else { - messageId = "DeleteBranchMessage" - } - message := gui.Tr.TemplateLocalize( - messageId, - Teml{ - "selectedBranchName": selectedBranch.Name, - }, - ) - return gui.createConfirmationPanel(g, v, title, message, func(g *gocui.Gui, v *gocui.View) error { - if err := gui.GitCommand.DeleteBranch(selectedBranch.Name, force); err != nil { - errMessage := err.Error() - if !force && strings.Contains(errMessage, "is not fully merged") { - return gui.deleteNamedBranch(g, v, selectedBranch, true) - } else { - return gui.createErrorPanel(g, errMessage) - } - } - return gui.refreshSidePanels(g) - }, nil) -} - -func (gui *Gui) handleMerge(g *gocui.Gui, v *gocui.View) error { - checkedOutBranch := gui.State.Branches[0] - selectedBranch := gui.getSelectedBranch(v) - defer gui.refreshSidePanels(g) - if checkedOutBranch.Name == selectedBranch.Name { - return gui.createErrorPanel(g, gui.Tr.SLocalize("CantMergeBranchIntoItself")) - } - if err := gui.GitCommand.Merge(selectedBranch.Name); err != nil { - return gui.createErrorPanel(g, err.Error()) - } - return nil -} - -func (gui *Gui) getSelectedBranch(v *gocui.View) *commands.Branch { +func (gui *Gui) getSelectedBranch() *commands.Branch { selectedLine := gui.State.Panels.Branches.SelectedLine if selectedLine == -1 { return nil @@ -145,7 +27,7 @@ func (gui *Gui) handleBranchSelect(g *gocui.Gui, v *gocui.View) error { if len(gui.State.Branches) == 0 { return gui.renderString(g, "main", gui.Tr.SLocalize("NoBranchesThisRepo")) } - branch := gui.getSelectedBranch(v) + branch := gui.getSelectedBranch() if err := gui.focusPoint(0, gui.State.Panels.Branches.SelectedLine, v); err != nil { return err } @@ -202,3 +84,132 @@ func (gui *Gui) handleBranchesPrevLine(g *gocui.Gui, v *gocui.View) error { return gui.handleBranchSelect(gui.g, v) } + +// specific functions + +func (gui *Gui) handleBranchPress(g *gocui.Gui, v *gocui.View) error { + 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() + if err := gui.GitCommand.Checkout(branch.Name, false); err != nil { + if err := gui.createErrorPanel(g, err.Error()); err != nil { + return err + } + } + return gui.refreshSidePanels(g) +} + +func (gui *Gui) handleCreatePullRequestPress(g *gocui.Gui, v *gocui.View) error { + pullRequest := commands.NewPullRequest(gui.GitCommand) + + branch := gui.getSelectedBranch() + if err := pullRequest.Create(branch); err != nil { + return gui.createErrorPanel(g, err.Error()) + } + + return nil +} + +func (gui *Gui) handleForceCheckout(g *gocui.Gui, v *gocui.View) error { + 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 { + if err := gui.GitCommand.Checkout(branch.Name, true); err != nil { + gui.createErrorPanel(g, err.Error()) + } + return gui.refreshSidePanels(g) + }, nil) +} + +func (gui *Gui) handleCheckoutByName(g *gocui.Gui, v *gocui.View) error { + gui.createPromptPanel(g, v, gui.Tr.SLocalize("BranchName")+":", func(g *gocui.Gui, v *gocui.View) error { + if err := gui.GitCommand.Checkout(gui.trimmedContent(v), false); err != nil { + return gui.createErrorPanel(g, err.Error()) + } + return gui.refreshSidePanels(g) + }) + return nil +} + +func (gui *Gui) handleNewBranch(g *gocui.Gui, v *gocui.View) error { + branch := gui.State.Branches[0] + message := gui.Tr.TemplateLocalize( + "NewBranchNameBranchOff", + Teml{ + "branchName": branch.Name, + }, + ) + gui.createPromptPanel(g, v, message, func(g *gocui.Gui, v *gocui.View) error { + if err := gui.GitCommand.NewBranch(gui.trimmedContent(v)); err != nil { + return gui.createErrorPanel(g, err.Error()) + } + gui.refreshSidePanels(g) + return gui.handleBranchSelect(g, v) + }) + return nil +} + +func (gui *Gui) handleDeleteBranch(g *gocui.Gui, v *gocui.View) error { + return gui.deleteBranch(g, v, false) +} + +func (gui *Gui) handleForceDeleteBranch(g *gocui.Gui, v *gocui.View) error { + return gui.deleteBranch(g, v, true) +} + +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] + if checkedOutBranch.Name == selectedBranch.Name { + return gui.createErrorPanel(g, gui.Tr.SLocalize("CantDeleteCheckOutBranch")) + } + return gui.deleteNamedBranch(g, v, selectedBranch, force) +} + +func (gui *Gui) deleteNamedBranch(g *gocui.Gui, v *gocui.View, selectedBranch *commands.Branch, force bool) error { + title := gui.Tr.SLocalize("DeleteBranch") + var messageId string + if force { + messageId = "ForceDeleteBranchMessage" + } else { + messageId = "DeleteBranchMessage" + } + message := gui.Tr.TemplateLocalize( + messageId, + Teml{ + "selectedBranchName": selectedBranch.Name, + }, + ) + return gui.createConfirmationPanel(g, v, title, message, func(g *gocui.Gui, v *gocui.View) error { + if err := gui.GitCommand.DeleteBranch(selectedBranch.Name, force); err != nil { + errMessage := err.Error() + if !force && strings.Contains(errMessage, "is not fully merged") { + return gui.deleteNamedBranch(g, v, selectedBranch, true) + } else { + return gui.createErrorPanel(g, errMessage) + } + } + return gui.refreshSidePanels(g) + }, nil) +} + +func (gui *Gui) handleMerge(g *gocui.Gui, v *gocui.View) error { + checkedOutBranch := gui.State.Branches[0] + selectedBranch := gui.getSelectedBranch() + defer gui.refreshSidePanels(g) + if checkedOutBranch.Name == selectedBranch.Name { + return gui.createErrorPanel(g, gui.Tr.SLocalize("CantMergeBranchIntoItself")) + } + if err := gui.GitCommand.Merge(selectedBranch.Name); err != nil { + return gui.createErrorPanel(g, err.Error()) + } + return nil +} diff --git a/pkg/gui/commit_message_panel.go b/pkg/gui/commit_message_panel.go index 809442481..60058d48c 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) + v.SetOrigin(0, 0) g.SetViewOnBottom("commitMessage") gui.switchFocus(g, v, gui.getFilesView(g)) - return gui.refreshCommits(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 be291091c..2591fa240 100644 --- a/pkg/gui/commits_panel.go +++ b/pkg/gui/commits_panel.go @@ -9,31 +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 - } gui.refreshSelectedLine(&gui.State.Panels.Commits.SelectedLine, len(gui.State.Commits)) - v.Clear() - 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 @@ -41,6 +64,22 @@ 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 := gui.getSelectedCommit(g) @@ -61,24 +100,8 @@ func (gui *Gui) handleResetToCommit(g *gocui.Gui, commitView *gocui.View) error }, nil) } -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) 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 { @@ -134,7 +157,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 { @@ -149,7 +172,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")) } @@ -160,28 +183,3 @@ func (gui *Gui) handleRenameCommitEditor(g *gocui.Gui, v *gocui.View) error { return nil } - -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) handleCommitsNextLine(g *gocui.Gui, v *gocui.View) error { - gui.Log.Info(utils.AsJson(gui.State.Panels)) - 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 { - gui.Log.Info(utils.AsJson(gui.State.Panels)) - panelState := gui.State.Panels.Commits - gui.changeSelectedLine(&panelState.SelectedLine, len(gui.State.Commits), true) - - return gui.handleCommitSelect(gui.g, v) -} diff --git a/pkg/gui/files_panel.go b/pkg/gui/files_panel.go index 13ce48d10..231e5d488 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() { + 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,15 +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) { - selectedLine := gui.State.Panels.Files.SelectedLine - if selectedLine == -1 { - return &commands.File{}, gui.Errors.ErrNoFiles - } - - return gui.State.Files[selectedLine], nil -} - func (gui *Gui) handleFileRemove(g *gocui.Gui, v *gocui.View) error { file, err := gui.getSelectedFile(g) if err != nil { @@ -191,27 +255,6 @@ func (gui *Gui) handleIgnoreFile(g *gocui.Gui, v *gocui.View) error { return gui.refreshFiles(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 - } - 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) handleCommitPress(g *gocui.Gui, filesView *gocui.View) error { if len(gui.stagedFiles()) == 0 && !gui.State.HasMergeConflicts { return gui.createErrorPanel(g, gui.Tr.SLocalize("NoStagedFilesToCommit")) @@ -335,45 +378,6 @@ func (gui *Gui) catSelectedFile(g *gocui.Gui) (string, error) { return cat, 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) -} - -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() { - gui.handleFileSelect(g, filesView) - } - return nil - }) - - return nil -} - func (gui *Gui) pullFiles(g *gocui.Gui, v *gocui.View) error { gui.createMessagePanel(g, v, "", gui.Tr.SLocalize("PullWait")) go func() { @@ -399,8 +403,7 @@ func (gui *Gui) pushWithForceFlag(currentView *gocui.View, force bool) error { _ = gui.createErrorPanel(gui.g, err.Error()) } else { _ = gui.closeConfirmationPrompt(gui.g) - _ = gui.refreshCommits(gui.g) - _ = gui.refreshStatus(gui.g) + _ = gui.refreshSidePanels(gui.g) } }() return nil diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index 1af3a5e1f..6a7eaaf98 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -95,12 +95,17 @@ 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 { @@ -137,6 +142,7 @@ func NewGui(log *logrus.Entry, gitCommand *commands.GitCommand, oSCommand *comma Branches: &branchPanelState{SelectedLine: 0}, Commits: &commitPanelState{SelectedLine: -1}, Stash: &stashPanelState{SelectedLine: -1}, + Menu: &menuPanelState{SelectedLine: 0}, }, } @@ -359,12 +365,12 @@ func (gui *Gui) layout(g *gocui.Gui) error { } gui.g.SetCurrentView(filesView.Name()) - gui.refreshFiles(g) - gui.refreshBranches(g) - gui.refreshCommits(g) - gui.refreshStashEntries(g) - if err := gui.renderGlobalOptions(g); err != nil { - return err + + gui.refreshSidePanels(gui.g) + if gui.g.CurrentView().Name() != "menu" { + if err := gui.renderGlobalOptions(g); err != nil { + return err + } } if err := gui.switchFocus(g, nil, filesView); err != nil { return err @@ -388,6 +394,9 @@ func (gui *Gui) layout(g *gocui.Gui) error { } } + // TODO: comment-out + gui.Log.Info(utils.AsJson(gui.State)) + return gui.resizeCurrentPopupPanel(g) } diff --git a/pkg/gui/keybindings.go b/pkg/gui/keybindings.go index c8c3d642f..c02074d6f 100644 --- a/pkg/gui/keybindings.go +++ b/pkg/gui/keybindings.go @@ -386,7 +386,12 @@ func (gui *Gui) GetKeybindings() []*Binding { Modifier: gocui.ModNone, Handler: gui.handleStashDrop, Description: gui.Tr.SLocalize("drop"), - }, { + }, + {ViewName: "stash", Key: 'k', Modifier: gocui.ModNone, Handler: gui.handleStashPrevLine}, + {ViewName: "stash", Key: gocui.KeyArrowUp, Modifier: gocui.ModNone, Handler: gui.handleStashPrevLine}, + {ViewName: "stash", Key: 'j', Modifier: gocui.ModNone, Handler: gui.handleStashNextLine}, + {ViewName: "stash", Key: gocui.KeyArrowDown, Modifier: gocui.ModNone, Handler: gui.handleStashNextLine}, + { ViewName: "commitMessage", Key: gocui.KeyEnter, Modifier: gocui.ModNone, @@ -406,7 +411,11 @@ func (gui *Gui) GetKeybindings() []*Binding { Key: 'q', Modifier: gocui.ModNone, Handler: gui.handleMenuClose, - }, { + }, + {ViewName: "menu", Key: 'k', Modifier: gocui.ModNone, Handler: gui.handleMenuPrevLine}, + {ViewName: "menu", Key: gocui.KeyArrowUp, Modifier: gocui.ModNone, Handler: gui.handleMenuPrevLine}, + {ViewName: "menu", Key: 'j', Modifier: gocui.ModNone, Handler: gui.handleMenuNextLine}, + {ViewName: "menu", Key: gocui.KeyArrowDown, Modifier: gocui.ModNone, Handler: gui.handleMenuNextLine}, { ViewName: "staging", Key: gocui.KeyEsc, Modifier: gocui.ModNone, @@ -468,17 +477,6 @@ 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", "commits", "stash", "menu"} { - bindings = append(bindings, []*Binding{ - {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: 'k', Modifier: gocui.ModNone, Handler: gui.cursorUp}, - {ViewName: viewName, Key: 'j', Modifier: gocui.ModNone, Handler: gui.cursorDown}, - }...) - } - 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}, diff --git a/pkg/gui/menu_panel.go b/pkg/gui/menu_panel.go index 753e8f84d..0d3d8cf2b 100644 --- a/pkg/gui/menu_panel.go +++ b/pkg/gui/menu_panel.go @@ -8,12 +8,32 @@ 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) 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) + + if err := gui.focusPoint(0, gui.State.Panels.Commits.SelectedLine, v); err != nil { + return err + } + + return gui.handleMenuSelect(g, v) +} + +// specific functions + func (gui *Gui) renderMenuOptions(g *gocui.Gui) error { optionsMap := map[string]string{ "esc/q": gui.Tr.SLocalize("close"), @@ -46,14 +66,15 @@ func (gui *Gui) createMenu(items interface{}, handlePress func(int) error) error menuView.FgColor = gocui.ColorWhite menuView.Clear() fmt.Fprint(menuView, list) + gui.State.Panels.Menu.SelectedLine = 0 if err := gui.renderMenuOptions(gui.g); err != nil { return err } 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/stash_panel.go b/pkg/gui/stash_panel.go index 196a33a08..ef54ef997 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,28 +55,22 @@ 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) handleStashEntrySelect(g *gocui.Gui, v *gocui.View) error { - 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 +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) } +// specific functions + func (gui *Gui) handleStashApply(g *gocui.Gui, v *gocui.View) error { return gui.stashDo(g, v, "apply") } diff --git a/pkg/gui/view_helpers.go b/pkg/gui/view_helpers.go index 4c5002efe..5b02b1ae9 100644 --- a/pkg/gui/view_helpers.go +++ b/pkg/gui/view_helpers.go @@ -16,6 +16,7 @@ func (gui *Gui) refreshSidePanels(g *gocui.Gui) error { gui.refreshBranches(g) gui.refreshFiles(g) gui.refreshCommits(g) + gui.refreshStashEntries(g) return nil } @@ -78,18 +79,19 @@ 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) + return nil case "status": return gui.handleStatusSelect(g, v) case "files": 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": @@ -99,10 +101,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) @@ -165,61 +163,6 @@ func (gui *Gui) switchFocus(g *gocui.Gui, oldView, newView *gocui.View) error { 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 { - return err - } - - gui.newLineFocused(g, v) - return nil -} - func (gui *Gui) resetOrigin(v *gocui.View) error { if err := v.SetCursor(0, 0); err != nil { return err @@ -227,12 +170,6 @@ func (gui *Gui) resetOrigin(v *gocui.View) error { return v.SetOrigin(0, 0) } -// 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() - return gui.focusPoint(cx, cy, v) -} - // if the cursor down past the last item, move it to the last line func (gui *Gui) focusPoint(cx int, cy int, v *gocui.View) error { if cy < 0 { @@ -283,6 +220,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) @@ -331,6 +271,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()) } From ff856b763033a241370bfde98d7386b43b7d0893 Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Fri, 7 Dec 2018 18:52:31 +1100 Subject: [PATCH 5/5] fetching branches without checking out --- pkg/app/app.go | 3 ++ pkg/commands/branch.go | 15 +++++-- pkg/commands/git.go | 35 +++++++++-------- pkg/commands/git_test.go | 6 +-- pkg/gui/branches_panel.go | 70 +++++++++++++++++++++++++++------ pkg/gui/commit_message_panel.go | 8 ++-- pkg/gui/commits_panel.go | 3 +- pkg/gui/confirmation_panel.go | 11 +++--- pkg/gui/files_panel.go | 4 +- pkg/gui/gui.go | 31 +++++++++------ pkg/gui/keybindings.go | 60 ++++++++++++++-------------- pkg/gui/menu_panel.go | 12 +----- pkg/gui/merge_panel.go | 7 +--- pkg/gui/stash_panel.go | 2 +- pkg/gui/status_panel.go | 6 ++- pkg/gui/view_helpers.go | 66 ++++++++++++++++++++----------- pkg/i18n/english.go | 6 +++ pkg/utils/utils_test.go | 11 ++++++ 18 files changed, 224 insertions(+), 132 deletions(-) diff --git a/pkg/app/app.go b/pkg/app/app.go index b43f9568c..74152b08b 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -52,6 +52,9 @@ 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" { 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 e185b0fbd..b78db1fa6 100644 --- a/pkg/commands/git.go +++ b/pkg/commands/git.go @@ -130,17 +130,6 @@ func (c *GitCommand) GetStashEntryDiff(index int) (string, error) { // GetStatusFiles git status files func (c *GitCommand) GetStatusFiles() []*File { - - // files := []*File{} - // for i := 0; i < 100; i++ { - // files = append(files, &File{ - // Name: strconv.Itoa(i), - // DisplayString: strconv.Itoa(i), - // Type: "file", - // }) - // } - // return files - statusOutput, _ := c.GitStatus() statusStrings := utils.SplitLines(statusOutput) files := []*File{} @@ -165,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 } @@ -228,14 +216,24 @@ func (c *GitCommand) ResetAndClean() error { 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 "?", "?" } @@ -618,3 +616,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 206696b22..997054bca 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 809deaa7d..a0e3448f6 100644 --- a/pkg/gui/branches_panel.go +++ b/pkg/gui/branches_panel.go @@ -7,7 +7,6 @@ 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 @@ -31,6 +30,9 @@ func (gui *Gui) handleBranchSelect(g *gocui.Gui, v *gocui.View) error { 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") { @@ -41,14 +43,23 @@ func (gui *Gui) handleBranchSelect(g *gocui.Gui, v *gocui.View) error { 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 { - v, err := g.View("branches") - if err != nil { - panic(err) - } builder, err := git.NewBranchListBuilder(gui.Log, gui.GitCommand) if err != nil { return err @@ -56,16 +67,13 @@ func (gui *Gui) refreshBranches(g *gocui.Gui) error { gui.State.Branches = builder.Build() gui.refreshSelectedLine(&gui.State.Panels.Branches.SelectedLine, len(gui.State.Branches)) - - v.Clear() - list, err := utils.RenderList(gui.State.Branches) - if err != nil { + if err := gui.resetOrigin(gui.getBranchesView(gui.g)); err != nil { + return err + } + if err := gui.RenderSelectedBranchUpstreamDifferences(); err != nil { return err } - fmt.Fprint(v, list) - - gui.resetOrigin(v) return gui.refreshStatus(g) }) return nil @@ -74,7 +82,6 @@ func (gui *Gui) refreshBranches(g *gocui.Gui) error { 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) } @@ -99,7 +106,10 @@ func (gui *Gui) handleBranchPress(g *gocui.Gui, v *gocui.View) error { if err := gui.createErrorPanel(g, err.Error()); err != nil { return err } + } else { + gui.State.Panels.Branches.SelectedLine = 0 } + return gui.refreshSidePanels(g) } @@ -213,3 +223,37 @@ func (gui *Gui) handleMerge(g *gocui.Gui, v *gocui.View) error { } return nil } + +func (gui *Gui) handleFastForward(g *gocui.Gui, v *gocui.View) error { + branch := gui.getSelectedBranch() + if branch == nil { + return nil + } + 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() { + _ = 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() + } + }() + return nil +} diff --git a/pkg/gui/commit_message_panel.go b/pkg/gui/commit_message_panel.go index 60058d48c..33eb5b218 100644 --- a/pkg/gui/commit_message_panel.go +++ b/pkg/gui/commit_message_panel.go @@ -24,10 +24,10 @@ func (gui *Gui) handleCommitConfirm(g *gocui.Gui, v *gocui.View) error { return gui.Errors.ErrSubProcess } v.Clear() - v.SetCursor(0, 0) - v.SetOrigin(0, 0) - g.SetViewOnBottom("commitMessage") - gui.switchFocus(g, v, gui.getFilesView(g)) + _ = v.SetCursor(0, 0) + _ = v.SetOrigin(0, 0) + _, _ = g.SetViewOnBottom("commitMessage") + _ = gui.switchFocus(g, v, gui.getFilesView(g)) return gui.refreshSidePanels(g) } diff --git a/pkg/gui/commits_panel.go b/pkg/gui/commits_panel.go index 2591fa240..4ff79960d 100644 --- a/pkg/gui/commits_panel.go +++ b/pkg/gui/commits_panel.go @@ -96,7 +96,8 @@ 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) } diff --git a/pkg/gui/confirmation_panel.go b/pkg/gui/confirmation_panel.go index afe53ca2c..30d5e0661 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 231e5d488..95b4f80a4 100644 --- a/pkg/gui/files_panel.go +++ b/pkg/gui/files_panel.go @@ -64,7 +64,7 @@ func (gui *Gui) refreshFiles(g *gocui.Gui) error { fmt.Fprint(filesView, list) if filesView == g.CurrentView() { - gui.handleFileSelect(g, filesView) + return gui.handleFileSelect(g, filesView) } return nil }) @@ -411,7 +411,7 @@ func (gui *Gui) pushWithForceFlag(currentView *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(v, false) } diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index 6a7eaaf98..26ebd5f2b 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -72,6 +72,9 @@ type Gui struct { statusManager *statusManager } +// 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 @@ -233,7 +236,7 @@ func (gui *Gui) layout(g *gocui.Gui) error { } return nil } else { - g.SetViewOnBottom("limit") + _, _ = g.SetViewOnBottom("limit") } g.DeleteView("limit") @@ -364,14 +367,14 @@ func (gui *Gui) layout(g *gocui.Gui) error { return err } - gui.g.SetCurrentView(filesView.Name()) - - gui.refreshSidePanels(gui.g) - if gui.g.CurrentView().Name() != "menu" { - if err := gui.renderGlobalOptions(g); err != nil { - return err - } + 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 } @@ -394,8 +397,10 @@ func (gui *Gui) layout(g *gocui.Gui) error { } } - // TODO: comment-out - gui.Log.Info(utils.AsJson(gui.State)) + // 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) } @@ -435,8 +440,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"), @@ -467,7 +472,7 @@ func (gui *Gui) Run() error { } gui.goEvery(g, time.Second*60, gui.fetch) - // gui.goEvery(g, time.Second*2, gui.refreshFiles) // TODO: comment back in + gui.goEvery(g, time.Second*2, gui.refreshFiles) gui.goEvery(g, time.Millisecond*50, gui.updateLoader) gui.goEvery(g, time.Millisecond*50, gui.renderAppStatus) diff --git a/pkg/gui/keybindings.go b/pkg/gui/keybindings.go index c02074d6f..b0c141d73 100644 --- a/pkg/gui/keybindings.go +++ b/pkg/gui/keybindings.go @@ -219,12 +219,7 @@ func (gui *Gui) GetKeybindings() []*Binding { Handler: gui.handleSwitchToStagingPanel, Description: gui.Tr.SLocalize("StageLines"), KeyReadable: "enter", - }, - {ViewName: "files", Key: 'k', Modifier: gocui.ModNone, Handler: gui.handleFilesPrevLine}, - {ViewName: "files", Key: gocui.KeyArrowUp, Modifier: gocui.ModNone, Handler: gui.handleFilesPrevLine}, - {ViewName: "files", Key: 'j', Modifier: gocui.ModNone, Handler: gui.handleFilesNextLine}, - {ViewName: "files", Key: gocui.KeyArrowDown, Modifier: gocui.ModNone, Handler: gui.handleFilesNextLine}, - { + }, { ViewName: "main", Key: gocui.KeyEsc, Modifier: gocui.ModNone, @@ -327,12 +322,13 @@ func (gui *Gui) GetKeybindings() []*Binding { Modifier: gocui.ModNone, Handler: gui.handleMerge, Description: gui.Tr.SLocalize("mergeIntoCurrentBranch"), - }, - {ViewName: "branches", Key: 'k', Modifier: gocui.ModNone, Handler: gui.handleBranchesPrevLine}, - {ViewName: "branches", Key: gocui.KeyArrowUp, Modifier: gocui.ModNone, Handler: gui.handleBranchesPrevLine}, - {ViewName: "branches", Key: 'j', Modifier: gocui.ModNone, Handler: gui.handleBranchesNextLine}, - {ViewName: "branches", Key: gocui.KeyArrowDown, Modifier: gocui.ModNone, Handler: gui.handleBranchesNextLine}, - { + }, { + ViewName: "branches", + Key: 'f', + Modifier: gocui.ModNone, + Handler: gui.handleFastForward, + Description: gui.Tr.SLocalize("FastForward"), + }, { ViewName: "commits", Key: 's', Modifier: gocui.ModNone, @@ -362,12 +358,7 @@ func (gui *Gui) GetKeybindings() []*Binding { Modifier: gocui.ModNone, Handler: gui.handleCommitFixup, Description: gui.Tr.SLocalize("fixupCommit"), - }, - {ViewName: "commits", Key: 'k', Modifier: gocui.ModNone, Handler: gui.handleCommitsPrevLine}, - {ViewName: "commits", Key: gocui.KeyArrowUp, Modifier: gocui.ModNone, Handler: gui.handleCommitsPrevLine}, - {ViewName: "commits", Key: 'j', Modifier: gocui.ModNone, Handler: gui.handleCommitsNextLine}, - {ViewName: "commits", Key: gocui.KeyArrowDown, Modifier: gocui.ModNone, Handler: gui.handleCommitsNextLine}, - { + }, { ViewName: "stash", Key: gocui.KeySpace, Modifier: gocui.ModNone, @@ -386,12 +377,7 @@ func (gui *Gui) GetKeybindings() []*Binding { Modifier: gocui.ModNone, Handler: gui.handleStashDrop, Description: gui.Tr.SLocalize("drop"), - }, - {ViewName: "stash", Key: 'k', Modifier: gocui.ModNone, Handler: gui.handleStashPrevLine}, - {ViewName: "stash", Key: gocui.KeyArrowUp, Modifier: gocui.ModNone, Handler: gui.handleStashPrevLine}, - {ViewName: "stash", Key: 'j', Modifier: gocui.ModNone, Handler: gui.handleStashNextLine}, - {ViewName: "stash", Key: gocui.KeyArrowDown, Modifier: gocui.ModNone, Handler: gui.handleStashNextLine}, - { + }, { ViewName: "commitMessage", Key: gocui.KeyEnter, Modifier: gocui.ModNone, @@ -411,11 +397,7 @@ func (gui *Gui) GetKeybindings() []*Binding { Key: 'q', Modifier: gocui.ModNone, Handler: gui.handleMenuClose, - }, - {ViewName: "menu", Key: 'k', Modifier: gocui.ModNone, Handler: gui.handleMenuPrevLine}, - {ViewName: "menu", Key: gocui.KeyArrowUp, Modifier: gocui.ModNone, Handler: gui.handleMenuPrevLine}, - {ViewName: "menu", Key: 'j', Modifier: gocui.ModNone, Handler: gui.handleMenuNextLine}, - {ViewName: "menu", Key: gocui.KeyArrowDown, Modifier: gocui.ModNone, Handler: gui.handleMenuNextLine}, { + }, { ViewName: "staging", Key: gocui.KeyEsc, Modifier: gocui.ModNone, @@ -487,6 +469,26 @@ func (gui *Gui) GetKeybindings() []*Binding { }...) } + 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}, + }...) + } + return bindings } diff --git a/pkg/gui/menu_panel.go b/pkg/gui/menu_panel.go index 0d3d8cf2b..8ce60f6be 100644 --- a/pkg/gui/menu_panel.go +++ b/pkg/gui/menu_panel.go @@ -25,22 +25,18 @@ func (gui *Gui) handleMenuPrevLine(g *gocui.Gui, v *gocui.View) error { panelState := gui.State.Panels.Menu gui.changeSelectedLine(&panelState.SelectedLine, v.LinesHeight(), true) - if err := gui.focusPoint(0, gui.State.Panels.Commits.SelectedLine, v); err != nil { - return err - } - return gui.handleMenuSelect(g, v) } // specific functions -func (gui *Gui) renderMenuOptions(g *gocui.Gui) error { +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 { @@ -68,10 +64,6 @@ func (gui *Gui) createMenu(items interface{}, handlePress func(int) error) error fmt.Fprint(menuView, list) gui.State.Panels.Menu.SelectedLine = 0 - if err := gui.renderMenuOptions(gui.g); err != nil { - return err - } - wrappedHandlePress := func(g *gocui.Gui, v *gocui.View) error { selectedLine := gui.State.Panels.Menu.SelectedLine return handlePress(selectedLine) 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/stash_panel.go b/pkg/gui/stash_panel.go index ef54ef997..306d61771 100644 --- a/pkg/gui/stash_panel.go +++ b/pkg/gui/stash_panel.go @@ -30,7 +30,7 @@ func (gui *Gui) handleStashEntrySelect(g *gocui.Gui, v *gocui.View) error { go func() { // doing this asynchronously cos it can take time diff, _ := gui.GitCommand.GetStashEntryDiff(stashEntry.Index) - gui.renderString(g, "main", diff) + _ = gui.renderString(g, "main", diff) }() return nil } diff --git a/pkg/gui/status_panel.go b/pkg/gui/status_panel.go index 15f3f27d1..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 { @@ -48,6 +48,8 @@ func (gui *Gui) handleCheckForUpdate(g *gocui.Gui, v *gocui.View) error { } func (gui *Gui) handleStatusSelect(g *gocui.Gui, v *gocui.View) error { + blue := color.New(color.FgBlue) + dashboardString := strings.Join( []string{ lazygitTitle(), @@ -56,7 +58,7 @@ 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") return gui.renderString(g, "main", dashboardString) diff --git a/pkg/gui/view_helpers.go b/pkg/gui/view_helpers.go index 5b02b1ae9..1508daa1b 100644 --- a/pkg/gui/view_helpers.go +++ b/pkg/gui/view_helpers.go @@ -13,11 +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) - gui.refreshStashEntries(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 { @@ -81,7 +86,7 @@ func (gui *Gui) previousView(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) newLineFocused(g *gocui.Gui, v *gocui.View) error { switch v.Name() { case "menu": - return nil + return gui.handleMenuSelect(g, v) case "status": return gui.handleStatusSelect(g, v) case "files": @@ -160,6 +165,10 @@ func (gui *Gui) switchFocus(g *gocui.Gui, oldView, newView *gocui.View) error { g.Cursor = newView.Editable + if err := gui.renderPanelOptions(); err != nil { + return err + } + return gui.newLineFocused(g, newView) } @@ -240,8 +249,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 @@ -312,22 +321,6 @@ func (gui *Gui) resizePopupPanel(g *gocui.Gui, v *gocui.View) error { return err } -// focusLine focuses and selects the given line -func (gui *Gui) focusLine(lineNumber int, v *gocui.View) error { - _, height := v.Size() - overScroll := lineNumber - 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 -} - // 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() @@ -367,3 +360,28 @@ func (gui *Gui) refreshSelectedLine(line *int, total int) { *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/english.go b/pkg/i18n/english.go index 97b6addf6..9ff671476 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -435,6 +435,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/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) +}