From 6eaece3696daf435facc5a1ddf0fb6e84e0c7683 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Tue, 30 Apr 2024 12:34:05 +0200 Subject: [PATCH] Show divergence from base branch in branches list --- pkg/commands/git_commands/branch_loader.go | 44 ++++++++++++++++++- pkg/commands/models/branch.go | 10 ++++- pkg/gui/controllers/helpers/refresh_helper.go | 15 ++++++- pkg/gui/presentation/branches.go | 43 +++++++++--------- pkg/integration/tests/sync/force_push.go | 2 +- .../sync/force_push_multiple_matching.go | 2 +- .../sync/force_push_multiple_upstream.go | 2 +- pkg/integration/tests/sync/pull.go | 2 +- .../tests/sync/pull_and_set_upstream.go | 2 +- pkg/integration/tests/sync/pull_merge.go | 2 +- .../tests/sync/pull_merge_conflict.go | 2 +- pkg/integration/tests/sync/pull_rebase.go | 2 +- .../tests/sync/pull_rebase_conflict.go | 2 +- .../sync/pull_rebase_interactive_conflict.go | 2 +- .../pull_rebase_interactive_conflict_drop.go | 2 +- 15 files changed, 98 insertions(+), 36 deletions(-) diff --git a/pkg/commands/git_commands/branch_loader.go b/pkg/commands/git_commands/branch_loader.go index eaf93e1a1..2f9a4dd6e 100644 --- a/pkg/commands/git_commands/branch_loader.go +++ b/pkg/commands/git_commands/branch_loader.go @@ -1,6 +1,7 @@ package git_commands import ( + "errors" "fmt" "regexp" "strconv" @@ -60,7 +61,7 @@ func NewBranchLoader( } // Load the list of branches for the current repo -func (self *BranchLoader) Load(reflogCommits []*models.Commit) ([]*models.Branch, error) { +func (self *BranchLoader) Load(reflogCommits []*models.Commit, existingMainBranches *ExistingMainBranches, oldBranches []*models.Branch, onWorker func(func() error), renderFunc func()) ([]*models.Branch, error) { branches := self.obtainBranches() if self.AppState.LocalBranchSortOrder == "recency" { @@ -119,8 +120,49 @@ func (self *BranchLoader) Load(reflogCommits []*models.Commit) ([]*models.Branch branch.UpstreamRemote = match.Remote branch.UpstreamBranch = match.Merge.Short() } + + if oldBranch, found := lo.Find(oldBranches, func(b *models.Branch) bool { + return b.Name == branch.Name + }); found { + branch.BehindBaseBranch.Store(oldBranch.BehindBaseBranch.Load()) + } } + onWorker(func() error { + mainBranches := existingMainBranches.Get() + if len(mainBranches) > 0 { + for _, branch := range branches { + baseBranch, err := self.GetBaseBranch(branch, existingMainBranches) + if err != nil { + return err + } + if baseBranch == "" { + continue + } + output, err := self.cmd.New( + NewGitCmd("rev-list"). + Arg("--left-right"). + Arg("--count"). + Arg(fmt.Sprintf("%s...%s", branch.FullRefName(), baseBranch)). + ToArgv(), + ).DontLog().RunWithOutput() + if err != nil { + return err + } + aheadBehindStr := strings.Split(strings.TrimSpace(output), "\t") + if len(aheadBehindStr) != 2 { + return errors.New("unexpected output from git rev-list") + } + if behind, err := strconv.Atoi(aheadBehindStr[1]); err == nil { + branch.BehindBaseBranch.Store(int32(behind)) + renderFunc() + } + } + } + + return nil + }) + return branches, nil } diff --git a/pkg/commands/models/branch.go b/pkg/commands/models/branch.go index c5fcfdaed..9ec699689 100644 --- a/pkg/commands/models/branch.go +++ b/pkg/commands/models/branch.go @@ -1,6 +1,9 @@ package models -import "fmt" +import ( + "fmt" + "sync/atomic" +) // Branch : A git branch // duplicating this for now @@ -28,6 +31,11 @@ type Branch struct { Subject string // commit hash CommitHash string + + // How far we have fallen behind our base branch. 0 means either not + // determined yet, or up to date with base branch. (We don't need to + // distinguish the two, as we don't draw anything in both cases.) + BehindBaseBranch atomic.Int32 } func (b *Branch) FullRefName() string { diff --git a/pkg/gui/controllers/helpers/refresh_helper.go b/pkg/gui/controllers/helpers/refresh_helper.go index 83ae2f15b..8f6f289c7 100644 --- a/pkg/gui/controllers/helpers/refresh_helper.go +++ b/pkg/gui/controllers/helpers/refresh_helper.go @@ -452,7 +452,20 @@ func (self *RefreshHelper) refreshBranches(refreshWorktrees bool, keepBranchSele } } - branches, err := self.c.Git().Loaders.BranchLoader.Load(reflogCommits) + branches, err := self.c.Git().Loaders.BranchLoader.Load(reflogCommits, self.c.Model().ExistingMainBranches, self.c.Model().Branches, + func(f func() error) { + self.c.OnWorker(func(_ gocui.Task) error { + return f() + }) + }, + func() { + self.c.OnUIThread(func() error { + if err := self.c.Contexts().Branches.HandleRender(); err != nil { + self.c.Log.Error(err) + } + return nil + }) + }) if err != nil { self.c.Log.Error(err) } diff --git a/pkg/gui/presentation/branches.go b/pkg/gui/presentation/branches.go index 17347a6ce..9586672ee 100644 --- a/pkg/gui/presentation/branches.go +++ b/pkg/gui/presentation/branches.go @@ -155,32 +155,31 @@ func BranchStatus( return style.FgCyan.Sprintf("%s %s", itemOperationStr, utils.Loader(now, userConfig.Gui.Spinner)) } - if !branch.IsTrackingRemote() { - return "" + result := "" + if branch.IsTrackingRemote() { + if branch.UpstreamGone { + result = style.FgRed.Sprint(tr.UpstreamGone) + } else if branch.MatchesUpstream() { + result = style.FgGreen.Sprint("✓") + } else if branch.RemoteBranchNotStoredLocally() { + result = style.FgMagenta.Sprint("?") + } else if branch.HasCommitsToPull() && branch.HasCommitsToPush() { + result = style.FgYellow.Sprintf("↓%s↑%s", branch.Pullables, branch.Pushables) + } else if branch.HasCommitsToPull() { + result = style.FgYellow.Sprintf("↓%s", branch.Pullables) + } else if branch.HasCommitsToPush() { + result = style.FgYellow.Sprintf("↑%s", branch.Pushables) + } } - if branch.UpstreamGone { - return style.FgRed.Sprint(tr.UpstreamGone) + if v := branch.BehindBaseBranch.Load(); v != 0 { + if result != "" { + result += " " + } + result += style.FgCyan.Sprintf("↓%d", v) } - if branch.MatchesUpstream() { - return style.FgGreen.Sprint("✓") - } - if branch.RemoteBranchNotStoredLocally() { - return style.FgMagenta.Sprint("?") - } - - if branch.HasCommitsToPull() && branch.HasCommitsToPush() { - return style.FgYellow.Sprintf("↓%s↑%s", branch.Pullables, branch.Pushables) - } - if branch.HasCommitsToPull() { - return style.FgYellow.Sprintf("↓%s", branch.Pullables) - } - if branch.HasCommitsToPush() { - return style.FgYellow.Sprintf("↑%s", branch.Pushables) - } - - return "" + return result } func SetCustomBranches(customBranchColors map[string]string) { diff --git a/pkg/integration/tests/sync/force_push.go b/pkg/integration/tests/sync/force_push.go index e563cfd28..ee258a0ee 100644 --- a/pkg/integration/tests/sync/force_push.go +++ b/pkg/integration/tests/sync/force_push.go @@ -40,7 +40,7 @@ var ForcePush = NewIntegrationTest(NewIntegrationTestArgs{ Contains("one"), ) - t.Views().Status().Content(Equals("✓ repo → master")) + t.Views().Status().Content(Equals("✓ ↓1 repo → master")) t.Views().Remotes().Focus(). Lines(Contains("origin")). diff --git a/pkg/integration/tests/sync/force_push_multiple_matching.go b/pkg/integration/tests/sync/force_push_multiple_matching.go index 63825ee4f..1060b4801 100644 --- a/pkg/integration/tests/sync/force_push_multiple_matching.go +++ b/pkg/integration/tests/sync/force_push_multiple_matching.go @@ -42,7 +42,7 @@ var ForcePushMultipleMatching = NewIntegrationTest(NewIntegrationTestArgs{ Contains("one"), ) - t.Views().Status().Content(Equals("✓ repo → master")) + t.Views().Status().Content(Equals("✓ ↓1 repo → master")) t.Views().Branches(). Lines( diff --git a/pkg/integration/tests/sync/force_push_multiple_upstream.go b/pkg/integration/tests/sync/force_push_multiple_upstream.go index 8c55b7e8c..d13ac7c81 100644 --- a/pkg/integration/tests/sync/force_push_multiple_upstream.go +++ b/pkg/integration/tests/sync/force_push_multiple_upstream.go @@ -41,7 +41,7 @@ var ForcePushMultipleUpstream = NewIntegrationTest(NewIntegrationTestArgs{ Contains("one"), ) - t.Views().Status().Content(Equals("✓ repo → master")) + t.Views().Status().Content(Equals("✓ ↓1 repo → master")) t.Views().Branches(). Lines( diff --git a/pkg/integration/tests/sync/pull.go b/pkg/integration/tests/sync/pull.go index b30cbb408..0d820a79f 100644 --- a/pkg/integration/tests/sync/pull.go +++ b/pkg/integration/tests/sync/pull.go @@ -36,6 +36,6 @@ var Pull = NewIntegrationTest(NewIntegrationTestArgs{ Contains("one"), ) - t.Views().Status().Content(Equals("✓ repo → master")) + t.Views().Status().Content(Equals("✓ ↓1 repo → master")) }, }) diff --git a/pkg/integration/tests/sync/pull_and_set_upstream.go b/pkg/integration/tests/sync/pull_and_set_upstream.go index acffa24be..5953505c7 100644 --- a/pkg/integration/tests/sync/pull_and_set_upstream.go +++ b/pkg/integration/tests/sync/pull_and_set_upstream.go @@ -40,6 +40,6 @@ var PullAndSetUpstream = NewIntegrationTest(NewIntegrationTestArgs{ Contains("one"), ) - t.Views().Status().Content(Equals("✓ repo → master")) + t.Views().Status().Content(Equals("✓ ↓1 repo → master")) }, }) diff --git a/pkg/integration/tests/sync/pull_merge.go b/pkg/integration/tests/sync/pull_merge.go index 39e447ebc..6016f0733 100644 --- a/pkg/integration/tests/sync/pull_merge.go +++ b/pkg/integration/tests/sync/pull_merge.go @@ -39,7 +39,7 @@ var PullMerge = NewIntegrationTest(NewIntegrationTestArgs{ IsFocused(). Press(keys.Universal.Pull) - t.Views().Status().Content(Equals("↑2 repo → master")) + t.Views().Status().Content(Equals("↑2 ↓2 repo → master")) t.Views().Commits(). Lines( diff --git a/pkg/integration/tests/sync/pull_merge_conflict.go b/pkg/integration/tests/sync/pull_merge_conflict.go index 2161f6abd..01bdfe4c4 100644 --- a/pkg/integration/tests/sync/pull_merge_conflict.go +++ b/pkg/integration/tests/sync/pull_merge_conflict.go @@ -62,7 +62,7 @@ var PullMergeConflict = NewIntegrationTest(NewIntegrationTestArgs{ t.Common().ContinueOnConflictsResolved() - t.Views().Status().Content(Equals("↑2 repo → master")) + t.Views().Status().Content(Equals("↑2 ↓2 repo → master")) t.Views().Commits(). Focus(). diff --git a/pkg/integration/tests/sync/pull_rebase.go b/pkg/integration/tests/sync/pull_rebase.go index a2657ffe6..74698b1dd 100644 --- a/pkg/integration/tests/sync/pull_rebase.go +++ b/pkg/integration/tests/sync/pull_rebase.go @@ -41,7 +41,7 @@ var PullRebase = NewIntegrationTest(NewIntegrationTestArgs{ IsFocused(). Press(keys.Universal.Pull) - t.Views().Status().Content(Equals("↑1 repo → master")) + t.Views().Status().Content(Equals("↑1 ↓2 repo → master")) t.Views().Commits(). Lines( diff --git a/pkg/integration/tests/sync/pull_rebase_conflict.go b/pkg/integration/tests/sync/pull_rebase_conflict.go index d9541e0ed..0b27caca8 100644 --- a/pkg/integration/tests/sync/pull_rebase_conflict.go +++ b/pkg/integration/tests/sync/pull_rebase_conflict.go @@ -63,7 +63,7 @@ var PullRebaseConflict = NewIntegrationTest(NewIntegrationTestArgs{ t.Common().ContinueOnConflictsResolved() - t.Views().Status().Content(Equals("↑1 repo → master")) + t.Views().Status().Content(Equals("↑1 ↓2 repo → master")) t.Views().Commits(). Focus(). diff --git a/pkg/integration/tests/sync/pull_rebase_interactive_conflict.go b/pkg/integration/tests/sync/pull_rebase_interactive_conflict.go index bf0fc050b..8d3cc89b9 100644 --- a/pkg/integration/tests/sync/pull_rebase_interactive_conflict.go +++ b/pkg/integration/tests/sync/pull_rebase_interactive_conflict.go @@ -76,7 +76,7 @@ var PullRebaseInteractiveConflict = NewIntegrationTest(NewIntegrationTestArgs{ t.Common().ContinueOnConflictsResolved() - t.Views().Status().Content(Equals("↑2 repo → master")) + t.Views().Status().Content(Equals("↑2 ↓2 repo → master")) t.Views().Commits(). Focus(). diff --git a/pkg/integration/tests/sync/pull_rebase_interactive_conflict_drop.go b/pkg/integration/tests/sync/pull_rebase_interactive_conflict_drop.go index 3eee12efd..d5075a54d 100644 --- a/pkg/integration/tests/sync/pull_rebase_interactive_conflict_drop.go +++ b/pkg/integration/tests/sync/pull_rebase_interactive_conflict_drop.go @@ -85,7 +85,7 @@ var PullRebaseInteractiveConflictDrop = NewIntegrationTest(NewIntegrationTestArg t.Common().ContinueOnConflictsResolved() - t.Views().Status().Content(Equals("↑1 repo → master")) + t.Views().Status().Content(Equals("↑1 ↓2 repo → master")) t.Views().Commits(). Focus().