diff --git a/pkg/gui/controllers/branches_controller.go b/pkg/gui/controllers/branches_controller.go index 47f95aa91..cfc46b503 100644 --- a/pkg/gui/controllers/branches_controller.go +++ b/pkg/gui/controllers/branches_controller.go @@ -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" } } diff --git a/pkg/gui/presentation/branches.go b/pkg/gui/presentation/branches.go index b705b4b2f..f58f34dc6 100644 --- a/pkg/gui/presentation/branches.go +++ b/pkg/gui/presentation/branches.go @@ -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 diff --git a/pkg/gui/presentation/branches_test.go b/pkg/gui/presentation/branches_test.go index 178ddec69..3d83ca0ba 100644 --- a/pkg/gui/presentation/branches_test.go +++ b/pkg/gui/presentation/branches_test.go @@ -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) { diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index 812c30fc5..deb55d6e9 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -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",