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.
This commit is contained in:
Stefan Haller
2026-08-02 13:06:21 +02:00
parent 320d33a8ef
commit 2669842445
4 changed files with 123 additions and 15 deletions
+1 -1
View File
@@ -211,7 +211,7 @@ func (self *BranchesController) GetOnRenderToMain() func() {
pr, ok := self.c.Model().PullRequestsMap[branch.Name]
if ok && presentation.ShouldShowPrForBranch(pr, branch.Name, self.c.UserConfig()) {
ptyTask.Prefix = presentation.FormatPullRequestHeader(pr)
ptyTask.Prefix = presentation.FormatPullRequestHeader(pr, self.c.Tr)
ptyTask.Prefix += strings.Repeat("─", self.c.Contexts().Normal.GetView().InnerWidth()) + "\n"
}
}
+45 -7
View File
@@ -150,6 +150,12 @@ func getBranchDisplayStrings(
prIcon = "●"
}
coloredPrIcon = WithPrColor(pr.State, prIcon, false)
if pr.State == "OPEN" {
icon, _, textStyle := checksStatePresentation(pr.ChecksState, tr)
if icon != "" {
coloredPrIcon = textStyle.Sprint(icon)
}
}
}
res = append(res, coloredPrIcon)
@@ -287,14 +293,21 @@ func WithPrColor(state string, text string, isBg bool) string {
}
}
func FormatPullRequestHeader(pr *models.GithubPullRequest) string {
func FormatPullRequestHeader(pr *models.GithubPullRequest, tr *i18n.TranslationSet) string {
icon := lo.Ternary(icons.IsIconEnabled(), icons.IconForRemoteUrl(pr.Url)+" ", "")
return style.PrintHyperlink(fmt.Sprintf("%s%s %s %s\n",
icon,
coloredPullRequestStateText(pr.State),
pr.Title,
style.FgCyan.Sprintf("#%d", pr.Number)),
pr.Url)
stateText := coloredPullRequestStateText(pr.State)
checksStateText := coloredChecksStateText(pr.ChecksState, tr)
numberText := style.FgCyan.Sprintf("#%d", pr.Number)
// The checks status links to the checks tab, so it needs to be its own
// hyperlink separate from the rest of the header.
parts := []string{style.PrintHyperlink(icon+stateText, pr.Url)}
if checksStateText != "" {
parts = append(parts, style.PrintHyperlink(checksStateText, strings.TrimSuffix(pr.Url, "/")+"/checks"))
}
parts = append(parts, style.PrintHyperlink(fmt.Sprintf("%s %s\n", pr.Title, numberText), pr.Url))
return strings.Join(parts, " ")
}
func pullRequestStateText(state string) string {
@@ -328,6 +341,31 @@ func coloredPullRequestStateText(state string) string {
return WithPrColor(state, pullRequestStateText(state), false)
}
func checksStatePresentation(state string, tr *i18n.TranslationSet) (string, string, style.TextStyle) {
switch state {
case "SUCCESS":
return "✓", tr.PullRequestChecksPassing, style.FgGreen
case "PENDING":
return "●", tr.PullRequestChecksPending, style.FgYellow
case "FAILURE":
return "✗", tr.PullRequestChecksFailing, style.FgRed
case "ERROR":
return "!", tr.PullRequestChecksError, style.FgRed
case "EXPECTED":
return "○", tr.PullRequestChecksExpected, style.FgDefault
default:
return "", "", style.Nothing
}
}
func coloredChecksStateText(state string, tr *i18n.TranslationSet) string {
icon, text, textStyle := checksStatePresentation(state, tr)
if text != "" {
return textStyle.Sprintf("%s %s", icon, text)
}
return ""
}
func ShouldShowPrForBranch(pr *models.GithubPullRequest, branchName string, userConfig *config.UserConfig) bool {
if !lo.Contains(userConfig.Git.MainBranches, branchName) {
return true
+67 -7
View File
@@ -12,6 +12,7 @@ import (
"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"
@@ -29,17 +30,76 @@ func TestFormatPullRequestHeader(t *testing.T) {
icons.SetNerdFontsVersion("")
pr := &models.GithubPullRequest{
Title: "Improve checks",
Number: 5871,
State: "OPEN",
Url: "https://github.com/jesseduffield/lazygit/pull/5871",
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()
actual := FormatPullRequestHeader(pr)
t.Run("links checks separately from the rest of the header", func(t *testing.T) {
actual := FormatPullRequestHeader(pr, tr)
expected := style.PrintHyperlink("Open Improve checks "+numberText+"\n", pr.Url)
assert.Equal(t, expected, actual)
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) {
+10
View File
@@ -367,6 +367,11 @@ type TranslationSet struct {
FwdNoLocalUpstream string
FwdCommitsToPush string
PullRequestNoUpstream string
PullRequestChecksPassing string
PullRequestChecksPending string
PullRequestChecksFailing string
PullRequestChecksError string
PullRequestChecksExpected string
ErrorOccurred string
ConflictLabel string
PendingRebaseTodosSectionHeader string
@@ -1519,6 +1524,11 @@ func EnglishTranslationSet() *TranslationSet {
FwdNoLocalUpstream: "Cannot fast-forward a branch whose remote is not registered locally",
FwdCommitsToPush: "Cannot fast-forward a branch with commits to push",
PullRequestNoUpstream: "Cannot open a pull request for a branch with no upstream",
PullRequestChecksPassing: "Passing",
PullRequestChecksPending: "Pending",
PullRequestChecksFailing: "Failing",
PullRequestChecksError: "Error",
PullRequestChecksExpected: "Expected",
ErrorOccurred: "An error occurred! Please create an issue at",
ConflictLabel: "CONFLICT",
PendingRebaseTodosSectionHeader: "Pending rebase todos",