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 <noreply@anthropic.com>
This commit is contained in:
Stefan Haller
2026-11-08 12:46:15 +01:00
co-authored by Claude Opus 5.5
parent 633bedf4fd
commit 462a168d87
7 changed files with 198 additions and 10 deletions
+3 -1
View File
@@ -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
+23 -2
View File
@@ -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 {
@@ -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))
})
}
}
+6 -1
View File
@@ -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)
}
+8 -1
View File
@@ -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)
}
@@ -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"))
},
})
+1
View File
@@ -156,6 +156,7 @@ var tests = []*components.IntegrationTest{
commit.FindBaseCommitForFixupDisregardMainBranch,
commit.FindBaseCommitForFixupOnlyAddedLines,
commit.FindBaseCommitForFixupWarningForAddedLines,
commit.GotoBottomInBranchStack,
commit.Highlight,
commit.History,
commit.HistoryComplex,