mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-09-14 09:36:24 -04:00
When a branch's fork point is reachable from more than one configured main branch and the candidates disagree on the behind count, the branches column was silently showing the config-order first candidate's number — confidently asserting something we don't actually know. Worse, "nothing" in the column means "up to date with the base", which may or may not be true under ambiguity. Compute behind values for every candidate (the fast path already had them; the legacy path now does too via baseBranchCandidatesAndBehinds), then classify: - all candidates agree → show that number (or nothing if 0) - some candidates 0, others not → "?" (we can't say if up to date) - all non-zero but differing → "↓?" (definitely behind, unknown amount) Two sentinel constants on the Branch model (BehindBaseAmbiguousMaybeUpToDate and BehindBaseAmbiguousDefinitelyBehind) encode these states in the existing atomic.Int32 field via negative values; the renderer switches on them.
419 lines
14 KiB
Go
419 lines
14 KiB
Go
package presentation
|
|
|
|
import (
|
|
"fmt"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/gookit/color"
|
|
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
|
"github.com/jesseduffield/lazygit/pkg/common"
|
|
"github.com/jesseduffield/lazygit/pkg/gui/presentation/icons"
|
|
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
|
"github.com/samber/lo"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/xo/terminfo"
|
|
)
|
|
|
|
func makeAtomic(v int32) *atomic.Int32 {
|
|
var result atomic.Int32
|
|
result.Store(v)
|
|
return &result
|
|
}
|
|
|
|
func Test_getBranchDisplayStrings(t *testing.T) {
|
|
scenarios := []struct {
|
|
branch *models.Branch
|
|
itemOperation types.ItemOperation
|
|
fullDescription bool
|
|
viewWidth int
|
|
useIcons bool
|
|
checkedOutByWorktree bool
|
|
showDivergenceCfg string
|
|
expected []string
|
|
}{
|
|
// First some tests for when the view is wide enough so that everything fits:
|
|
{
|
|
branch: &models.Branch{Name: "branch_name", Recency: "1m"},
|
|
itemOperation: types.ItemOperationNone,
|
|
fullDescription: false,
|
|
viewWidth: 100,
|
|
useIcons: false,
|
|
checkedOutByWorktree: false,
|
|
showDivergenceCfg: "none",
|
|
expected: []string{"1m", "", "branch_name"},
|
|
},
|
|
{
|
|
branch: &models.Branch{Name: "🍉_special_char", Recency: "1m"},
|
|
itemOperation: types.ItemOperationNone,
|
|
fullDescription: false,
|
|
viewWidth: 19,
|
|
useIcons: false,
|
|
checkedOutByWorktree: false,
|
|
showDivergenceCfg: "none",
|
|
expected: []string{"1m", "", "🍉_special_char"},
|
|
},
|
|
{
|
|
branch: &models.Branch{Name: "branch_name", Recency: "1m"},
|
|
itemOperation: types.ItemOperationNone,
|
|
fullDescription: false,
|
|
viewWidth: 100,
|
|
useIcons: false,
|
|
checkedOutByWorktree: true,
|
|
showDivergenceCfg: "none",
|
|
expected: []string{"1m", "", "branch_name (worktree other-worktree)"},
|
|
},
|
|
{
|
|
branch: &models.Branch{Name: "branch_name", Recency: "1m"},
|
|
itemOperation: types.ItemOperationNone,
|
|
fullDescription: false,
|
|
viewWidth: 100,
|
|
useIcons: true,
|
|
checkedOutByWorktree: true,
|
|
showDivergenceCfg: "none",
|
|
expected: []string{"1m", "", "branch_name ( other-worktree)"},
|
|
},
|
|
{
|
|
branch: &models.Branch{
|
|
Name: "branch_name",
|
|
Recency: "1m",
|
|
UpstreamRemote: "origin",
|
|
AheadForPull: "0",
|
|
BehindForPull: "0",
|
|
},
|
|
itemOperation: types.ItemOperationNone,
|
|
fullDescription: false,
|
|
viewWidth: 100,
|
|
useIcons: false,
|
|
checkedOutByWorktree: false,
|
|
showDivergenceCfg: "none",
|
|
expected: []string{"1m", "", "branch_name ✓"},
|
|
},
|
|
{
|
|
branch: &models.Branch{
|
|
Name: "branch_name",
|
|
Recency: "1m",
|
|
UpstreamRemote: "origin",
|
|
AheadForPull: "3",
|
|
BehindForPull: "5",
|
|
},
|
|
itemOperation: types.ItemOperationNone,
|
|
fullDescription: false,
|
|
viewWidth: 100,
|
|
useIcons: false,
|
|
checkedOutByWorktree: true,
|
|
showDivergenceCfg: "none",
|
|
expected: []string{"1m", "", "branch_name (worktree other-worktree) ↓5↑3"},
|
|
},
|
|
{
|
|
branch: &models.Branch{
|
|
Name: "branch_name",
|
|
Recency: "1m",
|
|
BehindBaseBranch: *makeAtomic(2),
|
|
},
|
|
itemOperation: types.ItemOperationNone,
|
|
fullDescription: false,
|
|
viewWidth: 20,
|
|
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: 22,
|
|
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: 26,
|
|
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,
|
|
fullDescription: false,
|
|
viewWidth: 100,
|
|
useIcons: false,
|
|
checkedOutByWorktree: false,
|
|
showDivergenceCfg: "none",
|
|
expected: []string{"1m", "", "branch_name Pushing |"},
|
|
},
|
|
{
|
|
branch: &models.Branch{
|
|
Name: "branch_name",
|
|
Recency: "1m",
|
|
CommitHash: "1234567890",
|
|
UpstreamRemote: "origin",
|
|
UpstreamBranch: "branch_name",
|
|
AheadForPull: "0",
|
|
BehindForPull: "0",
|
|
Subject: "commit title",
|
|
},
|
|
itemOperation: types.ItemOperationNone,
|
|
fullDescription: true,
|
|
viewWidth: 100,
|
|
useIcons: false,
|
|
checkedOutByWorktree: false,
|
|
showDivergenceCfg: "none",
|
|
expected: []string{"1m", "", "12345678", "branch_name ✓", "origin branch_name", "commit title"},
|
|
},
|
|
|
|
// Now tests for how we truncate the branch name when there's not enough room:
|
|
{
|
|
branch: &models.Branch{Name: "branch_name", Recency: "1m"},
|
|
itemOperation: types.ItemOperationNone,
|
|
fullDescription: false,
|
|
viewWidth: 14,
|
|
useIcons: false,
|
|
checkedOutByWorktree: false,
|
|
showDivergenceCfg: "none",
|
|
expected: []string{"1m", "", "branch_na…"},
|
|
},
|
|
{
|
|
branch: &models.Branch{Name: "🍉_special_char", Recency: "1m"},
|
|
itemOperation: types.ItemOperationNone,
|
|
fullDescription: false,
|
|
viewWidth: 18,
|
|
useIcons: false,
|
|
checkedOutByWorktree: false,
|
|
showDivergenceCfg: "none",
|
|
expected: []string{"1m", "", "🍉_special_ch…"},
|
|
},
|
|
{
|
|
branch: &models.Branch{Name: "branch_name", Recency: "1m"},
|
|
itemOperation: types.ItemOperationNone,
|
|
fullDescription: false,
|
|
viewWidth: 14,
|
|
useIcons: false,
|
|
checkedOutByWorktree: true,
|
|
showDivergenceCfg: "none",
|
|
expected: []string{"1m", "", "bra… (worktree)"},
|
|
},
|
|
{
|
|
branch: &models.Branch{Name: "branch_name", Recency: "1m"},
|
|
itemOperation: types.ItemOperationNone,
|
|
fullDescription: false,
|
|
viewWidth: 12,
|
|
useIcons: true,
|
|
checkedOutByWorktree: true,
|
|
showDivergenceCfg: "none",
|
|
expected: []string{"1m", "", "branc… "},
|
|
},
|
|
{
|
|
branch: &models.Branch{
|
|
Name: "branch_name",
|
|
Recency: "1m",
|
|
UpstreamRemote: "origin",
|
|
AheadForPull: "0",
|
|
BehindForPull: "0",
|
|
},
|
|
itemOperation: types.ItemOperationNone,
|
|
fullDescription: false,
|
|
viewWidth: 14,
|
|
useIcons: false,
|
|
checkedOutByWorktree: false,
|
|
showDivergenceCfg: "none",
|
|
expected: []string{"1m", "", "branch_… ✓"},
|
|
},
|
|
{
|
|
branch: &models.Branch{
|
|
Name: "branch_name",
|
|
Recency: "1m",
|
|
UpstreamRemote: "origin",
|
|
AheadForPull: "3",
|
|
BehindForPull: "5",
|
|
BehindBaseBranch: *makeAtomic(4),
|
|
},
|
|
itemOperation: types.ItemOperationNone,
|
|
fullDescription: false,
|
|
viewWidth: 21,
|
|
useIcons: false,
|
|
checkedOutByWorktree: false,
|
|
showDivergenceCfg: "arrowAndNumber",
|
|
expected: []string{"1m", "", "branch_n… ↓5↑3 ↓4"},
|
|
},
|
|
{
|
|
branch: &models.Branch{
|
|
Name: "branch_name",
|
|
Recency: "1m",
|
|
UpstreamRemote: "origin",
|
|
AheadForPull: "3",
|
|
BehindForPull: "5",
|
|
},
|
|
itemOperation: types.ItemOperationNone,
|
|
fullDescription: false,
|
|
viewWidth: 30,
|
|
useIcons: false,
|
|
checkedOutByWorktree: true,
|
|
showDivergenceCfg: "none",
|
|
expected: []string{"1m", "", "branch_na… (worktree) ↓5↑3"},
|
|
},
|
|
{
|
|
branch: &models.Branch{Name: "branch_name", Recency: "1m"},
|
|
itemOperation: types.ItemOperationPushing,
|
|
fullDescription: false,
|
|
viewWidth: 20,
|
|
useIcons: false,
|
|
checkedOutByWorktree: false,
|
|
showDivergenceCfg: "none",
|
|
expected: []string{"1m", "", "branc… Pushing |"},
|
|
},
|
|
// Ambiguous base, candidates disagree on whether branch is up to date
|
|
// → render "?" without an arrow (we don't know if it's behind).
|
|
{
|
|
branch: &models.Branch{
|
|
Name: "branch_name",
|
|
Recency: "1m",
|
|
BehindBaseBranch: *makeAtomic(models.BehindBaseAmbiguousMaybeUpToDate),
|
|
},
|
|
itemOperation: types.ItemOperationNone,
|
|
fullDescription: false,
|
|
viewWidth: 20,
|
|
useIcons: false,
|
|
checkedOutByWorktree: false,
|
|
showDivergenceCfg: "arrowAndNumber",
|
|
expected: []string{"1m", "", "branch_name ?"},
|
|
},
|
|
{
|
|
branch: &models.Branch{
|
|
Name: "branch_name",
|
|
Recency: "1m",
|
|
BehindBaseBranch: *makeAtomic(models.BehindBaseAmbiguousMaybeUpToDate),
|
|
},
|
|
itemOperation: types.ItemOperationNone,
|
|
fullDescription: false,
|
|
viewWidth: 20,
|
|
useIcons: false,
|
|
checkedOutByWorktree: false,
|
|
showDivergenceCfg: "onlyArrow",
|
|
expected: []string{"1m", "", "branch_name ?"},
|
|
},
|
|
// Ambiguous base, every candidate has branch behind by some non-zero
|
|
// amount → render "↓?" (arrowAndNumber) or "↓" (onlyArrow).
|
|
{
|
|
branch: &models.Branch{
|
|
Name: "branch_name",
|
|
Recency: "1m",
|
|
BehindBaseBranch: *makeAtomic(models.BehindBaseAmbiguousDefinitelyBehind),
|
|
},
|
|
itemOperation: types.ItemOperationNone,
|
|
fullDescription: false,
|
|
viewWidth: 20,
|
|
useIcons: false,
|
|
checkedOutByWorktree: false,
|
|
showDivergenceCfg: "arrowAndNumber",
|
|
expected: []string{"1m", "", "branch_name ↓?"},
|
|
},
|
|
{
|
|
branch: &models.Branch{
|
|
Name: "branch_name",
|
|
Recency: "1m",
|
|
BehindBaseBranch: *makeAtomic(models.BehindBaseAmbiguousDefinitelyBehind),
|
|
},
|
|
itemOperation: types.ItemOperationNone,
|
|
fullDescription: false,
|
|
viewWidth: 20,
|
|
useIcons: false,
|
|
checkedOutByWorktree: false,
|
|
showDivergenceCfg: "onlyArrow",
|
|
expected: []string{"1m", "", "branch_name ↓"},
|
|
},
|
|
{
|
|
branch: &models.Branch{Name: "abc", Recency: "1m"},
|
|
itemOperation: types.ItemOperationPushing,
|
|
fullDescription: false,
|
|
viewWidth: -1,
|
|
useIcons: false,
|
|
checkedOutByWorktree: false,
|
|
showDivergenceCfg: "none",
|
|
expected: []string{"1m", "", "abc Pushing |"},
|
|
},
|
|
{
|
|
branch: &models.Branch{Name: "ab", Recency: "1m"},
|
|
itemOperation: types.ItemOperationPushing,
|
|
fullDescription: false,
|
|
viewWidth: -1,
|
|
useIcons: false,
|
|
checkedOutByWorktree: false,
|
|
showDivergenceCfg: "none",
|
|
expected: []string{"1m", "", "ab Pushing |"},
|
|
},
|
|
{
|
|
branch: &models.Branch{Name: "a", Recency: "1m"},
|
|
itemOperation: types.ItemOperationPushing,
|
|
fullDescription: false,
|
|
viewWidth: -1,
|
|
useIcons: false,
|
|
checkedOutByWorktree: false,
|
|
showDivergenceCfg: "none",
|
|
expected: []string{"1m", "", "a Pushing |"},
|
|
},
|
|
{
|
|
branch: &models.Branch{
|
|
Name: "branch_name",
|
|
Recency: "1m",
|
|
CommitHash: "1234567890",
|
|
UpstreamRemote: "origin",
|
|
UpstreamBranch: "branch_name",
|
|
AheadForPull: "0",
|
|
BehindForPull: "0",
|
|
Subject: "commit title",
|
|
},
|
|
itemOperation: types.ItemOperationNone,
|
|
fullDescription: true,
|
|
viewWidth: 20,
|
|
useIcons: false,
|
|
checkedOutByWorktree: false,
|
|
showDivergenceCfg: "none",
|
|
expected: []string{"1m", "", "12345678", "bran… ✓", "origin branch_name", "commit title"},
|
|
},
|
|
}
|
|
|
|
oldColorLevel := color.ForceSetColorLevel(terminfo.ColorLevelNone)
|
|
defer color.ForceSetColorLevel(oldColorLevel)
|
|
|
|
c := common.NewDummyCommon()
|
|
SetCustomBranches(c.UserConfig().Gui.BranchColorPatterns, true)
|
|
|
|
for i, s := range scenarios {
|
|
icons.SetNerdFontsVersion(lo.Ternary(s.useIcons, "3", ""))
|
|
c.UserConfig().Gui.ShowDivergenceFromBaseBranch = s.showDivergenceCfg
|
|
|
|
worktrees := []*models.Worktree{}
|
|
if s.checkedOutByWorktree {
|
|
worktrees = append(worktrees, &models.Worktree{Branch: s.branch.Name, Name: "other-worktree"})
|
|
}
|
|
|
|
t.Run(fmt.Sprintf("getBranchDisplayStrings_%d", i), func(t *testing.T) {
|
|
strings := getBranchDisplayStrings(s.branch, s.itemOperation, s.fullDescription, false, s.viewWidth, c.Tr, c.UserConfig(), worktrees, time.Time{}, map[string]*models.GithubPullRequest{})
|
|
assert.Equal(t, s.expected, strings)
|
|
})
|
|
}
|
|
}
|