mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-11-06 13:00:59 -04:00
Don't offer to push or pull branches merged into a local main branch
When pushing master, lazygit offers to push other branches along with it if they were merged into master locally and have unpushed commits. Pulling master offers to update such branches in the same way. These branches aren't part of a stack, so the menu only adds an extra step to an ordinary push or pull. This happens because a branch counts as stacked below the current one if its tip is one of the current branch's commits that aren't merged into a main branch yet. A commit's merged status only looks at the remote branch of each main branch. So until master is pushed, the commits that were merged into it locally count as unmerged. The same goes for pushing or pulling a feature branch on top of such a master. Treat the commits that a local main branch contains as merged too, so that the search for stacked branches stops at a main branch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
c9c0db40bd
commit
fb1b6ed1c1
@@ -1,6 +1,7 @@
|
||||
package helpers
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/generics/set"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
@@ -11,6 +12,12 @@ import (
|
||||
// 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(
|
||||
@@ -20,16 +27,20 @@ func BranchesBelowInStack(
|
||||
mainBranches []string,
|
||||
) []*models.Branch {
|
||||
branchesByTip := map[string][]*models.Branch{}
|
||||
mainBranchTips := []string{}
|
||||
for _, other := range branches {
|
||||
if other.Name == branch.Name || lo.Contains(mainBranches, other.Name) {
|
||||
continue
|
||||
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)
|
||||
}
|
||||
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 {
|
||||
if commit.IsTODO() || commit.Status == models.StatusMerged || mergedLocally.Includes(commit.Hash()) {
|
||||
continue
|
||||
}
|
||||
result = append(result, branchesByTip[commit.Hash()]...)
|
||||
@@ -37,3 +48,30 @@ func BranchesBelowInStack(
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -12,8 +12,8 @@ import (
|
||||
|
||||
func TestBranchesBelowInStack(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})
|
||||
commit := func(hash string, status models.CommitStatus, parents ...string) *models.Commit {
|
||||
return models.NewCommit(hashPool, models.NewCommitOpts{Hash: hash, Status: status, Parents: parents})
|
||||
}
|
||||
todoCommit := func(hash string) *models.Commit {
|
||||
return models.NewCommit(hashPool, models.NewCommitOpts{Hash: hash, Action: todo.Pick, Status: models.StatusRebasing})
|
||||
@@ -29,6 +29,8 @@ func TestBranchesBelowInStack(t *testing.T) {
|
||||
testName string
|
||||
commits []*models.Commit
|
||||
branches []*models.Branch
|
||||
// The branch to look for branches below; the one called "current" if nil
|
||||
current *models.Branch
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
@@ -69,6 +71,41 @@ func TestBranchesBelowInStack(t *testing.T) {
|
||||
branches: []*models.Branch{current, branch("main", "c1")},
|
||||
expected: []string{},
|
||||
},
|
||||
{
|
||||
testName: "nothing is stacked below a main branch",
|
||||
commits: []*models.Commit{
|
||||
commit("m2", models.StatusUnpushed, "m1", "w1"),
|
||||
commit("w1", models.StatusUnpushed, "m1"),
|
||||
commit("m1", models.StatusMerged),
|
||||
},
|
||||
branches: []*models.Branch{branch("master", "m2"), branch("wip", "w1")},
|
||||
current: branch("master", "m2"),
|
||||
expected: []string{},
|
||||
},
|
||||
{
|
||||
testName: "branches merged into a local main branch below the current one are left out",
|
||||
commits: []*models.Commit{
|
||||
commit("c3", models.StatusUnpushed, "c2"),
|
||||
commit("c2", models.StatusUnpushed, "m2"),
|
||||
commit("m2", models.StatusUnpushed, "m1", "w1"),
|
||||
commit("w1", models.StatusUnpushed, "m1"),
|
||||
commit("m1", models.StatusMerged),
|
||||
},
|
||||
branches: []*models.Branch{current, branch("first", "c2"), branch("master", "m2"), branch("wip", "w1")},
|
||||
expected: []string{"first"},
|
||||
},
|
||||
{
|
||||
testName: "branches merged into a local main branch that was merged into the current one are left out",
|
||||
commits: []*models.Commit{
|
||||
commit("c3", models.StatusUnpushed, "c2", "m2"),
|
||||
commit("m2", models.StatusUnpushed, "m1", "w1"),
|
||||
commit("w1", models.StatusUnpushed, "m1"),
|
||||
commit("c2", models.StatusUnpushed, "m1"),
|
||||
commit("m1", models.StatusMerged),
|
||||
},
|
||||
branches: []*models.Branch{current, branch("first", "c2"), branch("master", "m2"), branch("wip", "w1")},
|
||||
expected: []string{"first"},
|
||||
},
|
||||
{
|
||||
testName: "branches whose tip is merged are left out",
|
||||
commits: []*models.Commit{
|
||||
@@ -100,7 +137,11 @@ func TestBranchesBelowInStack(t *testing.T) {
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
result := BranchesBelowInStack(s.commits, s.branches, current, mainBranches)
|
||||
branch := current
|
||||
if s.current != nil {
|
||||
branch = s.current
|
||||
}
|
||||
result := BranchesBelowInStack(s.commits, s.branches, branch, mainBranches)
|
||||
names := lo.Map(result, func(b *models.Branch, _ int) string { return b.Name })
|
||||
assert.Equal(t, s.expected, names)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user