diff --git a/docs-master/Stacked_Branches.md b/docs-master/Stacked_Branches.md index f961e74fd..6bbd7ae56 100644 --- a/docs-master/Stacked_Branches.md +++ b/docs-master/Stacked_Branches.md @@ -15,7 +15,9 @@ branches properly stacked onto it. Lazygit visualizes the individual branch heads in the stack by marking them with a cyan asterisk (or a cyan branch symbol if you are using [nerd -fonts](Config.md#display-nerd-fonts-icons)). +fonts](Config.md#display-nerd-fonts-icons)). In the commits panel, `>` selects +the first commit of the branch that the selection is in. Press it again to get +to the first commit of the next branch down the stack. When you push the topmost branch of the stack with `P`, and the branches below it have commits that haven't been pushed yet, lazygit offers to push them along diff --git a/pkg/gui/context/commit_list_goto_bottom.go b/pkg/gui/context/commit_list_goto_bottom.go index 6b0f9a94c..3d955f628 100644 --- a/pkg/gui/context/commit_list_goto_bottom.go +++ b/pkg/gui/context/commit_list_goto_bottom.go @@ -1,8 +1,10 @@ package context import ( + "github.com/jesseduffield/generics/set" "github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/samber/lo" + "github.com/stefanhaller/git-todo-parser/todo" ) // Returns the index of the commit that the "go to bottom" command selects in a @@ -13,14 +15,33 @@ import ( // // The sections are: // - the todos of a rebase, cherry-pick or revert -// - the commits of the branch that aren't merged into a main branch yet -func indexForGotoBottomInCommits(commits []*models.Commit, selectedIdx int) int { +// - each branch of a stack, given by branchesBelow. Among the todos of a +// rebase, update-ref todos separate the branches. +// - the commits that aren't merged into a main branch yet +func indexForGotoBottomInCommits( + commits []*models.Commit, selectedIdx int, branchesBelow []*models.Branch, +) int { stops := []int{} if headIdx := models.HeadCommitIdx(commits); headIdx > 0 { stops = append(stops, headIdx-1) } + tipsOfBranchesBelow := set.NewFromSlice(lo.Map(branchesBelow, func(branch *models.Branch, _ int) string { + return branch.CommitHash + })) + for idx, commit := range commits { + if commit.IsTODO() { + // If several update-ref todos follow each other, the branches + // between them have no todos, so only stop above the first one + if commit.Action == todo.UpdateRef && idx > 0 && commits[idx-1].Action != todo.UpdateRef { + stops = append(stops, idx-1) + } + } else if tipsOfBranchesBelow.Includes(commit.Hash()) { + stops = append(stops, idx-1) + } + } + if _, idx, found := lo.FindIndexOf(commits, func(c *models.Commit) bool { return c.Status == models.StatusMerged }); found { diff --git a/pkg/gui/context/commit_list_goto_bottom_test.go b/pkg/gui/context/commit_list_goto_bottom_test.go index b37784814..bb942e480 100644 --- a/pkg/gui/context/commit_list_goto_bottom_test.go +++ b/pkg/gui/context/commit_list_goto_bottom_test.go @@ -34,11 +34,39 @@ func TestIndexForGotoBottomInCommits(t *testing.T) { commit("m1", models.StatusMerged), } + updateRefTodo := func(branchName string) *models.Commit { + return models.NewCommit(hashPool, models.NewCommitOpts{ + Name: "refs/heads/" + branchName, Action: todo.UpdateRef, Status: models.StatusRebasing, + }) + } + branch := func(name string, tip string) *models.Branch { + return &models.Branch{Name: name, CommitHash: tip} + } + + stackCommits := []*models.Commit{ + commit("c4", models.StatusUnpushed), + commit("c3", models.StatusUnpushed), + commit("c2", models.StatusPushed), + commit("c1", models.StatusUnpushed), + commit("m1", models.StatusMerged), + } + + stackRebaseCommits := []*models.Commit{ + todoCommit("t4", todo.Pick), + todoCommit("t3", todo.Pick), + updateRefTodo("lower"), + todoCommit("t2", todo.Pick), + todoCommit("t1", todo.Pick), + commit("c1", models.StatusUnpushed), + commit("m1", models.StatusMerged), + } + scenarios := []struct { - testName string - selectedIdx int - commits []*models.Commit - expected int + testName string + selectedIdx int + commits []*models.Commit + branchesBelow []*models.Branch + expected int }{ { testName: "goes to the beginning of the branch first", @@ -111,11 +139,53 @@ func TestIndexForGotoBottomInCommits(t *testing.T) { }, expected: 1, }, + { + testName: "goes to the beginning of the branch of a stack that the selection is in", + selectedIdx: 0, + commits: stackCommits, + branchesBelow: []*models.Branch{branch("lower", "c2")}, + expected: 1, + }, + { + testName: "goes to the beginning of the lower branch of a stack from the upper one", + selectedIdx: 1, + commits: stackCommits, + branchesBelow: []*models.Branch{branch("lower", "c2")}, + expected: 3, + }, + { + testName: "goes to the beginning of the branch of a stack among the todos of a rebase", + selectedIdx: 0, + commits: stackRebaseCommits, + branchesBelow: []*models.Branch{}, + expected: 1, + }, + { + testName: "goes from the beginning of a branch among the todos to the first todo", + selectedIdx: 1, + commits: stackRebaseCommits, + branchesBelow: []*models.Branch{}, + expected: 4, + }, + { + testName: "goes past update-ref todos without todos between them", + selectedIdx: 0, + commits: []*models.Commit{ + todoCommit("t3", todo.Pick), + updateRefTodo("upper"), + updateRefTodo("lower"), + todoCommit("t2", todo.Pick), + todoCommit("t1", todo.Pick), + commit("c1", models.StatusUnpushed), + }, + branchesBelow: []*models.Branch{}, + expected: 4, + }, } for _, s := range scenarios { t.Run(s.testName, func(t *testing.T) { - assert.Equal(t, s.expected, indexForGotoBottomInCommits(s.commits, s.selectedIdx)) + assert.Equal(t, s.expected, indexForGotoBottomInCommits(s.commits, s.selectedIdx, s.branchesBelow)) }) } } diff --git a/pkg/gui/context/local_commits_context.go b/pkg/gui/context/local_commits_context.go index a00783147..23abc0239 100644 --- a/pkg/gui/context/local_commits_context.go +++ b/pkg/gui/context/local_commits_context.go @@ -11,6 +11,7 @@ import ( "github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/config" "github.com/jesseduffield/lazygit/pkg/gocui" + "github.com/jesseduffield/lazygit/pkg/gui/branchstack" "github.com/jesseduffield/lazygit/pkg/gui/presentation" "github.com/jesseduffield/lazygit/pkg/gui/presentation/graph" "github.com/jesseduffield/lazygit/pkg/gui/style" @@ -520,5 +521,9 @@ func searchModelCommits(caseSensitive bool, commits []*models.Commit, columnPosi } func (self *LocalCommitsContext) IndexForGotoBottom() int { - return indexForGotoBottomInCommits(self.GetCommits(), self.GetSelectedLineIdx()) + c := self.ListContextTrait.c + commits := self.GetCommits() + branchesBelow := branchstack.BranchesBelow( + commits, c.Model().Branches, c.Model().CheckedOutBranch, c.UserConfig().Git.MainBranches) + return indexForGotoBottomInCommits(commits, self.GetSelectedLineIdx(), branchesBelow) } diff --git a/pkg/gui/context/sub_commits_context.go b/pkg/gui/context/sub_commits_context.go index c6bad0f45..73adc4b89 100644 --- a/pkg/gui/context/sub_commits_context.go +++ b/pkg/gui/context/sub_commits_context.go @@ -6,6 +6,7 @@ import ( "github.com/jesseduffield/lazygit/pkg/commands/git_commands" "github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/gocui" + "github.com/jesseduffield/lazygit/pkg/gui/branchstack" "github.com/jesseduffield/lazygit/pkg/gui/presentation" "github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/samber/lo" @@ -269,5 +270,11 @@ func (self *SubCommitsContext) ModelSearchResults(searchStr string, caseSensitiv } func (self *SubCommitsContext) IndexForGotoBottom() int { - return indexForGotoBottomInCommits(self.GetCommits(), self.GetSelectedLineIdx()) + commits := self.GetCommits() + var branchesBelow []*models.Branch + if self.GetShowBranchHeads() { + branchesBelow = branchstack.BranchesBelow( + commits, self.c.Model().Branches, self.GetRef().RefName(), self.c.UserConfig().Git.MainBranches) + } + return indexForGotoBottomInCommits(commits, self.GetSelectedLineIdx(), branchesBelow) } diff --git a/pkg/integration/tests/commit/goto_bottom_in_branch_stack.go b/pkg/integration/tests/commit/goto_bottom_in_branch_stack.go new file mode 100644 index 000000000..cc9592d5d --- /dev/null +++ b/pkg/integration/tests/commit/goto_bottom_in_branch_stack.go @@ -0,0 +1,82 @@ +package commit + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var GotoBottomInBranchStack = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Go to the bottom of the commits list in steps, stopping at the beginning of each branch of a stack", + ExtraCmdArgs: []string{}, + Skip: false, + GitVersion: AtLeast("2.38.0"), + SetupConfig: func(config *config.AppConfig) {}, + SetupRepo: func(shell *Shell) { + shell. + CreateNCommits(2). + NewBranch("branch1"). + CreateNCommitsStartingAt(2, 3). + NewBranch("branch2"). + CreateNCommitsStartingAt(2, 5) + + shell.SetConfig("rebase.updateRefs", "true") + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Commits(). + Focus(). + Lines( + Contains("CI ○ commit-06").IsSelected(), + Contains("CI ○ commit-05"), + Contains("CI ○ * commit-04"), + Contains("CI ○ commit-03"), + Contains("CI ○ commit-02"), + Contains("CI ○ commit-01"), + ). + Press(keys.Universal.GotoBottom). + SelectedLine(Contains("commit-05")). + Press(keys.Universal.GotoBottom). + SelectedLine(Contains("commit-03")). + Press(keys.Universal.GotoBottom). + SelectedLine(Contains("commit-01")) + + t.Views().Branches(). + Focus(). + NavigateToLine(Contains("branch2")). + PressEnter() + + t.Views().SubCommits(). + IsFocused(). + SelectedLine(Contains("commit-06")). + Press(keys.Universal.GotoBottom). + SelectedLine(Contains("commit-05")). + Press(keys.Universal.GotoBottom). + SelectedLine(Contains("commit-03")). + Press(keys.Universal.GotoBottom). + SelectedLine(Contains("commit-01")) + + t.Views().Commits(). + Focus(). + NavigateToLine(Contains("commit-03")). + Press(keys.Universal.Edit). + NavigateToLine(Contains("commit-06")). + Lines( + Contains("─── Pending rebase todos"), + Contains("pick").Contains("CI commit-06").IsSelected(), + Contains("pick").Contains("CI commit-05"), + Contains("update-ref").Contains("branch1"), + Contains("pick").Contains("CI commit-04"), + Contains("─── Commits"), + Contains("CI ○ commit-03"), + Contains("CI ○ commit-02"), + Contains("CI ○ commit-01"), + ). + Press(keys.Universal.GotoBottom). + SelectedLine(Contains("commit-05")). + Press(keys.Universal.GotoBottom). + SelectedLine(Contains("commit-04")). + Press(keys.Universal.GotoBottom). + SelectedLine(Contains("commit-03")). + Press(keys.Universal.GotoBottom). + SelectedLine(Contains("commit-01")) + }, +}) diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go index d7ef9eec2..0219fb43e 100644 --- a/pkg/integration/tests/test_list.go +++ b/pkg/integration/tests/test_list.go @@ -156,6 +156,7 @@ var tests = []*components.IntegrationTest{ commit.FindBaseCommitForFixupDisregardMainBranch, commit.FindBaseCommitForFixupOnlyAddedLines, commit.FindBaseCommitForFixupWarningForAddedLines, + commit.GotoBottomInBranchStack, commit.Highlight, commit.History, commit.HistoryComplex,