mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-09-10 07:36:27 -04:00
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.
378 lines
11 KiB
Go
378 lines
11 KiB
Go
package presentation
|
|
|
|
import (
|
|
"fmt"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gookit/color"
|
|
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
|
|
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
|
"github.com/jesseduffield/lazygit/pkg/config"
|
|
"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/jesseduffield/lazygit/pkg/theme"
|
|
"github.com/jesseduffield/lazygit/pkg/utils"
|
|
"github.com/samber/lo"
|
|
)
|
|
|
|
type colorMatcher struct {
|
|
patterns map[string]*style.TextStyle
|
|
isRegex bool // NOTE: this value is needed only until the deprecated branchColors config is removed and only regex color patterns are used
|
|
}
|
|
|
|
var colorPatterns *colorMatcher
|
|
|
|
func GetBranchListDisplayStrings(
|
|
branches []*models.Branch,
|
|
getItemOperation func(item types.HasUrn) types.ItemOperation,
|
|
prs map[string]*models.GithubPullRequest,
|
|
fullDescription bool,
|
|
diffName string,
|
|
viewWidth int,
|
|
tr *i18n.TranslationSet,
|
|
userConfig *config.UserConfig,
|
|
worktrees []*models.Worktree,
|
|
) [][]string {
|
|
return lo.Map(branches, func(branch *models.Branch, _ int) []string {
|
|
diffed := branch.Name == diffName
|
|
return getBranchDisplayStrings(branch, getItemOperation(branch), fullDescription, diffed, viewWidth, tr, userConfig, worktrees, time.Now(), prs)
|
|
})
|
|
}
|
|
|
|
// getBranchDisplayStrings returns the display string of branch
|
|
func getBranchDisplayStrings(
|
|
b *models.Branch,
|
|
itemOperation types.ItemOperation,
|
|
fullDescription bool,
|
|
diffed bool,
|
|
viewWidth int,
|
|
tr *i18n.TranslationSet,
|
|
userConfig *config.UserConfig,
|
|
worktrees []*models.Worktree,
|
|
now time.Time,
|
|
prs map[string]*models.GithubPullRequest,
|
|
) []string {
|
|
checkedOutByWorkTree := git_commands.CheckedOutByOtherWorktree(b, worktrees)
|
|
showCommitHash := fullDescription || userConfig.Gui.ShowBranchCommitHash
|
|
branchStatus := BranchStatus(b, itemOperation, tr, now, userConfig)
|
|
divergence := divergenceStr(b, itemOperation, tr, userConfig)
|
|
|
|
// Recency is always three characters, plus one for the space
|
|
availableWidth := viewWidth - 4
|
|
if len(divergence) > 0 {
|
|
availableWidth -= utils.StringWidth(divergence) + 1
|
|
}
|
|
if showCommitHash {
|
|
availableWidth -= utils.COMMIT_HASH_SHORT_SIZE + 1
|
|
}
|
|
if len(prs) > 0 {
|
|
// if we have PRs then we assume that at least one branch in the list has one
|
|
availableWidth -= 2
|
|
}
|
|
paddingNeededForDivergence := availableWidth
|
|
|
|
displayName := b.Name
|
|
if b.DisplayName != "" {
|
|
displayName = b.DisplayName
|
|
}
|
|
|
|
if len(branchStatus) > 0 {
|
|
availableWidth -= utils.StringWidth(utils.Decolorise(branchStatus)) + 1
|
|
}
|
|
|
|
worktreeIcon := ""
|
|
if checkedOutByWorkTree {
|
|
if wt, ok := git_commands.WorktreeForBranch(b, worktrees); ok && wt.Name != b.Name {
|
|
if icons.IsIconEnabled() {
|
|
worktreeIcon = fmt.Sprintf("(%s %s)", icons.LINKED_WORKTREE_ICON, wt.Name)
|
|
} else {
|
|
worktreeIcon = fmt.Sprintf("(%s %s)", tr.LcWorktree, wt.Name)
|
|
}
|
|
|
|
// If the worktree name doesn't fit in the available width, omit it
|
|
remaining := availableWidth - utils.StringWidth(worktreeIcon) - 1
|
|
if remaining < utils.StringWidth(displayName) {
|
|
if icons.IsIconEnabled() {
|
|
worktreeIcon = icons.LINKED_WORKTREE_ICON
|
|
} else {
|
|
worktreeIcon = fmt.Sprintf("(%s)", tr.LcWorktree)
|
|
}
|
|
}
|
|
} else {
|
|
if icons.IsIconEnabled() {
|
|
worktreeIcon = icons.LINKED_WORKTREE_ICON
|
|
} else {
|
|
worktreeIcon = fmt.Sprintf("(%s)", tr.LcWorktree)
|
|
}
|
|
}
|
|
|
|
availableWidth -= utils.StringWidth(worktreeIcon) + 1
|
|
}
|
|
|
|
nameTextStyle := GetBranchTextStyle(b.Name)
|
|
if diffed {
|
|
nameTextStyle = theme.DiffTerminalColor
|
|
}
|
|
|
|
// Don't bother shortening branch names that are already 3 characters or less
|
|
if utils.StringWidth(displayName) > max(availableWidth, 3) {
|
|
// Never shorten the branch name to less then 3 characters
|
|
len := max(availableWidth, 4)
|
|
displayName = utils.TruncateWithEllipsis(displayName, len)
|
|
}
|
|
coloredName := nameTextStyle.Sprint(displayName)
|
|
if checkedOutByWorkTree {
|
|
coloredName = fmt.Sprintf("%s %s", coloredName, style.FgDefault.Sprint(worktreeIcon))
|
|
}
|
|
if len(branchStatus) > 0 {
|
|
coloredName = fmt.Sprintf("%s %s", coloredName, branchStatus)
|
|
}
|
|
|
|
recencyColor := style.FgCyan
|
|
if b.Recency == " *" {
|
|
recencyColor = style.FgGreen
|
|
}
|
|
|
|
res := make([]string, 0, 6)
|
|
res = append(res, recencyColor.Sprint(b.Recency))
|
|
|
|
var coloredPrIcon string
|
|
pr, hasPr := prs[b.Name]
|
|
if hasPr && ShouldShowPrForBranch(pr, b.Name, userConfig) {
|
|
var prIcon string
|
|
if icons.IsIconEnabled() {
|
|
prIcon = icons.IconForRemoteUrl(pr.Url)
|
|
} else {
|
|
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)
|
|
|
|
if showCommitHash {
|
|
res = append(res, utils.ShortHash(b.CommitHash))
|
|
}
|
|
|
|
if divergence != "" {
|
|
if fullDescription {
|
|
// don't right-align the divergence in half or full screen mode, since other fields
|
|
// follow in that case and we don't know the width of our column
|
|
paddingNeededForDivergence = 1
|
|
} else {
|
|
paddingNeededForDivergence -= utils.StringWidth(utils.Decolorise(coloredName)) - 1
|
|
}
|
|
if paddingNeededForDivergence > 0 {
|
|
coloredName += strings.Repeat(" ", paddingNeededForDivergence)
|
|
coloredName += style.FgCyan.Sprint(divergence)
|
|
}
|
|
}
|
|
res = append(res, coloredName)
|
|
|
|
if fullDescription {
|
|
res = append(
|
|
res,
|
|
fmt.Sprintf("%s %s",
|
|
style.FgYellow.Sprint(b.UpstreamRemote),
|
|
style.FgYellow.Sprint(b.UpstreamBranch),
|
|
),
|
|
utils.TruncateWithEllipsis(b.Subject, 60),
|
|
)
|
|
}
|
|
return res
|
|
}
|
|
|
|
// GetBranchTextStyle branch color
|
|
func GetBranchTextStyle(name string) style.TextStyle {
|
|
if style, ok := colorPatterns.match(name); ok {
|
|
return *style
|
|
}
|
|
|
|
return theme.DefaultTextColor
|
|
}
|
|
|
|
func (m *colorMatcher) match(name string) (*style.TextStyle, bool) {
|
|
if m.isRegex {
|
|
for pattern, style := range m.patterns {
|
|
if matched, _ := regexp.MatchString(pattern, name); matched {
|
|
return style, true
|
|
}
|
|
}
|
|
} else {
|
|
// old behavior using the deprecated branchColors behavior matching on branch type
|
|
branchType := strings.Split(name, "/")[0]
|
|
if value, ok := m.patterns[branchType]; ok {
|
|
return value, true
|
|
}
|
|
}
|
|
|
|
return nil, false
|
|
}
|
|
|
|
func BranchStatus(
|
|
branch *models.Branch,
|
|
itemOperation types.ItemOperation,
|
|
tr *i18n.TranslationSet,
|
|
now time.Time,
|
|
userConfig *config.UserConfig,
|
|
) string {
|
|
itemOperationStr := ItemOperationToString(itemOperation, tr)
|
|
if itemOperationStr != "" {
|
|
return style.FgCyan.Sprintf("%s %s", itemOperationStr, Loader(now, userConfig.Gui.Spinner))
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
func divergenceStr(
|
|
branch *models.Branch,
|
|
itemOperation types.ItemOperation,
|
|
tr *i18n.TranslationSet,
|
|
userConfig *config.UserConfig,
|
|
) string {
|
|
result := ""
|
|
if ItemOperationToString(itemOperation, tr) == "" && userConfig.Gui.ShowDivergenceFromBaseBranch != "none" {
|
|
behind := branch.BehindBaseBranch.Load()
|
|
if behind != 0 {
|
|
if userConfig.Gui.ShowDivergenceFromBaseBranch == "arrowAndNumber" {
|
|
result += fmt.Sprintf("↓%d", behind)
|
|
} else {
|
|
result += "↓"
|
|
}
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
func SetCustomBranches(customBranchColors map[string]string, isRegex bool) {
|
|
colorPatterns = &colorMatcher{
|
|
patterns: utils.SetCustomColors(customBranchColors),
|
|
isRegex: isRegex,
|
|
}
|
|
}
|
|
|
|
func WithPrColor(state string, text string, isBg bool) string {
|
|
switch state {
|
|
case "OPEN":
|
|
return color.RGB(0x43, 0x84, 0x40, isBg).Sprint(text)
|
|
case "CLOSED":
|
|
return color.RGB(0xC9, 0x45, 0x3C, isBg).Sprint(text)
|
|
case "MERGED":
|
|
return color.RGB(0x82, 0x59, 0xDD, isBg).Sprint(text)
|
|
case "DRAFT":
|
|
return color.RGB(0x67, 0x6C, 0x75, isBg).Sprint(text)
|
|
default:
|
|
return lo.Ternary(isBg, style.BgDefault, style.FgDefault).Sprint(text)
|
|
}
|
|
}
|
|
|
|
func FormatPullRequestHeader(pr *models.GithubPullRequest, tr *i18n.TranslationSet) string {
|
|
icon := lo.Ternary(icons.IsIconEnabled(), icons.IconForRemoteUrl(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 {
|
|
var icon, label string
|
|
switch state {
|
|
case "OPEN":
|
|
icon, label = " ", "Open"
|
|
case "CLOSED":
|
|
icon, label = " ", "Closed"
|
|
case "MERGED":
|
|
icon, label = " ", "Merged"
|
|
case "DRAFT":
|
|
icon, label = " ", "Draft"
|
|
default:
|
|
return ""
|
|
}
|
|
if icons.IsIconEnabled() {
|
|
return icon + label
|
|
}
|
|
return label
|
|
}
|
|
|
|
func coloredPullRequestStateText(state string) string {
|
|
if icons.IsIconEnabled() {
|
|
return fmt.Sprintf("%s%s%s",
|
|
WithPrColor(state, "", false),
|
|
WithPrColor(state, color.RGB(0xFF, 0xFF, 0xFF, false).Sprint(pullRequestStateText(state)), true),
|
|
WithPrColor(state, "", false))
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// For main branches we only want to show the PR if it's open (or draft), on the assumption that a
|
|
// closed PR for a main branch is always a mistake.
|
|
return pr.State != "CLOSED" && pr.State != "MERGED"
|
|
}
|