From c84a181de9b8a7f545376554690a5b8cb520e3db Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Sun, 8 Nov 2026 12:38:05 +0100 Subject: [PATCH] Extract finding the commits merged into a local main branch The commits list will need this to decide which branches to mark. It renders the list again whenever the selection changes, so stop the walk at merged commits. Their parents are merged too, and a local main branch is usually merged itself, so there is mostly nothing left to walk. Co-Authored-By: Claude Opus 5.5 --- pkg/gui/branchstack/branchstack.go | 49 +++++++++++++++++++----------- 1 file changed, 32 insertions(+), 17 deletions(-) diff --git a/pkg/gui/branchstack/branchstack.go b/pkg/gui/branchstack/branchstack.go index b9e9db75d..97e88cb31 100644 --- a/pkg/gui/branchstack/branchstack.go +++ b/pkg/gui/branchstack/branchstack.go @@ -27,16 +27,13 @@ func BranchesBelow( 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 { + if !lo.Contains(mainBranches, other.Name) && other.Name != branch.Name { branchesByTip[other.CommitHash] = append(branchesByTip[other.CommitHash], other) } } - mergedLocally := reachableCommits(commits, mainBranchTips) + mergedLocally := CommitsMergedIntoLocalMainBranch(commits, branches, mainBranches) result := []*models.Branch{} for _, commit := range commits { @@ -49,29 +46,47 @@ func BranchesBelow( 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{} +// 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() { - commitsByHash[commit.Hash()] = commit + if !commit.IsTODO() && commit.Status != models.StatusMerged { + unmergedCommitsByHash[commit.Hash()] = commit } } - reachable := set.New[string]() - pending := append([]string{}, tips...) + pending := mainBranchTips for len(pending) > 0 { hash := pending[len(pending)-1] pending = pending[:len(pending)-1] - commit, ok := commitsByHash[hash] - if !ok || reachable.Includes(hash) { + commit, ok := unmergedCommitsByHash[hash] + if !ok || result.Includes(hash) { continue } - reachable.Add(hash) + result.Add(hash) pending = append(pending, commit.Parents()...) } - return reachable + return result }