Make "go to bottom" stop at the beginning of each branch of a stack (#6193)

In the Commits panel, the "go to bottom" command (`>`) already has a
special case where it jumps to the beginning of the checked-out branch
first, and only then to the end of the list.

In this PR we take this one step further and also make it stop at the
beginning of each individual branch in a stack. This makes it easier to
navigate stacks of long branches.

Also, we make it stop at the beginning of the rebase todos in an
interactive rebase, and at the bad, current and good commits of a
bisect.
This commit is contained in:
Stefan Haller
2026-11-08 14:27:21 +01:00
committed by GitHub
14 changed files with 619 additions and 113 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
+92
View File
@@ -0,0 +1,92 @@
package branchstack
import (
"github.com/jesseduffield/generics/set"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/samber/lo"
)
// Returns the branches stacked below the branch with the given name: those
// whose tip is one of the given commits of the branch that hasn't been
// merged into a main branch yet. The branch itself and the main branches
// are left out. The result is in the order in which the tips appear in
// commits, so the branch closest to the given one comes first.
//
// A commit counts as merged if the remote branch of a main branch contains it
// (this is what the commit's status says), but also if the local main branch
// contains it. So if a branch was merged into the local main branch, it isn't
// stacked below a branch on top of that main branch, and if the given branch
// is itself a main branch, nothing is stacked below it.
//
// Only the commits passed in are looked at, so a branch whose tip is further
// down the history than the loaded commits is not found.
func BranchesBelow(
commits []*models.Commit,
branches []*models.Branch,
branchName string,
mainBranches []string,
) []*models.Branch {
branchesByTip := map[string][]*models.Branch{}
for _, other := range branches {
if !lo.Contains(mainBranches, other.Name) && other.Name != branchName {
branchesByTip[other.CommitHash] = append(branchesByTip[other.CommitHash], other)
}
}
mergedLocally := CommitsMergedIntoLocalMainBranch(commits, branches, mainBranches)
result := []*models.Branch{}
for _, commit := range commits {
if commit.IsTODO() || commit.Status == models.StatusMerged || mergedLocally.Includes(commit.Hash()) {
continue
}
result = append(result, branchesByTip[commit.Hash()]...)
}
return result
}
// Returns the hashes of those of the given commits that a local main branch
// contains, but whose status doesn't say that they are merged. The status
// looks at the remote branch of a main branch if it has one, so these are
// commits that were merged into a local main branch but not pushed yet.
func CommitsMergedIntoLocalMainBranch(
commits []*models.Commit,
branches []*models.Branch,
mainBranches []string,
) *set.Set[string] {
result := set.New[string]()
mainBranchTips := lo.FilterMap(branches, func(branch *models.Branch, _ int) (string, bool) {
return branch.CommitHash, lo.Contains(mainBranches, branch.Name)
})
if len(mainBranchTips) == 0 {
return result
}
// The parents of a merged commit are merged too, so the walk never needs
// to go past a merged commit. Usually the tip of a local main branch is
// merged itself, so there is nothing to walk; this keeps it cheap enough to
// do on every render of the commits list.
unmergedCommitsByHash := map[string]*models.Commit{}
for _, commit := range commits {
if !commit.IsTODO() && commit.Status != models.StatusMerged {
unmergedCommitsByHash[commit.Hash()] = commit
}
}
pending := mainBranchTips
for len(pending) > 0 {
hash := pending[len(pending)-1]
pending = pending[:len(pending)-1]
commit, ok := unmergedCommitsByHash[hash]
if !ok || result.Includes(hash) {
continue
}
result.Add(hash)
pending = append(pending, commit.Parents()...)
}
return result
}
@@ -1,4 +1,4 @@
package helpers
package branchstack
import (
"testing"
@@ -10,7 +10,7 @@ import (
"github.com/stretchr/testify/assert"
)
func TestBranchesBelowInStack(t *testing.T) {
func TestBranchesBelow(t *testing.T) {
hashPool := &utils.StringPool{}
commit := func(hash string, status models.CommitStatus, parents ...string) *models.Commit {
return models.NewCommit(hashPool, models.NewCommitOpts{Hash: hash, Status: status, Parents: parents})
@@ -141,7 +141,7 @@ func TestBranchesBelowInStack(t *testing.T) {
if s.current != nil {
branch = s.current
}
result := BranchesBelowInStack(s.commits, s.branches, branch, mainBranches)
result := BranchesBelow(s.commits, s.branches, branch.Name, mainBranches)
names := lo.Map(result, func(b *models.Branch, _ int) string { return b.Name })
assert.Equal(t, s.expected, names)
})
+108
View File
@@ -0,0 +1,108 @@
package context
import (
"github.com/jesseduffield/generics/set"
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
"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
// list of commits. If the list has several sections, the command first goes to
// the beginning of the section that the selection is in, which is its oldest
// commit. From there it goes to the beginning of the next section, and from the
// last section to the bottom of the list.
//
// The sections are:
// - the todos of a rebase, cherry-pick or revert
// - 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
// - the remote commits of a divergence view, which are above the local ones
//
// During a bisect, it stops at the bottommost commit marked bad, at the commit
// to test, and at the topmost commit marked good instead. The commits list
// doesn't color the commits by whether they are merged during a bisect, so the
// beginnings of the sections would look like arbitrary commits.
func indexForGotoBottomInCommits(
commits []*models.Commit,
selectedIdx int,
branchesBelow []*models.Branch,
bisectInfo *git_commands.BisectInfo,
) int {
if bisectInfo.Started() {
return closestStopBelow(bisectStops(commits, bisectInfo), selectedIdx, len(commits)-1)
}
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 {
stops = append(stops, idx-1)
}
if _, idx, found := lo.FindIndexOf(commits, func(c *models.Commit) bool {
return c.Divergence == models.DivergenceLeft
}); found {
stops = append(stops, idx-1)
}
return closestStopBelow(stops, selectedIdx, len(commits)-1)
}
func bisectStops(commits []*models.Commit, bisectInfo *git_commands.BisectInfo) []int {
hasStatus := func(status git_commands.BisectStatus) func(*models.Commit) bool {
return func(commit *models.Commit) bool {
commitStatus, ok := bisectInfo.Status(commit.Hash())
return ok && commitStatus == status
}
}
isCommitToTest := func(commit *models.Commit) bool {
return bisectInfo.HasCommitToTest() && commit.Hash() == bisectInfo.GetCurrentHash()
}
stops := []int{}
if _, idx, found := lo.FindLastIndexOf(commits, hasStatus(git_commands.BisectStatusNew)); found {
stops = append(stops, idx)
}
if _, idx, found := lo.FindIndexOf(commits, isCommitToTest); found {
stops = append(stops, idx)
}
if _, idx, found := lo.FindIndexOf(commits, hasStatus(git_commands.BisectStatusOld)); found {
stops = append(stops, idx)
}
return stops
}
// Returns the smallest of the given stops that is greater than the selected
// index, or the last index if there is none
func closestStopBelow(stops []int, selectedIdx int, lastIdx int) int {
result := lastIdx
for _, stop := range stops {
if stop > selectedIdx && stop < result {
result = stop
}
}
return result
}
@@ -0,0 +1,225 @@
package context
import (
"testing"
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/stefanhaller/git-todo-parser/todo"
"github.com/stretchr/testify/assert"
)
func TestIndexForGotoBottomInCommits(t *testing.T) {
hashPool := &utils.StringPool{}
commit := func(hash string, status models.CommitStatus) *models.Commit {
return models.NewCommit(hashPool, models.NewCommitOpts{Hash: hash, Status: status})
}
todoCommit := func(hash string, action todo.TodoCommand) *models.Commit {
return models.NewCommit(hashPool, models.NewCommitOpts{Hash: hash, Action: action, Status: models.StatusRebasing})
}
branchCommits := []*models.Commit{
commit("c3", models.StatusUnpushed),
commit("c2", models.StatusUnpushed),
commit("c1", models.StatusPushed),
commit("m2", models.StatusMerged),
commit("m1", models.StatusMerged),
}
rebaseCommits := []*models.Commit{
todoCommit("t3", todo.Pick),
todoCommit("t2", todo.Pick),
todoCommit("t1", todo.Edit),
commit("c1", models.StatusUnpushed),
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,
})
}
divergentCommit := func(hash string, divergence models.Divergence) *models.Commit {
return models.NewCommit(hashPool, models.NewCommitOpts{Hash: hash, Divergence: divergence})
}
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),
}
divergenceCommits := []*models.Commit{
divergentCommit("r2", models.DivergenceRight),
divergentCommit("r1", models.DivergenceRight),
divergentCommit("l3", models.DivergenceLeft),
divergentCommit("l2", models.DivergenceLeft),
divergentCommit("l1", models.DivergenceLeft),
}
scenarios := []struct {
testName string
selectedIdx int
commits []*models.Commit
branchesBelow []*models.Branch
expected int
}{
{
testName: "goes to the beginning of the branch first",
selectedIdx: 0,
commits: branchCommits,
expected: 2,
},
{
testName: "goes from the beginning of the branch to the bottom",
selectedIdx: 2,
commits: branchCommits,
expected: 4,
},
{
testName: "goes from a merged commit to the bottom",
selectedIdx: 3,
commits: branchCommits,
expected: 4,
},
{
testName: "goes to the bottom if no commit is merged",
selectedIdx: 0,
commits: []*models.Commit{
commit("c2", models.StatusUnpushed),
commit("c1", models.StatusUnpushed),
},
expected: 1,
},
{
testName: "goes to the bottom if the branch has no commits of its own",
selectedIdx: 0,
commits: []*models.Commit{
commit("m2", models.StatusMerged),
commit("m1", models.StatusMerged),
},
expected: 1,
},
{
testName: "goes down from a merged commit that an unmerged one follows",
selectedIdx: 1,
commits: []*models.Commit{
// c3 merges m2 of master into the branch
commit("c3", models.StatusUnpushed),
commit("m2", models.StatusMerged),
commit("c2", models.StatusUnpushed),
commit("c1", models.StatusUnpushed),
commit("m1", models.StatusMerged),
},
expected: 4,
},
{
testName: "goes to the first todo of a rebase",
selectedIdx: 0,
commits: rebaseCommits,
expected: 2,
},
{
testName: "goes from the first todo of a rebase to the beginning of the branch",
selectedIdx: 2,
commits: rebaseCommits,
expected: 3,
},
{
testName: "goes to the conflicted commit at the bottom of the todos",
selectedIdx: 0,
commits: []*models.Commit{
todoCommit("t2", todo.Pick),
models.NewCommit(hashPool, models.NewCommitOpts{Hash: "t1", Action: todo.Pick, Status: models.StatusConflicted}),
commit("c1", models.StatusUnpushed),
},
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,
},
{
testName: "goes to the beginning of the remote commits of a divergence view",
selectedIdx: 0,
commits: divergenceCommits,
expected: 1,
},
{
testName: "goes from the remote commits of a divergence view to the bottom",
selectedIdx: 1,
commits: divergenceCommits,
expected: 4,
},
{
testName: "goes to the bottom of a divergence view without remote commits",
selectedIdx: 0,
commits: []*models.Commit{
divergentCommit("l2", models.DivergenceLeft),
divergentCommit("l1", models.DivergenceLeft),
},
expected: 1,
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
assert.Equal(t, s.expected, indexForGotoBottomInCommits(
s.commits, s.selectedIdx, s.branchesBelow, git_commands.NewNullBisectInfo()))
})
}
}
+5 -13
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,18 +521,9 @@ func searchModelCommits(caseSensitive bool, commits []*models.Commit, columnPosi
}
func (self *LocalCommitsContext) IndexForGotoBottom() int {
c := self.ListContextTrait.c
commits := self.GetCommits()
selectedIdx := self.GetSelectedLineIdx()
if selectedIdx >= 0 && selectedIdx < len(commits)-1 {
if commits[selectedIdx+1].Status != models.StatusMerged {
_, idx, found := lo.FindIndexOf(commits, func(c *models.Commit) bool {
return c.Status == models.StatusMerged
})
if found {
return idx - 1
}
}
}
return self.list.Len() - 1
branchesBelow := branchstack.BranchesBelow(
commits, c.Model().Branches, c.Model().CheckedOutBranch, c.UserConfig().Git.MainBranches)
return indexForGotoBottomInCommits(commits, self.GetSelectedLineIdx(), branchesBelow, c.Model().BisectInfo)
}
+7 -12
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"
@@ -270,17 +271,11 @@ func (self *SubCommitsContext) ModelSearchResults(searchStr string, caseSensitiv
func (self *SubCommitsContext) IndexForGotoBottom() int {
commits := self.GetCommits()
selectedIdx := self.GetSelectedLineIdx()
if selectedIdx >= 0 && selectedIdx < len(commits)-1 {
if commits[selectedIdx+1].Status != models.StatusMerged {
_, idx, found := lo.FindIndexOf(commits, func(c *models.Commit) bool {
return c.Status == models.StatusMerged
})
if found {
return idx - 1
}
}
var branchesBelow []*models.Branch
if self.GetShowBranchHeads() {
branchesBelow = branchstack.BranchesBelow(
commits, self.c.Model().Branches, self.GetRef().RefName(), self.c.UserConfig().Git.MainBranches)
}
return self.list.Len() - 1
return indexForGotoBottomInCommits(
commits, self.GetSelectedLineIdx(), branchesBelow, git_commands.NewNullBisectInfo())
}
@@ -1,77 +0,0 @@
package helpers
import (
"github.com/jesseduffield/generics/set"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/samber/lo"
)
// Returns the branches stacked below the given branch: those whose tip is one
// of the given commits of the branch that hasn't been merged into a main
// branch yet. The branch itself and the main branches are left out. The
// result is in the order in which the tips appear in commits, so the branch
// closest to the given one comes first.
//
// A commit counts as merged if the remote branch of a main branch contains it
// (this is what the commit's status says), but also if the local main branch
// contains it. So if a branch was merged into the local main branch, it isn't
// stacked below a branch on top of that main branch, and if the given branch
// is itself a main branch, nothing is stacked below it.
//
// Only the commits passed in are looked at, so a branch whose tip is further
// down the history than the loaded commits is not found.
func BranchesBelowInStack(
commits []*models.Commit,
branches []*models.Branch,
branch *models.Branch,
mainBranches []string,
) []*models.Branch {
branchesByTip := map[string][]*models.Branch{}
mainBranchTips := []string{}
for _, other := range branches {
if lo.Contains(mainBranches, other.Name) {
mainBranchTips = append(mainBranchTips, other.CommitHash)
} else if other.Name != branch.Name {
branchesByTip[other.CommitHash] = append(branchesByTip[other.CommitHash], other)
}
}
mergedLocally := reachableCommits(commits, mainBranchTips)
result := []*models.Branch{}
for _, commit := range commits {
if commit.IsTODO() || commit.Status == models.StatusMerged || mergedLocally.Includes(commit.Hash()) {
continue
}
result = append(result, branchesByTip[commit.Hash()]...)
}
return result
}
// Returns the hashes of those of the given commits that can be reached from
// any of the given tips by following their parents
func reachableCommits(commits []*models.Commit, tips []string) *set.Set[string] {
commitsByHash := map[string]*models.Commit{}
for _, commit := range commits {
if !commit.IsTODO() {
commitsByHash[commit.Hash()] = commit
}
}
reachable := set.New[string]()
pending := append([]string{}, tips...)
for len(pending) > 0 {
hash := pending[len(pending)-1]
pending = pending[:len(pending)-1]
commit, ok := commitsByHash[hash]
if !ok || reachable.Includes(hash) {
continue
}
reachable.Add(hash)
pending = append(pending, commit.Parents()...)
}
return reachable
}
+5 -5
View File
@@ -8,7 +8,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/controllers/helpers"
"github.com/jesseduffield/lazygit/pkg/gui/branchstack"
"github.com/jesseduffield/lazygit/pkg/gui/style"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/utils"
@@ -128,8 +128,8 @@ func (self *SyncController) pushCurrentBranch(currentBranch *models.Branch) erro
// The branches stacked below the current one that have commits to push
func (self *SyncController) unpushedBranchesBelow(currentBranch *models.Branch) []*models.Branch {
branchesBelow := helpers.BranchesBelowInStack(
self.c.Model().Commits, self.c.Model().Branches, currentBranch, self.c.UserConfig().Git.MainBranches)
branchesBelow := branchstack.BranchesBelow(
self.c.Model().Commits, self.c.Model().Branches, currentBranch.Name, self.c.UserConfig().Git.MainBranches)
return lo.Filter(branchesBelow, func(branch *models.Branch, _ int) bool {
// Pushing a branch whose remote branch was deleted would recreate it.
@@ -289,8 +289,8 @@ func (self *SyncController) pullCurrentBranch(currentBranch *models.Branch, bran
// upstream branches without losing anything: those that are behind them, and
// those that diverged from them only because they were rewritten
func (self *SyncController) updatableBranchesBelow(currentBranch *models.Branch) []*models.Branch {
branchesBelow := helpers.BranchesBelowInStack(
self.c.Model().Commits, self.c.Model().Branches, currentBranch, self.c.UserConfig().Git.MainBranches)
branchesBelow := branchstack.BranchesBelow(
self.c.Model().Commits, self.c.Model().Branches, currentBranch.Name, self.c.UserConfig().Git.MainBranches)
return lo.Filter(branchesBelow, func(branch *models.Branch, _ int) bool {
// Updating a branch that is checked out in another worktree changes
+9 -2
View File
@@ -9,6 +9,7 @@ import (
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/common"
"github.com/jesseduffield/lazygit/pkg/gui/branchstack"
"github.com/jesseduffield/lazygit/pkg/gui/presentation/authors"
"github.com/jesseduffield/lazygit/pkg/gui/presentation/graph"
"github.com/jesseduffield/lazygit/pkg/gui/presentation/icons"
@@ -182,6 +183,9 @@ func GetCommitListDisplayStrings(
commits, common.UserConfig().Gui.CommitHashLength, fullDescription,
timeFormat, shortTimeFormat, now, bisectInfo)
mergedIntoLocalMainBranch := branchstack.CommitsMergedIntoLocalMainBranch(
commits, branches, common.UserConfig().Git.MainBranches)
lines := make([][]string, 0, len(filteredCommits))
var bisectStatus BisectStatus
willBeRebased := markedBaseCommit == ""
@@ -196,6 +200,7 @@ func GetCommitListDisplayStrings(
common,
commit,
branchHeadsToVisualize,
mergedIntoLocalMainBranch.Includes(commit.Hash()),
hasRebaseUpdateRefsConfig,
cherryPickedCommitHashSet,
isMarkedBaseCommit,
@@ -401,6 +406,7 @@ func displayCommit(
common *common.Common,
commit *models.Commit,
branchHeadsToVisualize *set.Set[string],
isMergedIntoLocalMainBranch bool,
hasRebaseUpdateRefsConfig bool,
cherryPickedCommitHashSet *set.Set[string],
isMarkedBaseCommit bool,
@@ -457,8 +463,9 @@ func displayCommit(
}
if branchHeadsToVisualize.Includes(commit.Hash()) &&
// Don't show branch head on commits that are already merged to a main branch
commit.Status != models.StatusMerged &&
// Don't show branch head on commits that are already merged to a
// main branch, be it its remote branch or the local one
commit.Status != models.StatusMerged && !isMergedIntoLocalMainBranch &&
// Don't show branch head on a "pick" todo if the rebase.updateRefs config is on
!(commit.IsTODO() && hasRebaseUpdateRefsConfig) {
tagString = style.FgCyan.SetBold().Sprint(
+31
View File
@@ -126,6 +126,37 @@ func TestGetCommitListDisplayStrings(t *testing.T) {
hash4 commit4
`),
},
{
testName: "don't show branch heads of branches merged into a local main branch",
commitOpts: []models.NewCommitOpts{
{Name: "commit1", Hash: "hash1", Parents: []string{"hash2"}},
{Name: "commit2", Hash: "hash2", Parents: []string{"hash3"}},
{Name: "commit3", Hash: "hash3", Parents: []string{"hash4"}},
{Name: "commit4", Hash: "hash4", Parents: []string{"hash5"}},
{Name: "commit5", Hash: "hash5", Status: models.StatusMerged},
},
branches: []*models.Branch{
{Name: "current-branch", CommitHash: "hash1", Head: true},
{Name: "other-branch", CommitHash: "hash2", Head: false},
{Name: "master", CommitHash: "hash3", Head: false},
{Name: "merged-into-local-master", CommitHash: "hash4", Head: false},
},
currentBranchName: "current-branch",
hasUpdateRefConfig: true,
startIdx: 0,
endIdx: 5,
showGraph: false,
bisectInfo: git_commands.NewNullBisectInfo(),
cherryPickedCommitHashSet: set.New[string](),
now: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
expected: formatExpected(`
hash1 commit1
hash2 * commit2
hash3 commit3
hash4 commit4
hash5 commit5
`),
},
{
testName: "show local branch head for head commit if updateRefs is on",
commitOpts: []models.NewCommitOpts{
@@ -0,0 +1,47 @@
package bisect
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var GotoBottom = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Go to the bottom of the commits list in steps, stopping only at the bad commit, the current commit, and the first good commit",
ExtraCmdArgs: []string{},
Skip: false,
SetupRepo: func(shell *Shell) {
shell.
CreateNCommits(3).
NewBranch("mybranch").
CreateNCommitsStartingAt(7, 4).
StartBisect("HEAD~1", "HEAD~8")
},
SetupConfig: func(cfg *config.AppConfig) {
cfg.GetUserConfig().Git.Log.ShowGraph = "never"
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Commits().
Focus().
Lines(
Contains("CI commit-10"),
Contains("CI commit-09").Contains("<-- bad"),
Contains("CI commit-08"),
Contains("CI commit-07"),
Contains("CI commit-06"),
Contains("CI commit-05").Contains("<-- current").IsSelected(),
Contains("CI commit-04"),
Contains("CI commit-03"),
Contains("CI commit-02").Contains("<-- good"),
Contains("CI commit-01"),
).
Press(keys.Universal.GotoTop).
Press(keys.Universal.GotoBottom).
SelectedLine(Contains("CI commit-09")).
Press(keys.Universal.GotoBottom).
SelectedLine(Contains("CI commit-05")).
Press(keys.Universal.GotoBottom).
SelectedLine(Contains("CI commit-02")).
Press(keys.Universal.GotoBottom).
SelectedLine(Contains("CI commit-01"))
},
})
@@ -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"))
},
})
+2
View File
@@ -39,6 +39,7 @@ var tests = []*components.IntegrationTest{
bisect.CheckOutBranchBeforeBisecting,
bisect.ChooseTerms,
bisect.FromOtherBranch,
bisect.GotoBottom,
bisect.MarkCandidatesWhileTestingMergeBase,
bisect.OfferResetWhenMergeBaseIsBad,
bisect.SelectCurrentCommit,
@@ -156,6 +157,7 @@ var tests = []*components.IntegrationTest{
commit.FindBaseCommitForFixupDisregardMainBranch,
commit.FindBaseCommitForFixupOnlyAddedLines,
commit.FindBaseCommitForFixupWarningForAddedLines,
commit.GotoBottomInBranchStack,
commit.Highlight,
commit.History,
commit.HistoryComplex,