Skip to content

Commit cb0a9a7

Browse files
committed
Show divergence from base branch in branches list
1 parent aa9da43 commit cb0a9a7

File tree

11 files changed

+283
-55
lines changed

11 files changed

+283
-55
lines changed

docs/Config.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,10 @@ gui:
187187
# If true, show commit hashes alongside branch names in the branches view.
188188
showBranchCommitHash: false
189189

190+
# Whether to show the divergence from the base branch in the branches view.
191+
# One of: 'none' | 'onlyArrow' | 'arrowAndNumber'
192+
showDivergenceFromBaseBranch: none
193+
190194
# Height of the command log view
191195
commandLogSize: 8
192196

pkg/commands/git_commands/branch_loader.go

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"regexp"
66
"strconv"
77
"strings"
8+
"time"
89

910
"github.com/jesseduffield/generics/set"
1011
"github.com/jesseduffield/go-git/v5/config"
@@ -14,6 +15,7 @@ import (
1415
"github.com/jesseduffield/lazygit/pkg/utils"
1516
"github.com/samber/lo"
1617
"golang.org/x/exp/slices"
18+
"golang.org/x/sync/errgroup"
1719
)
1820

1921
// context:
@@ -63,7 +65,13 @@ func NewBranchLoader(
6365
}
6466

6567
// Load the list of branches for the current repo
66-
func (self *BranchLoader) Load(reflogCommits []*models.Commit) ([]*models.Branch, error) {
68+
func (self *BranchLoader) Load(reflogCommits []*models.Commit,
69+
mainBranches *MainBranches,
70+
oldBranches []*models.Branch,
71+
loadBehindCounts bool,
72+
onWorker func(func() error),
73+
renderFunc func(),
74+
) ([]*models.Branch, error) {
6775
branches := self.obtainBranches(self.version.IsAtLeast(2, 22, 0))
6876

6977
if self.AppState.LocalBranchSortOrder == "recency" {
@@ -122,11 +130,75 @@ func (self *BranchLoader) Load(reflogCommits []*models.Commit) ([]*models.Branch
122130
branch.UpstreamRemote = match.Remote
123131
branch.UpstreamBranch = match.Merge.Short()
124132
}
133+
134+
// If the branch already existed, take over its BehindBaseBranch value
135+
// to reduce flicker
136+
if oldBranch, found := lo.Find(oldBranches, func(b *models.Branch) bool {
137+
return b.Name == branch.Name
138+
}); found {
139+
branch.BehindBaseBranch.Store(oldBranch.BehindBaseBranch.Load())
140+
}
141+
}
142+
143+
if loadBehindCounts && self.UserConfig.Gui.ShowDivergenceFromBaseBranch != "none" {
144+
onWorker(func() error {
145+
return self.GetBehindBaseBranchValuesForAllBranches(branches, mainBranches, renderFunc)
146+
})
125147
}
126148

127149
return branches, nil
128150
}
129151

152+
func (self *BranchLoader) GetBehindBaseBranchValuesForAllBranches(
153+
branches []*models.Branch,
154+
mainBranches *MainBranches,
155+
renderFunc func(),
156+
) error {
157+
mainBranchRefs := mainBranches.Get()
158+
if len(mainBranchRefs) == 0 {
159+
return nil
160+
}
161+
162+
t := time.Now()
163+
errg := errgroup.Group{}
164+
165+
for _, branch := range branches {
166+
errg.Go(func() error {
167+
baseBranch, err := self.GetBaseBranch(branch, mainBranches)
168+
if err != nil {
169+
return err
170+
}
171+
behind := 0 // prime it in case something below fails
172+
if baseBranch != "" {
173+
output, err := self.cmd.New(
174+
NewGitCmd("rev-list").
175+
Arg("--left-right").
176+
Arg("--count").
177+
Arg(fmt.Sprintf("%s...%s", branch.FullRefName(), baseBranch)).
178+
ToArgv(),
179+
).DontLog().RunWithOutput()
180+
if err != nil {
181+
return err
182+
}
183+
// The format of the output is "<ahead>\t<behind>"
184+
aheadBehindStr := strings.Split(strings.TrimSpace(output), "\t")
185+
if len(aheadBehindStr) == 2 {
186+
if value, err := strconv.Atoi(aheadBehindStr[1]); err == nil {
187+
behind = value
188+
}
189+
}
190+
}
191+
branch.BehindBaseBranch.Store(int32(behind))
192+
return nil
193+
})
194+
}
195+
196+
err := errg.Wait()
197+
self.Log.Debugf("time to get behind base branch values for all branches: %s", time.Since(t))
198+
renderFunc()
199+
return err
200+
}
201+
130202
// Find the base branch for the given branch (i.e. the main branch that the
131203
// given branch was forked off of)
132204
//

pkg/commands/models/branch.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package models
22

3-
import "fmt"
3+
import (
4+
"fmt"
5+
"sync/atomic"
6+
)
47

58
// Branch : A git branch
69
// duplicating this for now
@@ -32,6 +35,11 @@ type Branch struct {
3235
Subject string
3336
// commit hash
3437
CommitHash string
38+
39+
// How far we have fallen behind our base branch. 0 means either not
40+
// determined yet, or up to date with base branch. (We don't need to
41+
// distinguish the two, as we don't draw anything in both cases.)
42+
BehindBaseBranch atomic.Int32
3543
}
3644

3745
func (b *Branch) FullRefName() string {

pkg/config/user_config.go

Lines changed: 25 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,9 @@ type GuiConfig struct {
129129
CommitHashLength int `yaml:"commitHashLength" jsonschema:"minimum=0"`
130130
// If true, show commit hashes alongside branch names in the branches view.
131131
ShowBranchCommitHash bool `yaml:"showBranchCommitHash"`
132+
// Whether to show the divergence from the base branch in the branches view.
133+
// One of: 'none' | 'onlyArrow' | 'arrowAndNumber'
134+
ShowDivergenceFromBaseBranch string `yaml:"showDivergenceFromBaseBranch" jsonschema:"enum=none,enum=onlyArrow,enum=arrowAndNumber"`
132135
// Height of the command log view
133136
CommandLogSize int `yaml:"commandLogSize" jsonschema:"minimum=0"`
134137
// Whether to split the main window when viewing file changes.
@@ -673,27 +676,28 @@ func GetDefaultConfig() *UserConfig {
673676
UnstagedChangesColor: []string{"red"},
674677
DefaultFgColor: []string{"default"},
675678
},
676-
CommitLength: CommitLengthConfig{Show: true},
677-
SkipNoStagedFilesWarning: false,
678-
ShowListFooter: true,
679-
ShowCommandLog: true,
680-
ShowBottomLine: true,
681-
ShowPanelJumps: true,
682-
ShowFileTree: true,
683-
ShowRandomTip: true,
684-
ShowIcons: false,
685-
NerdFontsVersion: "",
686-
ShowFileIcons: true,
687-
CommitHashLength: 8,
688-
ShowBranchCommitHash: false,
689-
CommandLogSize: 8,
690-
SplitDiff: "auto",
691-
SkipRewordInEditorWarning: false,
692-
WindowSize: "normal",
693-
Border: "rounded",
694-
AnimateExplosion: true,
695-
PortraitMode: "auto",
696-
FilterMode: "substring",
679+
CommitLength: CommitLengthConfig{Show: true},
680+
SkipNoStagedFilesWarning: false,
681+
ShowListFooter: true,
682+
ShowCommandLog: true,
683+
ShowBottomLine: true,
684+
ShowPanelJumps: true,
685+
ShowFileTree: true,
686+
ShowRandomTip: true,
687+
ShowIcons: false,
688+
NerdFontsVersion: "",
689+
ShowFileIcons: true,
690+
CommitHashLength: 8,
691+
ShowBranchCommitHash: false,
692+
ShowDivergenceFromBaseBranch: "none",
693+
CommandLogSize: 8,
694+
SplitDiff: "auto",
695+
SkipRewordInEditorWarning: false,
696+
WindowSize: "normal",
697+
Border: "rounded",
698+
AnimateExplosion: true,
699+
PortraitMode: "auto",
700+
FilterMode: "substring",
697701
Spinner: SpinnerConfig{
698702
Frames: []string{"|", "/", "-", "\\"},
699703
Rate: 50,

pkg/config/user_config_validation.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,12 @@ import (
77
)
88

99
func (config *UserConfig) Validate() error {
10-
if err := validateEnum("gui.statusPanelView", config.Gui.StatusPanelView, []string{"dashboard", "allBranchesLog"}); err != nil {
10+
if err := validateEnum("gui.statusPanelView", config.Gui.StatusPanelView,
11+
[]string{"dashboard", "allBranchesLog"}); err != nil {
12+
return err
13+
}
14+
if err := validateEnum("gui.showDivergenceFromBaseBranch", config.Gui.ShowDivergenceFromBaseBranch,
15+
[]string{"none", "onlyArrow", "arrowAndNumber"}); err != nil {
1116
return err
1217
}
1318
return nil

pkg/gui/controllers/helpers/refresh_helper.go

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ func (self *RefreshHelper) Refresh(options types.RefreshOptions) error {
130130
if self.c.AppState.LocalBranchSortOrder == "recency" {
131131
refresh("reflog and branches", func() { self.refreshReflogAndBranches(includeWorktreesWithBranches, options.KeepBranchSelectionIndex) })
132132
} else {
133-
refresh("branches", func() { self.refreshBranches(includeWorktreesWithBranches, options.KeepBranchSelectionIndex) })
133+
refresh("branches", func() { self.refreshBranches(includeWorktreesWithBranches, options.KeepBranchSelectionIndex, true) })
134134
refresh("reflog", func() { _ = self.refreshReflogCommits() })
135135
}
136136
} else if scopeSet.Includes(types.REBASE_COMMITS) {
@@ -256,7 +256,7 @@ func (self *RefreshHelper) refreshReflogCommitsConsideringStartup() {
256256
case types.INITIAL:
257257
self.c.OnWorker(func(_ gocui.Task) error {
258258
_ = self.refreshReflogCommits()
259-
self.refreshBranches(false, true)
259+
self.refreshBranches(false, true, true)
260260
self.c.State().GetRepoState().SetStartupStage(types.COMPLETE)
261261
return nil
262262
})
@@ -269,7 +269,7 @@ func (self *RefreshHelper) refreshReflogCommitsConsideringStartup() {
269269
func (self *RefreshHelper) refreshReflogAndBranches(refreshWorktrees bool, keepBranchSelectionIndex bool) {
270270
self.refreshReflogCommitsConsideringStartup()
271271

272-
self.refreshBranches(refreshWorktrees, keepBranchSelectionIndex)
272+
self.refreshBranches(refreshWorktrees, keepBranchSelectionIndex, false)
273273
}
274274

275275
func (self *RefreshHelper) refreshCommitsAndCommitFiles() {
@@ -438,7 +438,7 @@ func (self *RefreshHelper) refreshStateSubmoduleConfigs() error {
438438

439439
// self.refreshStatus is called at the end of this because that's when we can
440440
// be sure there is a State.Model.Branches array to pick the current branch from
441-
func (self *RefreshHelper) refreshBranches(refreshWorktrees bool, keepBranchSelectionIndex bool) {
441+
func (self *RefreshHelper) refreshBranches(refreshWorktrees bool, keepBranchSelectionIndex bool, loadBehindCounts bool) {
442442
self.c.Mutexes().RefreshingBranchesMutex.Lock()
443443
defer self.c.Mutexes().RefreshingBranchesMutex.Unlock()
444444

@@ -457,7 +457,25 @@ func (self *RefreshHelper) refreshBranches(refreshWorktrees bool, keepBranchSele
457457
}
458458
}
459459

460-
branches, err := self.c.Git().Loaders.BranchLoader.Load(reflogCommits)
460+
branches, err := self.c.Git().Loaders.BranchLoader.Load(
461+
reflogCommits,
462+
self.c.Model().MainBranches,
463+
self.c.Model().Branches,
464+
loadBehindCounts,
465+
func(f func() error) {
466+
self.c.OnWorker(func(_ gocui.Task) error {
467+
return f()
468+
})
469+
},
470+
func() {
471+
self.c.OnUIThread(func() error {
472+
if err := self.c.Contexts().Branches.HandleRender(); err != nil {
473+
self.c.Log.Error(err)
474+
}
475+
self.refreshStatus()
476+
return nil
477+
})
478+
})
461479
if err != nil {
462480
self.c.Log.Error(err)
463481
}

pkg/gui/presentation/branches.go

Lines changed: 32 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -155,32 +155,38 @@ func BranchStatus(
155155
return style.FgCyan.Sprintf("%s %s", itemOperationStr, utils.Loader(now, userConfig.Gui.Spinner))
156156
}
157157

158-
if !branch.IsTrackingRemote() {
159-
return ""
160-
}
161-
162-
if branch.UpstreamGone {
163-
return style.FgRed.Sprint(tr.UpstreamGone)
164-
}
165-
166-
if branch.MatchesUpstream() {
167-
return style.FgGreen.Sprint("✓")
168-
}
169-
if branch.RemoteBranchNotStoredLocally() {
170-
return style.FgMagenta.Sprint("?")
171-
}
172-
173-
if branch.IsBehindForPull() && branch.IsAheadForPull() {
174-
return style.FgYellow.Sprintf("↓%s↑%s", branch.BehindForPull, branch.AheadForPull)
175-
}
176-
if branch.IsBehindForPull() {
177-
return style.FgYellow.Sprintf("↓%s", branch.BehindForPull)
178-
}
179-
if branch.IsAheadForPull() {
180-
return style.FgYellow.Sprintf("↑%s", branch.AheadForPull)
181-
}
182-
183-
return ""
158+
result := ""
159+
if branch.IsTrackingRemote() {
160+
if branch.UpstreamGone {
161+
result = style.FgRed.Sprint(tr.UpstreamGone)
162+
} else if branch.MatchesUpstream() {
163+
result = style.FgGreen.Sprint("✓")
164+
} else if branch.RemoteBranchNotStoredLocally() {
165+
result = style.FgMagenta.Sprint("?")
166+
} else if branch.IsBehindForPull() && branch.IsAheadForPull() {
167+
result = style.FgYellow.Sprintf("↓%s↑%s", branch.BehindForPull, branch.AheadForPull)
168+
} else if branch.IsBehindForPull() {
169+
result = style.FgYellow.Sprintf("↓%s", branch.BehindForPull)
170+
} else if branch.IsAheadForPull() {
171+
result = style.FgYellow.Sprintf("↑%s", branch.AheadForPull)
172+
}
173+
}
174+
175+
if userConfig.Gui.ShowDivergenceFromBaseBranch != "none" {
176+
behind := branch.BehindBaseBranch.Load()
177+
if behind != 0 {
178+
if result != "" {
179+
result += " "
180+
}
181+
if userConfig.Gui.ShowDivergenceFromBaseBranch == "arrowAndNumber" {
182+
result += style.FgCyan.Sprintf("↓%d", behind)
183+
} else {
184+
result += style.FgCyan.Sprintf("↓")
185+
}
186+
}
187+
}
188+
189+
return result
184190
}
185191

186192
func SetCustomBranches(customBranchColors map[string]string) {

0 commit comments

Comments
 (0)