mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-11-14 00:33:35 -04:00
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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
+3
-3
@@ -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)
|
||||
})
|
||||
@@ -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()))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,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(
|
||||
|
||||
@@ -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"))
|
||||
},
|
||||
})
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user