mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-09-12 16:46:25 -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.
315 lines
9.2 KiB
Go
315 lines
9.2 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)
|
|
}
|
|
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()
|
|
showNumber := userConfig.Gui.ShowDivergenceFromBaseBranch == "arrowAndNumber"
|
|
switch {
|
|
case behind > 0:
|
|
if showNumber {
|
|
result += fmt.Sprintf("↓%d", behind)
|
|
} else {
|
|
result += "↓"
|
|
}
|
|
case behind == models.BehindBaseAmbiguousMaybeUpToDate:
|
|
// We don't know whether the branch is up to date or behind,
|
|
// because the candidate bases disagree. "?" intentionally has
|
|
// no arrow — we'd be implying "behind" if it did.
|
|
result += "?"
|
|
case behind == models.BehindBaseAmbiguousDefinitelyBehind:
|
|
// Every candidate has the branch behind, but by different
|
|
// amounts — show that it's behind without committing to a
|
|
// specific count.
|
|
if showNumber {
|
|
result += "↓?"
|
|
} 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 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"
|
|
}
|