From 19d0048cc486b087ca938a14ce2be91ea0f0a3ae Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Sat, 4 May 2024 12:35:57 +0200 Subject: [PATCH 1/7] More explicit test of status panel content MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use Equals instead of Contains for asserting the status view content. This solves the problem that we might assert Contains("↓2 repo"), but what it really shows is "↑1↓2 repo", and the test still succeeds. At best this is confusing. Also, this way we don't have to use the awkward DoesNotContain to check that it really doesn't show a checkmark. To do this, we need to fix two whitespace problems: - there was always a space at the end for no reason. Simply remove it. It was added in efb51eee96, but from looking at that diff it seems it was added accidentally. - there was a space at the beginning if the branch status was empty. This is actually a cosmetic problem, for branches without a status the text was indented by once space. Change this so that the space is added conditionally. It's a bit awkward that we have to use Decolorise here, but this will go away again later in this branch. --- pkg/gui/presentation/status.go | 8 ++++++-- .../branch/delete_remote_branch_with_credential_prompt.go | 6 +++--- pkg/integration/tests/sync/force_push.go | 4 ++-- .../tests/sync/force_push_multiple_matching.go | 4 ++-- .../tests/sync/force_push_multiple_upstream.go | 4 ++-- pkg/integration/tests/sync/pull.go | 4 ++-- pkg/integration/tests/sync/pull_and_set_upstream.go | 4 ++-- pkg/integration/tests/sync/pull_merge.go | 4 ++-- pkg/integration/tests/sync/pull_merge_conflict.go | 4 ++-- pkg/integration/tests/sync/pull_rebase.go | 4 ++-- pkg/integration/tests/sync/pull_rebase_conflict.go | 4 ++-- .../tests/sync/pull_rebase_interactive_conflict.go | 4 ++-- .../tests/sync/pull_rebase_interactive_conflict_drop.go | 4 ++-- pkg/integration/tests/sync/push.go | 2 +- pkg/integration/tests/sync/push_and_auto_set_upstream.go | 2 +- pkg/integration/tests/sync/push_and_set_upstream.go | 2 +- pkg/integration/tests/sync/push_follow_tags.go | 4 ++-- pkg/integration/tests/sync/push_no_follow_tags.go | 4 ++-- pkg/integration/tests/sync/push_with_credential_prompt.go | 6 +++--- pkg/integration/tests/sync/shared.go | 2 +- 20 files changed, 42 insertions(+), 38 deletions(-) diff --git a/pkg/gui/presentation/status.go b/pkg/gui/presentation/status.go index d3686510e..5ac591c48 100644 --- a/pkg/gui/presentation/status.go +++ b/pkg/gui/presentation/status.go @@ -10,6 +10,7 @@ import ( "github.com/jesseduffield/lazygit/pkg/gui/style" "github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/i18n" + "github.com/jesseduffield/lazygit/pkg/utils" ) func FormatStatus( @@ -24,7 +25,10 @@ func FormatStatus( status := "" if currentBranch.IsRealBranch() { - status += ColoredBranchStatus(currentBranch, itemOperation, tr, userConfig) + " " + status += ColoredBranchStatus(currentBranch, itemOperation, tr, userConfig) + if utils.Decolorise(status) != "" { + status += " " + } } if workingTreeState != enums.REBASE_MODE_NONE { @@ -40,7 +44,7 @@ func FormatStatus( } repoName = fmt.Sprintf("%s(%s%s)", repoName, icon, style.FgCyan.Sprint(linkedWorktreeName)) } - status += fmt.Sprintf("%s → %s ", repoName, name) + status += fmt.Sprintf("%s → %s", repoName, name) return status } diff --git a/pkg/integration/tests/branch/delete_remote_branch_with_credential_prompt.go b/pkg/integration/tests/branch/delete_remote_branch_with_credential_prompt.go index ad8f70c43..6e3f52028 100644 --- a/pkg/integration/tests/branch/delete_remote_branch_with_credential_prompt.go +++ b/pkg/integration/tests/branch/delete_remote_branch_with_credential_prompt.go @@ -44,7 +44,7 @@ var DeleteRemoteBranchWithCredentialPrompt = NewIntegrationTest(NewIntegrationTe Confirm() } - t.Views().Status().Content(Contains("✓ repo → mybranch")) + t.Views().Status().Content(Equals("✓ repo → mybranch")) deleteBranch() @@ -66,7 +66,7 @@ var DeleteRemoteBranchWithCredentialPrompt = NewIntegrationTest(NewIntegrationTe Content(Contains("incorrect username/password")). Confirm() - t.Views().Status().Content(Contains("✓ repo → mybranch")) + t.Views().Status().Content(Equals("✓ repo → mybranch")) // try again with correct password deleteBranch() @@ -81,7 +81,7 @@ var DeleteRemoteBranchWithCredentialPrompt = NewIntegrationTest(NewIntegrationTe Type("password"). Confirm() - t.Views().Status().Content(Contains("repo → mybranch").DoesNotContain("✓")) + t.Views().Status().Content(Equals("(upstream gone) repo → mybranch")) t.Views().Branches().TopLines(Contains("mybranch (upstream gone)")) }, }) diff --git a/pkg/integration/tests/sync/force_push.go b/pkg/integration/tests/sync/force_push.go index 89d8a8371..e563cfd28 100644 --- a/pkg/integration/tests/sync/force_push.go +++ b/pkg/integration/tests/sync/force_push.go @@ -26,7 +26,7 @@ var ForcePush = NewIntegrationTest(NewIntegrationTestArgs{ Contains("one"), ) - t.Views().Status().Content(Contains("↓1 repo → master")) + t.Views().Status().Content(Equals("↓1 repo → master")) t.Views().Files().IsFocused().Press(keys.Universal.Push) @@ -40,7 +40,7 @@ var ForcePush = NewIntegrationTest(NewIntegrationTestArgs{ Contains("one"), ) - t.Views().Status().Content(Contains("✓ repo → master")) + t.Views().Status().Content(Equals("✓ 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 37c43f264..63825ee4f 100644 --- a/pkg/integration/tests/sync/force_push_multiple_matching.go +++ b/pkg/integration/tests/sync/force_push_multiple_matching.go @@ -22,7 +22,7 @@ var ForcePushMultipleMatching = NewIntegrationTest(NewIntegrationTestArgs{ Contains("one"), ) - t.Views().Status().Content(Contains("↓1 repo → master")) + t.Views().Status().Content(Equals("↓1 repo → master")) t.Views().Branches(). Lines( @@ -42,7 +42,7 @@ var ForcePushMultipleMatching = NewIntegrationTest(NewIntegrationTestArgs{ Contains("one"), ) - t.Views().Status().Content(Contains("✓ repo → master")) + t.Views().Status().Content(Equals("✓ 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 1ea512720..8c55b7e8c 100644 --- a/pkg/integration/tests/sync/force_push_multiple_upstream.go +++ b/pkg/integration/tests/sync/force_push_multiple_upstream.go @@ -21,7 +21,7 @@ var ForcePushMultipleUpstream = NewIntegrationTest(NewIntegrationTestArgs{ Contains("one"), ) - t.Views().Status().Content(Contains("↓1 repo → master")) + t.Views().Status().Content(Equals("↓1 repo → master")) t.Views().Branches(). Lines( @@ -41,7 +41,7 @@ var ForcePushMultipleUpstream = NewIntegrationTest(NewIntegrationTestArgs{ Contains("one"), ) - t.Views().Status().Content(Contains("✓ repo → master")) + t.Views().Status().Content(Equals("✓ repo → master")) t.Views().Branches(). Lines( diff --git a/pkg/integration/tests/sync/pull.go b/pkg/integration/tests/sync/pull.go index 7f5703791..b30cbb408 100644 --- a/pkg/integration/tests/sync/pull.go +++ b/pkg/integration/tests/sync/pull.go @@ -26,7 +26,7 @@ var Pull = NewIntegrationTest(NewIntegrationTestArgs{ Contains("one"), ) - t.Views().Status().Content(Contains("↓1 repo → master")) + t.Views().Status().Content(Equals("↓1 repo → master")) t.Views().Files().IsFocused().Press(keys.Universal.Pull) @@ -36,6 +36,6 @@ var Pull = NewIntegrationTest(NewIntegrationTestArgs{ Contains("one"), ) - t.Views().Status().Content(Contains("✓ repo → master")) + t.Views().Status().Content(Equals("✓ 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 b2dbfddb4..acffa24be 100644 --- a/pkg/integration/tests/sync/pull_and_set_upstream.go +++ b/pkg/integration/tests/sync/pull_and_set_upstream.go @@ -25,7 +25,7 @@ var PullAndSetUpstream = NewIntegrationTest(NewIntegrationTestArgs{ Contains("one"), ) - t.Views().Status().Content(Contains("repo → master")) + t.Views().Status().Content(Equals("repo → master")) t.Views().Files().IsFocused().Press(keys.Universal.Pull) @@ -40,6 +40,6 @@ var PullAndSetUpstream = NewIntegrationTest(NewIntegrationTestArgs{ Contains("one"), ) - t.Views().Status().Content(Contains("✓ repo → master")) + t.Views().Status().Content(Equals("✓ repo → master")) }, }) diff --git a/pkg/integration/tests/sync/pull_merge.go b/pkg/integration/tests/sync/pull_merge.go index d9c9e107d..d4809f00e 100644 --- a/pkg/integration/tests/sync/pull_merge.go +++ b/pkg/integration/tests/sync/pull_merge.go @@ -33,13 +33,13 @@ var PullMerge = NewIntegrationTest(NewIntegrationTestArgs{ Contains("one"), ) - t.Views().Status().Content(Contains("↓2 repo → master")) + t.Views().Status().Content(Equals("↑1↓2 repo → master")) t.Views().Files(). IsFocused(). Press(keys.Universal.Pull) - t.Views().Status().Content(Contains("↑2 repo → master")) + t.Views().Status().Content(Equals("↑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 3177cabe5..6e05fafae 100644 --- a/pkg/integration/tests/sync/pull_merge_conflict.go +++ b/pkg/integration/tests/sync/pull_merge_conflict.go @@ -34,7 +34,7 @@ var PullMergeConflict = NewIntegrationTest(NewIntegrationTestArgs{ Contains("one"), ) - t.Views().Status().Content(Contains("↓2 repo → master")) + t.Views().Status().Content(Equals("↑1↓2 repo → master")) t.Views().Files(). IsFocused(). @@ -62,7 +62,7 @@ var PullMergeConflict = NewIntegrationTest(NewIntegrationTestArgs{ t.Common().ContinueOnConflictsResolved() - t.Views().Status().Content(Contains("↑2 repo → master")) + t.Views().Status().Content(Equals("↑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 77810426e..158cdf3d3 100644 --- a/pkg/integration/tests/sync/pull_rebase.go +++ b/pkg/integration/tests/sync/pull_rebase.go @@ -35,13 +35,13 @@ var PullRebase = NewIntegrationTest(NewIntegrationTestArgs{ Contains("one"), ) - t.Views().Status().Content(Contains("↓2 repo → master")) + t.Views().Status().Content(Equals("↑1↓2 repo → master")) t.Views().Files(). IsFocused(). Press(keys.Universal.Pull) - t.Views().Status().Content(Contains("↑1 repo → master")) + t.Views().Status().Content(Equals("↑1 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 3b6c83b85..fa2920b7b 100644 --- a/pkg/integration/tests/sync/pull_rebase_conflict.go +++ b/pkg/integration/tests/sync/pull_rebase_conflict.go @@ -34,7 +34,7 @@ var PullRebaseConflict = NewIntegrationTest(NewIntegrationTestArgs{ Contains("one"), ) - t.Views().Status().Content(Contains("↓2 repo → master")) + t.Views().Status().Content(Equals("↑1↓2 repo → master")) t.Views().Files(). IsFocused(). @@ -63,7 +63,7 @@ var PullRebaseConflict = NewIntegrationTest(NewIntegrationTestArgs{ t.Common().ContinueOnConflictsResolved() - t.Views().Status().Content(Contains("↑1 repo → master")) + t.Views().Status().Content(Equals("↑1 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 a6a3f5356..e93ca2e37 100644 --- a/pkg/integration/tests/sync/pull_rebase_interactive_conflict.go +++ b/pkg/integration/tests/sync/pull_rebase_interactive_conflict.go @@ -38,7 +38,7 @@ var PullRebaseInteractiveConflict = NewIntegrationTest(NewIntegrationTestArgs{ Contains("one"), ) - t.Views().Status().Content(Contains("↓2 repo → master")) + t.Views().Status().Content(Equals("↑2↓2 repo → master")) t.Views().Files(). IsFocused(). @@ -76,7 +76,7 @@ var PullRebaseInteractiveConflict = NewIntegrationTest(NewIntegrationTestArgs{ t.Common().ContinueOnConflictsResolved() - t.Views().Status().Content(Contains("↑2 repo → master")) + t.Views().Status().Content(Equals("↑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 b53790964..eba3ba746 100644 --- a/pkg/integration/tests/sync/pull_rebase_interactive_conflict_drop.go +++ b/pkg/integration/tests/sync/pull_rebase_interactive_conflict_drop.go @@ -38,7 +38,7 @@ var PullRebaseInteractiveConflictDrop = NewIntegrationTest(NewIntegrationTestArg Contains("one"), ) - t.Views().Status().Content(Contains("↓2 repo → master")) + t.Views().Status().Content(Equals("↑2↓2 repo → master")) t.Views().Files(). IsFocused(). @@ -85,7 +85,7 @@ var PullRebaseInteractiveConflictDrop = NewIntegrationTest(NewIntegrationTestArg t.Common().ContinueOnConflictsResolved() - t.Views().Status().Content(Contains("↑1 repo → master")) + t.Views().Status().Content(Equals("↑1 repo → master")) t.Views().Commits(). Focus(). diff --git a/pkg/integration/tests/sync/push.go b/pkg/integration/tests/sync/push.go index ea27b399c..cb1e11aa9 100644 --- a/pkg/integration/tests/sync/push.go +++ b/pkg/integration/tests/sync/push.go @@ -21,7 +21,7 @@ var Push = NewIntegrationTest(NewIntegrationTestArgs{ shell.EmptyCommit("two") }, Run: func(t *TestDriver, keys config.KeybindingConfig) { - t.Views().Status().Content(Contains("↑1 repo → master")) + t.Views().Status().Content(Equals("↑1 repo → master")) t.Views().Files(). IsFocused(). diff --git a/pkg/integration/tests/sync/push_and_auto_set_upstream.go b/pkg/integration/tests/sync/push_and_auto_set_upstream.go index c3f58d644..d8a336ea4 100644 --- a/pkg/integration/tests/sync/push_and_auto_set_upstream.go +++ b/pkg/integration/tests/sync/push_and_auto_set_upstream.go @@ -22,7 +22,7 @@ var PushAndAutoSetUpstream = NewIntegrationTest(NewIntegrationTestArgs{ }, Run: func(t *TestDriver, keys config.KeybindingConfig) { // assert no mention of upstream/downstream changes - t.Views().Status().Content(MatchesRegexp(`^\s+repo → master`)) + t.Views().Status().Content(Equals("repo → master")) t.Views().Files(). IsFocused(). diff --git a/pkg/integration/tests/sync/push_and_set_upstream.go b/pkg/integration/tests/sync/push_and_set_upstream.go index 0521c4b21..d900452eb 100644 --- a/pkg/integration/tests/sync/push_and_set_upstream.go +++ b/pkg/integration/tests/sync/push_and_set_upstream.go @@ -19,7 +19,7 @@ var PushAndSetUpstream = NewIntegrationTest(NewIntegrationTestArgs{ }, Run: func(t *TestDriver, keys config.KeybindingConfig) { // assert no mention of upstream/downstream changes - t.Views().Status().Content(MatchesRegexp(`^\s+repo → master`)) + t.Views().Status().Content(Equals("repo → master")) t.Views().Files(). IsFocused(). diff --git a/pkg/integration/tests/sync/push_follow_tags.go b/pkg/integration/tests/sync/push_follow_tags.go index 463172abf..c293cf005 100644 --- a/pkg/integration/tests/sync/push_follow_tags.go +++ b/pkg/integration/tests/sync/push_follow_tags.go @@ -24,13 +24,13 @@ var PushFollowTags = NewIntegrationTest(NewIntegrationTestArgs{ shell.SetConfig("push.followTags", "true") }, Run: func(t *TestDriver, keys config.KeybindingConfig) { - t.Views().Status().Content(Contains("↑1 repo → master")) + t.Views().Status().Content(Equals("↑1 repo → master")) t.Views().Files(). IsFocused(). Press(keys.Universal.Push) - t.Views().Status().Content(Contains("✓ repo → master")) + t.Views().Status().Content(Equals("✓ repo → master")) t.Views().Remotes(). Focus(). diff --git a/pkg/integration/tests/sync/push_no_follow_tags.go b/pkg/integration/tests/sync/push_no_follow_tags.go index 599d05a64..18a1cf62d 100644 --- a/pkg/integration/tests/sync/push_no_follow_tags.go +++ b/pkg/integration/tests/sync/push_no_follow_tags.go @@ -22,13 +22,13 @@ var PushNoFollowTags = NewIntegrationTest(NewIntegrationTestArgs{ shell.CreateAnnotatedTag("mytag", "message", "HEAD") }, Run: func(t *TestDriver, keys config.KeybindingConfig) { - t.Views().Status().Content(Contains("✓ repo → master")) + t.Views().Status().Content(Equals("✓ repo → master")) t.Views().Files(). IsFocused(). Press(keys.Universal.Push) - t.Views().Status().Content(Contains("✓ repo → master")) + t.Views().Status().Content(Equals("✓ repo → master")) t.Views().Remotes(). Focus(). diff --git a/pkg/integration/tests/sync/push_with_credential_prompt.go b/pkg/integration/tests/sync/push_with_credential_prompt.go index 79d7fcc1d..62be89bf4 100644 --- a/pkg/integration/tests/sync/push_with_credential_prompt.go +++ b/pkg/integration/tests/sync/push_with_credential_prompt.go @@ -26,7 +26,7 @@ var PushWithCredentialPrompt = NewIntegrationTest(NewIntegrationTestArgs{ shell.CopyHelpFile("pre-push", ".git/hooks/pre-push") }, Run: func(t *TestDriver, keys config.KeybindingConfig) { - t.Views().Status().Content(Contains("↑1 repo → master")) + t.Views().Status().Content(Equals("↑1 repo → master")) t.Views().Files(). IsFocused(). @@ -50,7 +50,7 @@ var PushWithCredentialPrompt = NewIntegrationTest(NewIntegrationTestArgs{ Content(Contains("incorrect username/password")). Confirm() - t.Views().Status().Content(Contains("↑1 repo → master")) + t.Views().Status().Content(Equals("↑1 repo → master")) // try again with correct password t.Views().Files(). @@ -67,7 +67,7 @@ var PushWithCredentialPrompt = NewIntegrationTest(NewIntegrationTestArgs{ Type("password"). Confirm() - t.Views().Status().Content(Contains("✓ repo → master")) + t.Views().Status().Content(Equals("✓ repo → master")) assertSuccessfullyPushed(t) }, diff --git a/pkg/integration/tests/sync/shared.go b/pkg/integration/tests/sync/shared.go index 8ea9a6c25..3e3d5c017 100644 --- a/pkg/integration/tests/sync/shared.go +++ b/pkg/integration/tests/sync/shared.go @@ -24,7 +24,7 @@ func createTwoBranchesReadyToForcePush(shell *Shell) { } func assertSuccessfullyPushed(t *TestDriver) { - t.Views().Status().Content(Contains("✓ repo → master")) + t.Views().Status().Content(Equals("✓ repo → master")) t.Views().Remotes(). Focus(). From b2011dca359fbf5a5dde7118fe2ad27a41410ed6 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Wed, 29 May 2024 16:25:41 +0200 Subject: [PATCH 2/7] Remove the cache invalidation logic from getMergeBase It is a valid case for a branch to share no history with any of the main branches, in which case git merge-base returns an error (and an empty string). Since we can't distinguish this from one of the main branches having been deleted, we shouldn't invalidate the cache in that case. --- pkg/commands/git_commands/commit_loader.go | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/pkg/commands/git_commands/commit_loader.go b/pkg/commands/git_commands/commit_loader.go index 737e4c077..86418453d 100644 --- a/pkg/commands/git_commands/commit_loader.go +++ b/pkg/commands/git_commands/commit_loader.go @@ -483,16 +483,17 @@ func (self *CommitLoader) getMergeBase(refName string) string { // We pass all configured main branches to the merge-base call; git will // return the base commit for the closest one. - output, err := self.cmd.New( + // We ignore errors from this call, since we can't distinguish whether the + // error is because one of the main branches has been deleted since the last + // call to determineMainBranches, or because the refName has no common + // history with any of the main branches. Since the former should happen + // very rarely, users must quit and restart lazygit to fix it; the latter is + // also not very common, but can totally happen and is not an error. + + output, _ := self.cmd.New( NewGitCmd("merge-base").Arg(refName).Arg(self.mainBranches...). ToArgv(), ).DontLog().RunWithOutput() - if err != nil { - // If there's an error, it must be because one of the main branches that - // used to exist when we called getExistingMainBranches() was deleted - // meanwhile. To fix this for next time, throw away our cache. - self.mainBranches = nil - } return ignoringWarnings(output) } From f4d922bc80c449c59f1ce24d1347793397c30834 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Mon, 29 Apr 2024 18:37:34 +0200 Subject: [PATCH 3/7] Factor out CommitLoader.mainBranches into its own class, and store it in Model --- pkg/commands/git_commands/commit_loader.go | 73 ++------------ .../git_commands/commit_loader_test.go | 7 +- pkg/commands/git_commands/main_branches.go | 98 +++++++++++++++++++ pkg/gui/controllers/helpers/refresh_helper.go | 2 + .../controllers/helpers/sub_commits_helper.go | 1 + pkg/gui/gui.go | 1 + pkg/gui/types/common.go | 2 + 7 files changed, 116 insertions(+), 68 deletions(-) create mode 100644 pkg/commands/git_commands/main_branches.go diff --git a/pkg/commands/git_commands/commit_loader.go b/pkg/commands/git_commands/commit_loader.go index 86418453d..643a7c184 100644 --- a/pkg/commands/git_commands/commit_loader.go +++ b/pkg/commands/git_commands/commit_loader.go @@ -35,11 +35,6 @@ type CommitLoader struct { readFile func(filename string) ([]byte, error) walkFiles func(root string, fn filepath.WalkFunc) error dotGitDir string - // List of main branches that exist in the repo. - // We use these to obtain the merge base of the branch. - // When nil, we're yet to obtain the list of existing main branches. - // When an empty slice, we've obtained the list and it's empty. - mainBranches []string *GitCommon } @@ -56,7 +51,6 @@ func NewCommitLoader( getRebaseMode: getRebaseMode, readFile: os.ReadFile, walkFiles: filepath.Walk, - mainBranches: nil, GitCommon: gitCommon, } } @@ -72,6 +66,7 @@ type GetCommitsOptions struct { All bool // If non-empty, show divergence from this ref (left-right log) RefToShowDivergenceFrom string + MainBranches *MainBranches } // GetCommits obtains the commits of the current branch @@ -108,9 +103,9 @@ func (self *CommitLoader) GetCommits(opts GetCommitsOptions) ([]*models.Commit, go utils.Safe(func() { defer wg.Done() - ancestor = self.getMergeBase(opts.RefName) + ancestor = self.getMergeBase(opts.RefName, opts.MainBranches) if opts.RefToShowDivergenceFrom != "" { - remoteAncestor = self.getMergeBase(opts.RefToShowDivergenceFrom) + remoteAncestor = self.getMergeBase(opts.RefToShowDivergenceFrom, opts.MainBranches) } }) @@ -471,12 +466,9 @@ func setCommitMergedStatuses(ancestor string, commits []*models.Commit) { } } -func (self *CommitLoader) getMergeBase(refName string) string { - if self.mainBranches == nil { - self.mainBranches = self.getExistingMainBranches() - } - - if len(self.mainBranches) == 0 { +func (self *CommitLoader) getMergeBase(refName string, existingMainBranches *ExistingMainBranches) string { + mainBranches := existingMainBranches.Get() + if len(mainBranches) == 0 { return "" } @@ -491,63 +483,12 @@ func (self *CommitLoader) getMergeBase(refName string) string { // also not very common, but can totally happen and is not an error. output, _ := self.cmd.New( - NewGitCmd("merge-base").Arg(refName).Arg(self.mainBranches...). + NewGitCmd("merge-base").Arg(refName).Arg(mainBranches...). ToArgv(), ).DontLog().RunWithOutput() return ignoringWarnings(output) } -func (self *CommitLoader) getExistingMainBranches() []string { - var existingBranches []string - var wg sync.WaitGroup - - mainBranches := self.UserConfig.Git.MainBranches - existingBranches = make([]string, len(mainBranches)) - - for i, branchName := range mainBranches { - wg.Add(1) - go utils.Safe(func() { - defer wg.Done() - - // Try to determine upstream of local main branch - if ref, err := self.cmd.New( - NewGitCmd("rev-parse").Arg("--symbolic-full-name", branchName+"@{u}").ToArgv(), - ).DontLog().RunWithOutput(); err == nil { - existingBranches[i] = strings.TrimSpace(ref) - return - } - - // If this failed, a local branch for this main branch doesn't exist or it - // has no upstream configured. Try looking for one in the "origin" remote. - ref := "refs/remotes/origin/" + branchName - if err := self.cmd.New( - NewGitCmd("rev-parse").Arg("--verify", "--quiet", ref).ToArgv(), - ).DontLog().Run(); err == nil { - existingBranches[i] = ref - return - } - - // If this failed as well, try if we have the main branch as a local - // branch. This covers the case where somebody is using git locally - // for something, but never pushing anywhere. - ref = "refs/heads/" + branchName - if err := self.cmd.New( - NewGitCmd("rev-parse").Arg("--verify", "--quiet", ref).ToArgv(), - ).DontLog().Run(); err == nil { - existingBranches[i] = ref - } - }) - } - - wg.Wait() - - existingBranches = lo.Filter(existingBranches, func(branch string, _ int) bool { - return branch != "" - }) - - return existingBranches -} - func ignoringWarnings(commandOutput string) string { trimmedOutput := strings.TrimSpace(commandOutput) split := strings.Split(trimmedOutput, "\n") diff --git a/pkg/commands/git_commands/commit_loader_test.go b/pkg/commands/git_commands/commit_loader_test.go index fe4f39585..a8ef9e69a 100644 --- a/pkg/commands/git_commands/commit_loader_test.go +++ b/pkg/commands/git_commands/commit_loader_test.go @@ -307,10 +307,11 @@ func TestGetCommits(t *testing.T) { common := utils.NewDummyCommon() common.AppState = &config.AppState{} common.AppState.GitLogOrder = scenario.logOrder + cmd := oscommands.NewDummyCmdObjBuilder(scenario.runner) builder := &CommitLoader{ Common: common, - cmd: oscommands.NewDummyCmdObjBuilder(scenario.runner), + cmd: cmd, getRebaseMode: func() (enums.RebaseMode, error) { return scenario.rebaseMode, nil }, dotGitDir: ".git", readFile: func(filename string) ([]byte, error) { @@ -322,7 +323,9 @@ func TestGetCommits(t *testing.T) { } common.UserConfig.Git.MainBranches = scenario.mainBranches - commits, err := builder.GetCommits(scenario.opts) + opts := scenario.opts + opts.MainBranches = NewMainBranches(scenario.mainBranches, cmd) + commits, err := builder.GetCommits(opts) assert.Equal(t, scenario.expectedCommits, commits) assert.Equal(t, scenario.expectedError, err) diff --git a/pkg/commands/git_commands/main_branches.go b/pkg/commands/git_commands/main_branches.go new file mode 100644 index 000000000..f8b8b85f2 --- /dev/null +++ b/pkg/commands/git_commands/main_branches.go @@ -0,0 +1,98 @@ +package git_commands + +import ( + "strings" + "sync" + + "github.com/jesseduffield/lazygit/pkg/commands/oscommands" + "github.com/jesseduffield/lazygit/pkg/utils" + "github.com/samber/lo" + "github.com/sasha-s/go-deadlock" +) + +type MainBranches struct { + // List of main branches configured by the user. Just the bare names. + configuredMainBranches []string + // Which of these actually exist in the repository. Full ref names, and it + // could be either "refs/heads/..." or "refs/remotes/origin/..." depending + // on which one exists for a given bare name. + existingMainBranches []string + + cmd oscommands.ICmdObjBuilder + mutex *deadlock.Mutex +} + +func NewMainBranches( + configuredMainBranches []string, + cmd oscommands.ICmdObjBuilder, +) *MainBranches { + return &MainBranches{ + configuredMainBranches: configuredMainBranches, + existingMainBranches: nil, + cmd: cmd, + mutex: &deadlock.Mutex{}, + } +} + +// Get the list of main branches that exist in the repository. This is a list of +// full ref names. +func (self *MainBranches) Get() []string { + self.mutex.Lock() + defer self.mutex.Unlock() + + if self.existingMainBranches == nil { + self.existingMainBranches = self.determineMainBranches() + } + + return self.existingMainBranches +} + +func (self *MainBranches) determineMainBranches() []string { + var existingBranches []string + var wg sync.WaitGroup + + existingBranches = make([]string, len(self.configuredMainBranches)) + + for i, branchName := range self.configuredMainBranches { + wg.Add(1) + go utils.Safe(func() { + defer wg.Done() + + // Try to determine upstream of local main branch + if ref, err := self.cmd.New( + NewGitCmd("rev-parse").Arg("--symbolic-full-name", branchName+"@{u}").ToArgv(), + ).DontLog().RunWithOutput(); err == nil { + existingBranches[i] = strings.TrimSpace(ref) + return + } + + // If this failed, a local branch for this main branch doesn't exist or it + // has no upstream configured. Try looking for one in the "origin" remote. + ref := "refs/remotes/origin/" + branchName + if err := self.cmd.New( + NewGitCmd("rev-parse").Arg("--verify", "--quiet", ref).ToArgv(), + ).DontLog().Run(); err == nil { + existingBranches[i] = ref + return + } + + // If this failed as well, try if we have the main branch as a local + // branch. This covers the case where somebody is using git locally + // for something, but never pushing anywhere. + ref = "refs/heads/" + branchName + if err := self.cmd.New( + NewGitCmd("rev-parse").Arg("--verify", "--quiet", ref).ToArgv(), + ).DontLog().Run(); err == nil { + existingBranches[i] = ref + } + }) + } + + wg.Wait() + + existingBranches = lo.Filter(existingBranches, func(branch string, _ int) bool { + return branch != "" + }) + + return existingBranches +} diff --git a/pkg/gui/controllers/helpers/refresh_helper.go b/pkg/gui/controllers/helpers/refresh_helper.go index b927296fc..02a26ded9 100644 --- a/pkg/gui/controllers/helpers/refresh_helper.go +++ b/pkg/gui/controllers/helpers/refresh_helper.go @@ -331,6 +331,7 @@ func (self *RefreshHelper) refreshCommitsWithLimit() error { RefName: self.refForLog(), RefForPushedStatus: checkedOutBranchName, All: self.c.Contexts().LocalCommits.GetShowWholeGitGraph(), + MainBranches: self.c.Model().MainBranches, }, ) if err != nil { @@ -357,6 +358,7 @@ func (self *RefreshHelper) refreshSubCommitsWithLimit() error { RefName: self.c.Contexts().SubCommits.GetRef().FullRefName(), RefToShowDivergenceFrom: self.c.Contexts().SubCommits.GetRefToShowDivergenceFrom(), RefForPushedStatus: self.c.Contexts().SubCommits.GetRef().FullRefName(), + MainBranches: self.c.Model().MainBranches, }, ) if err != nil { diff --git a/pkg/gui/controllers/helpers/sub_commits_helper.go b/pkg/gui/controllers/helpers/sub_commits_helper.go index c31d50937..f1cecf7f5 100644 --- a/pkg/gui/controllers/helpers/sub_commits_helper.go +++ b/pkg/gui/controllers/helpers/sub_commits_helper.go @@ -44,6 +44,7 @@ func (self *SubCommitsHelper) ViewSubCommits(opts ViewSubCommitsOpts) error { RefName: opts.Ref.FullRefName(), RefForPushedStatus: opts.Ref.FullRefName(), RefToShowDivergenceFrom: opts.RefToShowDivergenceFrom, + MainBranches: self.c.Model().MainBranches, }, ) if err != nil { diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index 0c0f36370..5f2fd55bf 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -379,6 +379,7 @@ func (gui *Gui) resetState(startArgs appTypes.StartArgs) types.Context { BisectInfo: git_commands.NewNullBisectInfo(), FilesTrie: patricia.NewTrie(), Authors: map[string]*models.Author{}, + MainBranches: git_commands.NewMainBranches(gui.UserConfig.Git.MainBranches, gui.os.Cmd), }, Modes: &types.Modes{ Filtering: filtering.New(startArgs.FilterPath, ""), diff --git a/pkg/gui/types/common.go b/pkg/gui/types/common.go index d50173078..77f2f56eb 100644 --- a/pkg/gui/types/common.go +++ b/pkg/gui/types/common.go @@ -281,6 +281,8 @@ type Model struct { // we're on a detached head because we're rebasing or bisecting. CheckedOutBranch string + MainBranches *git_commands.MainBranches + // for displaying suggestions while typing in a file name FilesTrie *patricia.Trie From e79b4259e45247bd5ffaccc9f49355fa63ca5598 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Sat, 18 May 2024 20:24:05 +0200 Subject: [PATCH 4/7] Make GetMergeBase a method of ExistingMainBranches --- pkg/commands/git_commands/commit_loader.go | 27 ++-------------------- pkg/commands/git_commands/main_branches.go | 24 +++++++++++++++++++ 2 files changed, 26 insertions(+), 25 deletions(-) diff --git a/pkg/commands/git_commands/commit_loader.go b/pkg/commands/git_commands/commit_loader.go index 643a7c184..f116ded1f 100644 --- a/pkg/commands/git_commands/commit_loader.go +++ b/pkg/commands/git_commands/commit_loader.go @@ -103,9 +103,9 @@ func (self *CommitLoader) GetCommits(opts GetCommitsOptions) ([]*models.Commit, go utils.Safe(func() { defer wg.Done() - ancestor = self.getMergeBase(opts.RefName, opts.MainBranches) + ancestor = opts.MainBranches.GetMergeBase(opts.RefName) if opts.RefToShowDivergenceFrom != "" { - remoteAncestor = self.getMergeBase(opts.RefToShowDivergenceFrom, opts.MainBranches) + remoteAncestor = opts.MainBranches.GetMergeBase(opts.RefToShowDivergenceFrom) } }) @@ -466,29 +466,6 @@ func setCommitMergedStatuses(ancestor string, commits []*models.Commit) { } } -func (self *CommitLoader) getMergeBase(refName string, existingMainBranches *ExistingMainBranches) string { - mainBranches := existingMainBranches.Get() - if len(mainBranches) == 0 { - return "" - } - - // We pass all configured main branches to the merge-base call; git will - // return the base commit for the closest one. - - // We ignore errors from this call, since we can't distinguish whether the - // error is because one of the main branches has been deleted since the last - // call to determineMainBranches, or because the refName has no common - // history with any of the main branches. Since the former should happen - // very rarely, users must quit and restart lazygit to fix it; the latter is - // also not very common, but can totally happen and is not an error. - - output, _ := self.cmd.New( - NewGitCmd("merge-base").Arg(refName).Arg(mainBranches...). - ToArgv(), - ).DontLog().RunWithOutput() - return ignoringWarnings(output) -} - func ignoringWarnings(commandOutput string) string { trimmedOutput := strings.TrimSpace(commandOutput) split := strings.Split(trimmedOutput, "\n") diff --git a/pkg/commands/git_commands/main_branches.go b/pkg/commands/git_commands/main_branches.go index f8b8b85f2..341232b04 100644 --- a/pkg/commands/git_commands/main_branches.go +++ b/pkg/commands/git_commands/main_branches.go @@ -47,6 +47,30 @@ func (self *MainBranches) Get() []string { return self.existingMainBranches } +// Return the merge base of the given refName with the closest main branch. +func (self *MainBranches) GetMergeBase(refName string) string { + mainBranches := self.Get() + if len(mainBranches) == 0 { + return "" + } + + // We pass all existing main branches to the merge-base call; git will + // return the base commit for the closest one. + + // We ignore errors from this call, since we can't distinguish whether the + // error is because one of the main branches has been deleted since the last + // call to determineMainBranches, or because the refName has no common + // history with any of the main branches. Since the former should happen + // very rarely, users must quit and restart lazygit to fix it; the latter is + // also not very common, but can totally happen and is not an error. + + output, _ := self.cmd.New( + NewGitCmd("merge-base").Arg(refName).Arg(mainBranches...). + ToArgv(), + ).DontLog().RunWithOutput() + return ignoringWarnings(output) +} + func (self *MainBranches) determineMainBranches() []string { var existingBranches []string var wg sync.WaitGroup From 8c385731f7542241be00630afd2a9515563163ae Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Fri, 3 May 2024 20:14:02 +0200 Subject: [PATCH 5/7] Add GetBaseBranch function --- pkg/commands/git_commands/branch_loader.go | 33 ++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/pkg/commands/git_commands/branch_loader.go b/pkg/commands/git_commands/branch_loader.go index 16777243a..1052626a9 100644 --- a/pkg/commands/git_commands/branch_loader.go +++ b/pkg/commands/git_commands/branch_loader.go @@ -127,6 +127,39 @@ func (self *BranchLoader) Load(reflogCommits []*models.Commit) ([]*models.Branch return branches, nil } +// Find the base branch for the given branch (i.e. the main branch that the +// given branch was forked off of) +// +// Note that this function may return an empty string even if the returned error +// is nil, e.g. when none of the configured main branches exist. This is not +// considered an error condition, so callers need to check both the returned +// error and whether the returned base branch is empty (and possibly react +// differently in both cases). +func (self *BranchLoader) GetBaseBranch(branch *models.Branch, mainBranches *MainBranches) (string, error) { + mergeBase := mainBranches.GetMergeBase(branch.FullRefName()) + if mergeBase == "" { + return "", nil + } + + output, err := self.cmd.New( + NewGitCmd("for-each-ref"). + Arg("--contains"). + Arg(mergeBase). + Arg("--format=%(refname)"). + Arg(mainBranches.Get()...). + ToArgv(), + ).DontLog().RunWithOutput() + if err != nil { + return "", err + } + trimmedOutput := strings.TrimSpace(output) + split := strings.Split(trimmedOutput, "\n") + if len(split) == 0 || split[0] == "" { + return "", nil + } + return split[0], nil +} + func (self *BranchLoader) obtainBranches(canUsePushTrack bool) []*models.Branch { output, err := self.getRawBranches() if err != nil { From 5b613f5bc78c44351945cbc3ffdf9e5950c04bd8 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Thu, 2 May 2024 20:05:16 +0200 Subject: [PATCH 6/7] Remove ColoredBranchStatus and branchStatusColor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously the entire status was colored in a single color, so the API made sense. This is going to change in the next commit, so now we must include the color in the string returned from BranchStatus(), which means that callers who need to do hit detection or measure the length need to decolorize it. While we're at it, switch the order of ↑3↓7 to ↓7↑3. For some reason that I can't really explain I find it more logical this way. The software out there is pretty undecided about it, it seems: VS Code puts ↓7 first, and so does the shell prompt that comes with git; git status and git branch -v put "ahead" first though. Shrug. --- pkg/gui/controllers/status_controller.go | 3 +- pkg/gui/presentation/branches.go | 49 +++++-------------- pkg/gui/presentation/branches_test.go | 4 +- pkg/gui/presentation/status.go | 6 +-- 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 +- 10 files changed, 26 insertions(+), 48 deletions(-) diff --git a/pkg/gui/controllers/status_controller.go b/pkg/gui/controllers/status_controller.go index 4c4384bfd..483acdda6 100644 --- a/pkg/gui/controllers/status_controller.go +++ b/pkg/gui/controllers/status_controller.go @@ -12,6 +12,7 @@ import ( "github.com/jesseduffield/lazygit/pkg/gui/presentation" "github.com/jesseduffield/lazygit/pkg/gui/style" "github.com/jesseduffield/lazygit/pkg/gui/types" + "github.com/jesseduffield/lazygit/pkg/utils" "github.com/samber/lo" ) @@ -116,7 +117,7 @@ func (self *StatusController) onClick(opts gocui.ViewMouseBindingOpts) error { return err } - upstreamStatus := presentation.BranchStatus(currentBranch, types.ItemOperationNone, self.c.Tr, time.Now(), self.c.UserConfig) + upstreamStatus := utils.Decolorise(presentation.BranchStatus(currentBranch, types.ItemOperationNone, self.c.Tr, time.Now(), self.c.UserConfig)) repoName := self.c.Git().RepoPaths.RepoName() workingTreeState := self.c.Git().Status.WorkingTreeState() switch workingTreeState { diff --git a/pkg/gui/presentation/branches.go b/pkg/gui/presentation/branches.go index 406a580d5..0e48a3935 100644 --- a/pkg/gui/presentation/branches.go +++ b/pkg/gui/presentation/branches.go @@ -56,7 +56,7 @@ func getBranchDisplayStrings( // Recency is always three characters, plus one for the space availableWidth := viewWidth - 4 if len(branchStatus) > 0 { - availableWidth -= runewidth.StringWidth(branchStatus) + 1 + availableWidth -= runewidth.StringWidth(utils.Decolorise(branchStatus)) + 1 } if icons.IsIconEnabled() { availableWidth -= 2 // one for the icon, one for the space @@ -89,8 +89,7 @@ func getBranchDisplayStrings( coloredName = fmt.Sprintf("%s %s", coloredName, style.FgDefault.Sprint(worktreeIcon)) } if len(branchStatus) > 0 { - coloredStatus := branchStatusColor(b, itemOperation).Sprint(branchStatus) - coloredName = fmt.Sprintf("%s %s", coloredName, coloredStatus) + coloredName = fmt.Sprintf("%s %s", coloredName, branchStatus) } recencyColor := style.FgCyan @@ -144,30 +143,6 @@ func GetBranchTextStyle(name string) style.TextStyle { } } -func branchStatusColor(branch *models.Branch, itemOperation types.ItemOperation) style.TextStyle { - colour := style.FgYellow - if itemOperation != types.ItemOperationNone { - colour = style.FgCyan - } else if branch.UpstreamGone { - colour = style.FgRed - } else if branch.MatchesUpstream() { - colour = style.FgGreen - } else if branch.RemoteBranchNotStoredLocally() { - colour = style.FgMagenta - } - - return colour -} - -func ColoredBranchStatus( - branch *models.Branch, - itemOperation types.ItemOperation, - tr *i18n.TranslationSet, - userConfig *config.UserConfig, -) string { - return branchStatusColor(branch, itemOperation).Sprint(BranchStatus(branch, itemOperation, tr, time.Now(), userConfig)) -} - func BranchStatus( branch *models.Branch, itemOperation types.ItemOperation, @@ -177,7 +152,7 @@ func BranchStatus( ) string { itemOperationStr := ItemOperationToString(itemOperation, tr) if itemOperationStr != "" { - return itemOperationStr + " " + utils.Loader(now, userConfig.Gui.Spinner) + return style.FgCyan.Sprintf("%s %s", itemOperationStr, utils.Loader(now, userConfig.Gui.Spinner)) } if !branch.IsTrackingRemote() { @@ -185,25 +160,27 @@ func BranchStatus( } if branch.UpstreamGone { - return tr.UpstreamGone + return style.FgRed.Sprint(tr.UpstreamGone) } if branch.MatchesUpstream() { - return "✓" + return style.FgGreen.Sprint("✓") } if branch.RemoteBranchNotStoredLocally() { - return "?" + return style.FgMagenta.Sprint("?") } - result := "" - if branch.IsAheadForPull() { - result = fmt.Sprintf("↑%s", branch.AheadForPull) + if branch.IsBehindForPull() && branch.IsAheadForPull() { + return style.FgYellow.Sprintf("↓%s↑%s", branch.BehindForPull, branch.AheadForPull) } if branch.IsBehindForPull() { - result = fmt.Sprintf("%s↓%s", result, branch.BehindForPull) + return style.FgYellow.Sprintf("↓%s", branch.BehindForPull) + } + if branch.IsAheadForPull() { + return style.FgYellow.Sprintf("↑%s", branch.AheadForPull) } - return result + return "" } func SetCustomBranches(customBranchColors map[string]string) { diff --git a/pkg/gui/presentation/branches_test.go b/pkg/gui/presentation/branches_test.go index cf2f1d994..db4868970 100644 --- a/pkg/gui/presentation/branches_test.go +++ b/pkg/gui/presentation/branches_test.go @@ -81,7 +81,7 @@ func Test_getBranchDisplayStrings(t *testing.T) { viewWidth: 100, useIcons: false, checkedOutByWorktree: true, - expected: []string{"1m", "branch_name (worktree) ↑3↓5"}, + expected: []string{"1m", "branch_name (worktree) ↓5↑3"}, }, { branch: &models.Branch{Name: "branch_name", Recency: "1m"}, @@ -167,7 +167,7 @@ func Test_getBranchDisplayStrings(t *testing.T) { viewWidth: 30, useIcons: false, checkedOutByWorktree: true, - expected: []string{"1m", "branch_na… (worktree) ↑3↓5"}, + expected: []string{"1m", "branch_na… (worktree) ↓5↑3"}, }, { branch: &models.Branch{Name: "branch_name", Recency: "1m"}, diff --git a/pkg/gui/presentation/status.go b/pkg/gui/presentation/status.go index 5ac591c48..b3b210067 100644 --- a/pkg/gui/presentation/status.go +++ b/pkg/gui/presentation/status.go @@ -2,6 +2,7 @@ package presentation import ( "fmt" + "time" "github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/commands/types/enums" @@ -10,7 +11,6 @@ import ( "github.com/jesseduffield/lazygit/pkg/gui/style" "github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/i18n" - "github.com/jesseduffield/lazygit/pkg/utils" ) func FormatStatus( @@ -25,8 +25,8 @@ func FormatStatus( status := "" if currentBranch.IsRealBranch() { - status += ColoredBranchStatus(currentBranch, itemOperation, tr, userConfig) - if utils.Decolorise(status) != "" { + status += BranchStatus(currentBranch, itemOperation, tr, time.Now(), userConfig) + if status != "" { status += " " } } diff --git a/pkg/integration/tests/sync/pull_merge.go b/pkg/integration/tests/sync/pull_merge.go index d4809f00e..39e447ebc 100644 --- a/pkg/integration/tests/sync/pull_merge.go +++ b/pkg/integration/tests/sync/pull_merge.go @@ -33,7 +33,7 @@ var PullMerge = NewIntegrationTest(NewIntegrationTestArgs{ Contains("one"), ) - t.Views().Status().Content(Equals("↑1↓2 repo → master")) + t.Views().Status().Content(Equals("↓2↑1 repo → master")) t.Views().Files(). IsFocused(). diff --git a/pkg/integration/tests/sync/pull_merge_conflict.go b/pkg/integration/tests/sync/pull_merge_conflict.go index 6e05fafae..2161f6abd 100644 --- a/pkg/integration/tests/sync/pull_merge_conflict.go +++ b/pkg/integration/tests/sync/pull_merge_conflict.go @@ -34,7 +34,7 @@ var PullMergeConflict = NewIntegrationTest(NewIntegrationTestArgs{ Contains("one"), ) - t.Views().Status().Content(Equals("↑1↓2 repo → master")) + t.Views().Status().Content(Equals("↓2↑1 repo → master")) t.Views().Files(). IsFocused(). diff --git a/pkg/integration/tests/sync/pull_rebase.go b/pkg/integration/tests/sync/pull_rebase.go index 158cdf3d3..a2657ffe6 100644 --- a/pkg/integration/tests/sync/pull_rebase.go +++ b/pkg/integration/tests/sync/pull_rebase.go @@ -35,7 +35,7 @@ var PullRebase = NewIntegrationTest(NewIntegrationTestArgs{ Contains("one"), ) - t.Views().Status().Content(Equals("↑1↓2 repo → master")) + t.Views().Status().Content(Equals("↓2↑1 repo → master")) t.Views().Files(). IsFocused(). diff --git a/pkg/integration/tests/sync/pull_rebase_conflict.go b/pkg/integration/tests/sync/pull_rebase_conflict.go index fa2920b7b..d9541e0ed 100644 --- a/pkg/integration/tests/sync/pull_rebase_conflict.go +++ b/pkg/integration/tests/sync/pull_rebase_conflict.go @@ -34,7 +34,7 @@ var PullRebaseConflict = NewIntegrationTest(NewIntegrationTestArgs{ Contains("one"), ) - t.Views().Status().Content(Equals("↑1↓2 repo → master")) + t.Views().Status().Content(Equals("↓2↑1 repo → master")) t.Views().Files(). IsFocused(). diff --git a/pkg/integration/tests/sync/pull_rebase_interactive_conflict.go b/pkg/integration/tests/sync/pull_rebase_interactive_conflict.go index e93ca2e37..bf0fc050b 100644 --- a/pkg/integration/tests/sync/pull_rebase_interactive_conflict.go +++ b/pkg/integration/tests/sync/pull_rebase_interactive_conflict.go @@ -38,7 +38,7 @@ var PullRebaseInteractiveConflict = NewIntegrationTest(NewIntegrationTestArgs{ Contains("one"), ) - t.Views().Status().Content(Equals("↑2↓2 repo → master")) + t.Views().Status().Content(Equals("↓2↑2 repo → master")) t.Views().Files(). IsFocused(). 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 eba3ba746..3eee12efd 100644 --- a/pkg/integration/tests/sync/pull_rebase_interactive_conflict_drop.go +++ b/pkg/integration/tests/sync/pull_rebase_interactive_conflict_drop.go @@ -38,7 +38,7 @@ var PullRebaseInteractiveConflictDrop = NewIntegrationTest(NewIntegrationTestArg Contains("one"), ) - t.Views().Status().Content(Equals("↑2↓2 repo → master")) + t.Views().Status().Content(Equals("↓2↑2 repo → master")) t.Views().Files(). IsFocused(). From 373b1970cab78b639bbdb5e5a7c684735f4d95a7 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Tue, 30 Apr 2024 12:34:05 +0200 Subject: [PATCH 7/7] Show divergence from base branch in branches list --- docs/Config.md | 4 + pkg/commands/git_commands/branch_loader.go | 74 ++++++++++++++++++- pkg/commands/models/branch.go | 10 ++- pkg/config/user_config.go | 46 ++++++------ pkg/config/user_config_validation.go | 7 +- pkg/gui/controllers/helpers/refresh_helper.go | 30 ++++++-- pkg/gui/presentation/branches.go | 50 +++++++------ pkg/gui/presentation/branches_test.go | 73 ++++++++++++++++++ .../show_divergence_from_base_branch.go | 27 +++++++ pkg/integration/tests/test_list.go | 1 + schema/config.json | 10 +++ 11 files changed, 281 insertions(+), 51 deletions(-) create mode 100644 pkg/integration/tests/status/show_divergence_from_base_branch.go diff --git a/docs/Config.md b/docs/Config.md index 5662a4d73..7cc602c0b 100644 --- a/docs/Config.md +++ b/docs/Config.md @@ -187,6 +187,10 @@ gui: # If true, show commit hashes alongside branch names in the branches view. showBranchCommitHash: false + # Whether to show the divergence from the base branch in the branches view. + # One of: 'none' | 'onlyArrow' | 'arrowAndNumber' + showDivergenceFromBaseBranch: none + # Height of the command log view commandLogSize: 8 diff --git a/pkg/commands/git_commands/branch_loader.go b/pkg/commands/git_commands/branch_loader.go index 1052626a9..929d5964d 100644 --- a/pkg/commands/git_commands/branch_loader.go +++ b/pkg/commands/git_commands/branch_loader.go @@ -5,6 +5,7 @@ import ( "regexp" "strconv" "strings" + "time" "github.com/jesseduffield/generics/set" "github.com/jesseduffield/go-git/v5/config" @@ -14,6 +15,7 @@ import ( "github.com/jesseduffield/lazygit/pkg/utils" "github.com/samber/lo" "golang.org/x/exp/slices" + "golang.org/x/sync/errgroup" ) // context: @@ -63,7 +65,13 @@ 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, + mainBranches *MainBranches, + oldBranches []*models.Branch, + loadBehindCounts bool, + onWorker func(func() error), + renderFunc func(), +) ([]*models.Branch, error) { branches := self.obtainBranches(self.version.IsAtLeast(2, 22, 0)) if self.AppState.LocalBranchSortOrder == "recency" { @@ -122,11 +130,75 @@ func (self *BranchLoader) Load(reflogCommits []*models.Commit) ([]*models.Branch branch.UpstreamRemote = match.Remote branch.UpstreamBranch = match.Merge.Short() } + + // If the branch already existed, take over its BehindBaseBranch value + // to reduce flicker + if oldBranch, found := lo.Find(oldBranches, func(b *models.Branch) bool { + return b.Name == branch.Name + }); found { + branch.BehindBaseBranch.Store(oldBranch.BehindBaseBranch.Load()) + } + } + + if loadBehindCounts && self.UserConfig.Gui.ShowDivergenceFromBaseBranch != "none" { + onWorker(func() error { + return self.GetBehindBaseBranchValuesForAllBranches(branches, mainBranches, renderFunc) + }) } return branches, nil } +func (self *BranchLoader) GetBehindBaseBranchValuesForAllBranches( + branches []*models.Branch, + mainBranches *MainBranches, + renderFunc func(), +) error { + mainBranchRefs := mainBranches.Get() + if len(mainBranchRefs) == 0 { + return nil + } + + t := time.Now() + errg := errgroup.Group{} + + for _, branch := range branches { + errg.Go(func() error { + baseBranch, err := self.GetBaseBranch(branch, mainBranches) + if err != nil { + return err + } + behind := 0 // prime it in case something below fails + if baseBranch != "" { + 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 + } + // The format of the output is "\t" + aheadBehindStr := strings.Split(strings.TrimSpace(output), "\t") + if len(aheadBehindStr) == 2 { + if value, err := strconv.Atoi(aheadBehindStr[1]); err == nil { + behind = value + } + } + } + branch.BehindBaseBranch.Store(int32(behind)) + return nil + }) + } + + err := errg.Wait() + self.Log.Debugf("time to get behind base branch values for all branches: %s", time.Since(t)) + renderFunc() + return err +} + // Find the base branch for the given branch (i.e. the main branch that the // given branch was forked off of) // diff --git a/pkg/commands/models/branch.go b/pkg/commands/models/branch.go index 5a34ba5e8..04f869ebd 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 @@ -32,6 +35,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/config/user_config.go b/pkg/config/user_config.go index 41d3dfe10..c8895710e 100644 --- a/pkg/config/user_config.go +++ b/pkg/config/user_config.go @@ -129,6 +129,9 @@ type GuiConfig struct { CommitHashLength int `yaml:"commitHashLength" jsonschema:"minimum=0"` // If true, show commit hashes alongside branch names in the branches view. ShowBranchCommitHash bool `yaml:"showBranchCommitHash"` + // Whether to show the divergence from the base branch in the branches view. + // One of: 'none' | 'onlyArrow' | 'arrowAndNumber' + ShowDivergenceFromBaseBranch string `yaml:"showDivergenceFromBaseBranch" jsonschema:"enum=none,enum=onlyArrow,enum=arrowAndNumber"` // Height of the command log view CommandLogSize int `yaml:"commandLogSize" jsonschema:"minimum=0"` // Whether to split the main window when viewing file changes. @@ -673,27 +676,28 @@ func GetDefaultConfig() *UserConfig { UnstagedChangesColor: []string{"red"}, DefaultFgColor: []string{"default"}, }, - CommitLength: CommitLengthConfig{Show: true}, - SkipNoStagedFilesWarning: false, - ShowListFooter: true, - ShowCommandLog: true, - ShowBottomLine: true, - ShowPanelJumps: true, - ShowFileTree: true, - ShowRandomTip: true, - ShowIcons: false, - NerdFontsVersion: "", - ShowFileIcons: true, - CommitHashLength: 8, - ShowBranchCommitHash: false, - CommandLogSize: 8, - SplitDiff: "auto", - SkipRewordInEditorWarning: false, - WindowSize: "normal", - Border: "rounded", - AnimateExplosion: true, - PortraitMode: "auto", - FilterMode: "substring", + CommitLength: CommitLengthConfig{Show: true}, + SkipNoStagedFilesWarning: false, + ShowListFooter: true, + ShowCommandLog: true, + ShowBottomLine: true, + ShowPanelJumps: true, + ShowFileTree: true, + ShowRandomTip: true, + ShowIcons: false, + NerdFontsVersion: "", + ShowFileIcons: true, + CommitHashLength: 8, + ShowBranchCommitHash: false, + ShowDivergenceFromBaseBranch: "none", + CommandLogSize: 8, + SplitDiff: "auto", + SkipRewordInEditorWarning: false, + WindowSize: "normal", + Border: "rounded", + AnimateExplosion: true, + PortraitMode: "auto", + FilterMode: "substring", Spinner: SpinnerConfig{ Frames: []string{"|", "/", "-", "\\"}, Rate: 50, diff --git a/pkg/config/user_config_validation.go b/pkg/config/user_config_validation.go index 945979db9..403119ada 100644 --- a/pkg/config/user_config_validation.go +++ b/pkg/config/user_config_validation.go @@ -7,7 +7,12 @@ import ( ) func (config *UserConfig) Validate() error { - if err := validateEnum("gui.statusPanelView", config.Gui.StatusPanelView, []string{"dashboard", "allBranchesLog"}); err != nil { + if err := validateEnum("gui.statusPanelView", config.Gui.StatusPanelView, + []string{"dashboard", "allBranchesLog"}); err != nil { + return err + } + if err := validateEnum("gui.showDivergenceFromBaseBranch", config.Gui.ShowDivergenceFromBaseBranch, + []string{"none", "onlyArrow", "arrowAndNumber"}); err != nil { return err } return nil diff --git a/pkg/gui/controllers/helpers/refresh_helper.go b/pkg/gui/controllers/helpers/refresh_helper.go index 02a26ded9..4872add08 100644 --- a/pkg/gui/controllers/helpers/refresh_helper.go +++ b/pkg/gui/controllers/helpers/refresh_helper.go @@ -130,7 +130,7 @@ func (self *RefreshHelper) Refresh(options types.RefreshOptions) error { if self.c.AppState.LocalBranchSortOrder == "recency" { refresh("reflog and branches", func() { self.refreshReflogAndBranches(includeWorktreesWithBranches, options.KeepBranchSelectionIndex) }) } else { - refresh("branches", func() { self.refreshBranches(includeWorktreesWithBranches, options.KeepBranchSelectionIndex) }) + refresh("branches", func() { self.refreshBranches(includeWorktreesWithBranches, options.KeepBranchSelectionIndex, true) }) refresh("reflog", func() { _ = self.refreshReflogCommits() }) } } else if scopeSet.Includes(types.REBASE_COMMITS) { @@ -256,7 +256,7 @@ func (self *RefreshHelper) refreshReflogCommitsConsideringStartup() { case types.INITIAL: self.c.OnWorker(func(_ gocui.Task) error { _ = self.refreshReflogCommits() - self.refreshBranches(false, true) + self.refreshBranches(false, true, true) self.c.State().GetRepoState().SetStartupStage(types.COMPLETE) return nil }) @@ -267,9 +267,11 @@ func (self *RefreshHelper) refreshReflogCommitsConsideringStartup() { } func (self *RefreshHelper) refreshReflogAndBranches(refreshWorktrees bool, keepBranchSelectionIndex bool) { + loadBehindCounts := self.c.State().GetRepoState().GetStartupStage() == types.COMPLETE + self.refreshReflogCommitsConsideringStartup() - self.refreshBranches(refreshWorktrees, keepBranchSelectionIndex) + self.refreshBranches(refreshWorktrees, keepBranchSelectionIndex, loadBehindCounts) } func (self *RefreshHelper) refreshCommitsAndCommitFiles() { @@ -438,7 +440,7 @@ func (self *RefreshHelper) refreshStateSubmoduleConfigs() error { // self.refreshStatus is called at the end of this because that's when we can // be sure there is a State.Model.Branches array to pick the current branch from -func (self *RefreshHelper) refreshBranches(refreshWorktrees bool, keepBranchSelectionIndex bool) { +func (self *RefreshHelper) refreshBranches(refreshWorktrees bool, keepBranchSelectionIndex bool, loadBehindCounts bool) { self.c.Mutexes().RefreshingBranchesMutex.Lock() defer self.c.Mutexes().RefreshingBranchesMutex.Unlock() @@ -457,7 +459,25 @@ 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().MainBranches, + self.c.Model().Branches, + loadBehindCounts, + 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) + } + self.refreshStatus() + 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 0e48a3935..aab51fe61 100644 --- a/pkg/gui/presentation/branches.go +++ b/pkg/gui/presentation/branches.go @@ -155,32 +155,38 @@ 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.IsBehindForPull() && branch.IsAheadForPull() { + result = style.FgYellow.Sprintf("↓%s↑%s", branch.BehindForPull, branch.AheadForPull) + } else if branch.IsBehindForPull() { + result = style.FgYellow.Sprintf("↓%s", branch.BehindForPull) + } else if branch.IsAheadForPull() { + result = style.FgYellow.Sprintf("↑%s", branch.AheadForPull) + } } - if branch.UpstreamGone { - return style.FgRed.Sprint(tr.UpstreamGone) + if userConfig.Gui.ShowDivergenceFromBaseBranch != "none" { + behind := branch.BehindBaseBranch.Load() + if behind != 0 { + if result != "" { + result += " " + } + if userConfig.Gui.ShowDivergenceFromBaseBranch == "arrowAndNumber" { + result += style.FgCyan.Sprintf("↓%d", behind) + } else { + result += style.FgCyan.Sprintf("↓") + } + } } - if branch.MatchesUpstream() { - return style.FgGreen.Sprint("✓") - } - if branch.RemoteBranchNotStoredLocally() { - return style.FgMagenta.Sprint("?") - } - - if branch.IsBehindForPull() && branch.IsAheadForPull() { - return style.FgYellow.Sprintf("↓%s↑%s", branch.BehindForPull, branch.AheadForPull) - } - if branch.IsBehindForPull() { - return style.FgYellow.Sprintf("↓%s", branch.BehindForPull) - } - if branch.IsAheadForPull() { - return style.FgYellow.Sprintf("↑%s", branch.AheadForPull) - } - - return "" + return result } func SetCustomBranches(customBranchColors map[string]string) { diff --git a/pkg/gui/presentation/branches_test.go b/pkg/gui/presentation/branches_test.go index db4868970..ba79f16ce 100644 --- a/pkg/gui/presentation/branches_test.go +++ b/pkg/gui/presentation/branches_test.go @@ -2,6 +2,7 @@ package presentation import ( "fmt" + "sync/atomic" "testing" "time" @@ -15,6 +16,11 @@ import ( "github.com/xo/terminfo" ) +func makeAtomic(v int32) (result atomic.Int32) { + result.Store(v) + return //nolint: nakedret +} + func Test_getBranchDisplayStrings(t *testing.T) { scenarios := []struct { branch *models.Branch @@ -23,6 +29,7 @@ func Test_getBranchDisplayStrings(t *testing.T) { viewWidth int useIcons bool checkedOutByWorktree bool + showDivergenceCfg string expected []string }{ // First some tests for when the view is wide enough so that everything fits: @@ -33,6 +40,7 @@ func Test_getBranchDisplayStrings(t *testing.T) { viewWidth: 100, useIcons: false, checkedOutByWorktree: false, + showDivergenceCfg: "none", expected: []string{"1m", "branch_name"}, }, { @@ -42,6 +50,7 @@ func Test_getBranchDisplayStrings(t *testing.T) { viewWidth: 100, useIcons: false, checkedOutByWorktree: true, + showDivergenceCfg: "none", expected: []string{"1m", "branch_name (worktree)"}, }, { @@ -51,6 +60,7 @@ func Test_getBranchDisplayStrings(t *testing.T) { viewWidth: 100, useIcons: true, checkedOutByWorktree: true, + showDivergenceCfg: "none", expected: []string{"1m", "󰘬", "branch_name 󰌹"}, }, { @@ -66,6 +76,7 @@ func Test_getBranchDisplayStrings(t *testing.T) { viewWidth: 100, useIcons: false, checkedOutByWorktree: false, + showDivergenceCfg: "none", expected: []string{"1m", "branch_name ✓"}, }, { @@ -81,8 +92,57 @@ func Test_getBranchDisplayStrings(t *testing.T) { viewWidth: 100, useIcons: false, checkedOutByWorktree: true, + showDivergenceCfg: "none", expected: []string{"1m", "branch_name (worktree) ↓5↑3"}, }, + { + branch: &models.Branch{ + Name: "branch_name", + Recency: "1m", + BehindBaseBranch: makeAtomic(2), + }, + itemOperation: types.ItemOperationNone, + fullDescription: false, + viewWidth: 100, + useIcons: false, + checkedOutByWorktree: false, + showDivergenceCfg: "onlyArrow", + expected: []string{"1m", "branch_name ↓"}, + }, + { + branch: &models.Branch{ + Name: "branch_name", + Recency: "1m", + UpstreamRemote: "origin", + AheadForPull: "0", + BehindForPull: "0", + BehindBaseBranch: makeAtomic(2), + }, + itemOperation: types.ItemOperationNone, + fullDescription: false, + viewWidth: 100, + useIcons: false, + checkedOutByWorktree: false, + showDivergenceCfg: "arrowAndNumber", + expected: []string{"1m", "branch_name ✓ ↓2"}, + }, + { + branch: &models.Branch{ + Name: "branch_name", + Recency: "1m", + UpstreamRemote: "origin", + AheadForPull: "3", + BehindForPull: "5", + BehindBaseBranch: makeAtomic(2), + }, + itemOperation: types.ItemOperationNone, + fullDescription: false, + viewWidth: 100, + useIcons: false, + checkedOutByWorktree: false, + showDivergenceCfg: "arrowAndNumber", + expected: []string{"1m", "branch_name ↓5↑3 ↓2"}, + }, { branch: &models.Branch{Name: "branch_name", Recency: "1m"}, itemOperation: types.ItemOperationPushing, @@ -90,6 +150,7 @@ func Test_getBranchDisplayStrings(t *testing.T) { viewWidth: 100, useIcons: false, checkedOutByWorktree: false, + showDivergenceCfg: "none", expected: []string{"1m", "branch_name Pushing |"}, }, { @@ -108,6 +169,7 @@ func Test_getBranchDisplayStrings(t *testing.T) { viewWidth: 100, useIcons: false, checkedOutByWorktree: false, + showDivergenceCfg: "none", expected: []string{"1m", "12345678", "branch_name ✓", "origin branch_name", "commit title"}, }, @@ -119,6 +181,7 @@ func Test_getBranchDisplayStrings(t *testing.T) { viewWidth: 14, useIcons: false, checkedOutByWorktree: false, + showDivergenceCfg: "none", expected: []string{"1m", "branch_na…"}, }, { @@ -128,6 +191,7 @@ func Test_getBranchDisplayStrings(t *testing.T) { viewWidth: 14, useIcons: false, checkedOutByWorktree: true, + showDivergenceCfg: "none", expected: []string{"1m", "bra… (worktree)"}, }, { @@ -137,6 +201,7 @@ func Test_getBranchDisplayStrings(t *testing.T) { viewWidth: 14, useIcons: true, checkedOutByWorktree: true, + showDivergenceCfg: "none", expected: []string{"1m", "󰘬", "branc… 󰌹"}, }, { @@ -152,6 +217,7 @@ func Test_getBranchDisplayStrings(t *testing.T) { viewWidth: 14, useIcons: false, checkedOutByWorktree: false, + showDivergenceCfg: "none", expected: []string{"1m", "branch_… ✓"}, }, { @@ -167,6 +233,7 @@ func Test_getBranchDisplayStrings(t *testing.T) { viewWidth: 30, useIcons: false, checkedOutByWorktree: true, + showDivergenceCfg: "none", expected: []string{"1m", "branch_na… (worktree) ↓5↑3"}, }, { @@ -176,6 +243,7 @@ func Test_getBranchDisplayStrings(t *testing.T) { viewWidth: 20, useIcons: false, checkedOutByWorktree: false, + showDivergenceCfg: "none", expected: []string{"1m", "branc… Pushing |"}, }, { @@ -185,6 +253,7 @@ func Test_getBranchDisplayStrings(t *testing.T) { viewWidth: -1, useIcons: false, checkedOutByWorktree: false, + showDivergenceCfg: "none", expected: []string{"1m", "abc Pushing |"}, }, { @@ -194,6 +263,7 @@ func Test_getBranchDisplayStrings(t *testing.T) { viewWidth: -1, useIcons: false, checkedOutByWorktree: false, + showDivergenceCfg: "none", expected: []string{"1m", "ab Pushing |"}, }, { @@ -203,6 +273,7 @@ func Test_getBranchDisplayStrings(t *testing.T) { viewWidth: -1, useIcons: false, checkedOutByWorktree: false, + showDivergenceCfg: "none", expected: []string{"1m", "a Pushing |"}, }, { @@ -221,6 +292,7 @@ func Test_getBranchDisplayStrings(t *testing.T) { viewWidth: 20, useIcons: false, checkedOutByWorktree: false, + showDivergenceCfg: "none", expected: []string{"1m", "12345678", "bran… ✓", "origin branch_name", "commit title"}, }, } @@ -232,6 +304,7 @@ func Test_getBranchDisplayStrings(t *testing.T) { for i, s := range scenarios { icons.SetNerdFontsVersion(lo.Ternary(s.useIcons, "3", "")) + c.UserConfig.Gui.ShowDivergenceFromBaseBranch = s.showDivergenceCfg worktrees := []*models.Worktree{} if s.checkedOutByWorktree { diff --git a/pkg/integration/tests/status/show_divergence_from_base_branch.go b/pkg/integration/tests/status/show_divergence_from_base_branch.go new file mode 100644 index 000000000..53ab0ab2f --- /dev/null +++ b/pkg/integration/tests/status/show_divergence_from_base_branch.go @@ -0,0 +1,27 @@ +package status + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var ShowDivergenceFromBaseBranch = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Show divergence from base branch in the status panel", + ExtraCmdArgs: []string{}, + Skip: false, + SetupConfig: func(config *config.AppConfig) { + config.UserConfig.Gui.ShowDivergenceFromBaseBranch = "arrowAndNumber" + }, + SetupRepo: func(shell *Shell) { + shell.CreateNCommits(2) + shell.CloneIntoRemote("origin") + shell.NewBranch("feature") + shell.HardReset("HEAD^") + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.GlobalPress(keys.Universal.NextBlock) + + t.Views().Status(). + Content(Equals("↓1 repo → feature")) + }, +}) diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go index d4a093de8..0ab0e4331 100644 --- a/pkg/integration/tests/test_list.go +++ b/pkg/integration/tests/test_list.go @@ -266,6 +266,7 @@ var tests = []*components.IntegrationTest{ status.ClickRepoNameToOpenReposMenu, status.ClickToFocus, status.ClickWorkingTreeStateToOpenRebaseOptionsMenu, + status.ShowDivergenceFromBaseBranch, submodule.Add, submodule.Enter, submodule.EnterNested, diff --git a/schema/config.json b/schema/config.json index f5f7bab86..cf25ce007 100644 --- a/schema/config.json +++ b/schema/config.json @@ -331,6 +331,16 @@ "description": "If true, show commit hashes alongside branch names in the branches view.", "default": false }, + "showDivergenceFromBaseBranch": { + "type": "string", + "enum": [ + "none", + "onlyArrow", + "arrowAndNumber" + ], + "description": "Whether to show the divergence from the base branch in the branches view.\nOne of: 'none' | 'onlyArrow' | 'arrowAndNumber'", + "default": "none" + }, "commandLogSize": { "type": "integer", "minimum": 0,