Files
Stefan Haller 2669842445 Show a Github PR's combined checks state in branches list and main view
In the branches list we show the checks icon (✓, ✗ etc) instead of the
gihub icon for branches that are open and have a state. It is a little
confusing, because the ✓ in front of the name means something very
different than the ✓ after it, but the checks status is just too useful
to see in the list.

In the main view we show it as a compact status before the PR title,
with a hyperlink that takes you directly to the checks tab in Github.
2026-08-02 13:06:21 +02:00

439 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/style"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/i18n"
"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 TestFormatPullRequestHeader(t *testing.T) {
oldColorLevel := color.ForceSetColorLevel(terminfo.ColorLevelNone)
defer color.ForceSetColorLevel(oldColorLevel)
icons.SetNerdFontsVersion("")
pr := &models.GithubPullRequest{
Title: "Improve checks",
Number: 5871,
State: "OPEN",
ChecksState: "SUCCESS",
Url: "https://github.com/jesseduffield/lazygit/pull/5871",
}
numberText := style.FgCyan.Sprint("#5871")
tr := i18n.EnglishTranslationSet()
t.Run("links checks separately from the rest of the header", func(t *testing.T) {
actual := FormatPullRequestHeader(pr, tr)
expected := style.PrintHyperlink("Open", pr.Url) +
" " +
style.PrintHyperlink("✓ Passing", pr.Url+"/checks") +
" " +
style.PrintHyperlink("Improve checks "+numberText+"\n", pr.Url)
assert.Equal(t, expected, actual)
})
t.Run("leaves the separator unlinked when checks are unavailable", func(t *testing.T) {
prWithoutChecks := *pr
prWithoutChecks.ChecksState = ""
actual := FormatPullRequestHeader(&prWithoutChecks, tr)
expected := style.PrintHyperlink("Open", pr.Url) +
" " +
style.PrintHyperlink("Improve checks "+numberText+"\n", pr.Url)
assert.Equal(t, expected, actual)
})
t.Run("avoids a double slash in the checks URL", func(t *testing.T) {
prWithTrailingSlash := *pr
prWithTrailingSlash.Url += "/"
actual := FormatPullRequestHeader(&prWithTrailingSlash, tr)
assert.Contains(t, actual, "https://github.com/jesseduffield/lazygit/pull/5871/checks")
assert.NotContains(t, actual, "pull/5871//checks")
})
}
func TestChecksStatePresentation(t *testing.T) {
tr := i18n.EnglishTranslationSet()
testCases := []struct {
name string
state string
expectedIcon string
expectedText string
expectedStyle style.TextStyle
}{
{name: "success", state: "SUCCESS", expectedIcon: "✓", expectedText: "Passing", expectedStyle: style.FgGreen},
{name: "pending", state: "PENDING", expectedIcon: "●", expectedText: "Pending", expectedStyle: style.FgYellow},
{name: "failure", state: "FAILURE", expectedIcon: "✗", expectedText: "Failing", expectedStyle: style.FgRed},
{name: "error", state: "ERROR", expectedIcon: "!", expectedText: "Error", expectedStyle: style.FgRed},
{name: "expected", state: "EXPECTED", expectedIcon: "○", expectedText: "Expected", expectedStyle: style.FgDefault},
{name: "empty", state: "", expectedIcon: "", expectedText: "", expectedStyle: style.Nothing},
{name: "unknown", state: "FUTURE_STATE", expectedIcon: "", expectedText: "", expectedStyle: style.Nothing},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
icon, text, textStyle := checksStatePresentation(testCase.state, tr)
assert.Equal(t, testCase.expectedIcon, icon)
assert.Equal(t, testCase.expectedText, text)
assert.Equal(t, testCase.expectedStyle, textStyle)
})
}
}
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", "", "bra… Pushing ●∙∙"},
},
{
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)
})
}
}