From 462a168d87bcb93ca12e4d6b5a52893f916aaf0f Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Sun, 8 Nov 2026 12:46:15 +0100 Subject: [PATCH] Make "go to bottom" stop at the beginning of each branch of a stack In a stack of branches, pressing '>' in the commits panel went to the beginning of the bottommost branch right away. Make it go to the beginning of the branch that the selection is in first, and then to the beginning of each branch below it. During a rebase, the update-ref todos separate the branches among the todos, so stop above each of them too. The branches of the stack are the ones that the commits list marks, and that pushing and pulling a stack include. Co-Authored-By: Claude Opus 5.5 --- docs-master/Stacked_Branches.md | 4 +- pkg/gui/context/commit_list_goto_bottom.go | 25 +++++- .../context/commit_list_goto_bottom_test.go | 80 ++++++++++++++++-- pkg/gui/context/local_commits_context.go | 7 +- pkg/gui/context/sub_commits_context.go | 9 +- .../commit/goto_bottom_in_branch_stack.go | 82 +++++++++++++++++++ pkg/integration/tests/test_list.go | 1 + 7 files changed, 198 insertions(+), 10 deletions(-) create mode 100644 pkg/integration/tests/commit/goto_bottom_in_branch_stack.go 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,