mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-10-03 04:26:50 -04:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
88e7f4a8e0 | ||
|
|
ce16a37be9 | ||
|
|
c34becb0b4 | ||
|
|
88f258134b | ||
|
|
b7f9b91e22 | ||
|
|
d7c9ad90a7 |
@@ -301,6 +301,18 @@ gui:
|
||||
# NerdFontsVersion is not empty.
|
||||
showFileIcons: true
|
||||
|
||||
# How the commit graph is drawn.
|
||||
# One of: 'auto' (default) | 'classic' | 'detailed'
|
||||
# 'detailed' connects the lines to the commit circles, and shows exactly where
|
||||
# branches fork off and merge. It draws the graph with the git branch drawing
|
||||
# symbols (U+F5D0 to U+F60D), so it needs a terminal that draws these itself:
|
||||
# kitty, Ghostty, WezTerm (nightly builds), Contour, or VS Code's terminal with
|
||||
# GPU acceleration. Other terminals need a font that contains them, such as
|
||||
# https://github.com/rbong/flog-symbols.
|
||||
# 'auto' uses 'detailed' if lazygit recognizes the terminal as one that draws
|
||||
# these symbols (kitty and Ghostty), and 'classic' otherwise.
|
||||
commitGraphStyle: auto
|
||||
|
||||
# Length of author name in (non-expanded) commits view. 2 means show initials
|
||||
# only.
|
||||
commitAuthorShortLength: 2
|
||||
|
||||
@@ -179,6 +179,11 @@ type GuiConfig struct {
|
||||
NerdFontsVersion string `yaml:"nerdFontsVersion" jsonschema:"enum=2,enum=3,enum="`
|
||||
// If true (default), file icons are shown in the file views. Only relevant if NerdFontsVersion is not empty.
|
||||
ShowFileIcons bool `yaml:"showFileIcons"`
|
||||
// How the commit graph is drawn.
|
||||
// One of: 'auto' (default) | 'classic' | 'detailed'
|
||||
// 'detailed' connects the lines to the commit circles, and shows exactly where branches fork off and merge. It draws the graph with the git branch drawing symbols (U+F5D0 to U+F60D), so it needs a terminal that draws these itself: kitty, Ghostty, WezTerm (nightly builds), Contour, or VS Code's terminal with GPU acceleration. Other terminals need a font that contains them, such as https://github.com/rbong/flog-symbols.
|
||||
// 'auto' uses 'detailed' if lazygit recognizes the terminal as one that draws these symbols (kitty and Ghostty), and 'classic' otherwise.
|
||||
CommitGraphStyle string `yaml:"commitGraphStyle" jsonschema:"enum=auto,enum=classic,enum=detailed"`
|
||||
// Length of author name in (non-expanded) commits view. 2 means show initials only.
|
||||
CommitAuthorShortLength int `yaml:"commitAuthorShortLength"`
|
||||
// Length of author name in expanded commits view. 2 means show initials only.
|
||||
@@ -922,6 +927,7 @@ func GetDefaultConfigForPlatform(platform string) *UserConfig {
|
||||
ShowIcons: false,
|
||||
NerdFontsVersion: "",
|
||||
ShowFileIcons: true,
|
||||
CommitGraphStyle: "auto",
|
||||
CommitAuthorShortLength: 2,
|
||||
CommitAuthorLongLength: 17,
|
||||
CommitHashLength: 8,
|
||||
|
||||
@@ -30,6 +30,10 @@ func (config *UserConfig) Validate() error {
|
||||
[]string{"mixed", "filesFirst", "foldersFirst"}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateEnum("gui.commitGraphStyle", config.Gui.CommitGraphStyle,
|
||||
[]string{"auto", "classic", "detailed"}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateEnum("git.autoForwardBranches", config.Git.AutoForwardBranches,
|
||||
[]string{"none", "onlyMainBranches", "allBranches"}); err != nil {
|
||||
return err
|
||||
|
||||
@@ -733,6 +733,12 @@ func (g *Gui) DetectedColorScheme() DetectedColorScheme {
|
||||
return g.colorScheme
|
||||
}
|
||||
|
||||
// Terminal returns the name and version that the terminal reported at
|
||||
// startup. Both are empty if it didn't report them.
|
||||
func (g *Gui) Terminal() (string, string) {
|
||||
return g.screen.Terminal()
|
||||
}
|
||||
|
||||
// SetColorSchemeChangeHandler sets a function to call on the UI thread whenever
|
||||
// the terminal's colors change after startup.
|
||||
func (g *Gui) SetColorSchemeChangeHandler(handler func(DetectedColorScheme) error) {
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/presentation/graph"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/style"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
"github.com/samber/lo"
|
||||
@@ -73,6 +74,7 @@ func NewLocalCommitsContext(c *ContextCommon) *LocalCommitsContext {
|
||||
startIdx,
|
||||
endIdx,
|
||||
shouldShowGraph(c),
|
||||
commitGraphSymbolSet(c),
|
||||
c.Model().BisectInfo,
|
||||
)
|
||||
}
|
||||
@@ -349,6 +351,18 @@ func shouldShowGraph(c *ContextCommon) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func commitGraphSymbolSet(c *ContextCommon) graph.SymbolSet {
|
||||
switch c.UserConfig().Gui.CommitGraphStyle {
|
||||
case "detailed":
|
||||
return graph.BranchDrawingSymbols
|
||||
case "auto":
|
||||
if graph.TerminalDrawsBranchDrawingSymbols(c.GocuiGui().Terminal()) {
|
||||
return graph.BranchDrawingSymbols
|
||||
}
|
||||
}
|
||||
return graph.BoxDrawingSymbols
|
||||
}
|
||||
|
||||
func searchModelCommits(caseSensitive bool, commits []*models.Commit, columnPositions []int,
|
||||
modelToViewIndex func(int) int, searchStr string,
|
||||
) []gocui.SearchPosition {
|
||||
|
||||
@@ -75,6 +75,7 @@ func NewSubCommitsContext(
|
||||
startIdx,
|
||||
endIdx,
|
||||
shouldShowGraph(c),
|
||||
commitGraphSymbolSet(c),
|
||||
git_commands.NewNullBisectInfo(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -934,6 +934,8 @@ func (gui *Gui) Run(startArgs appTypes.StartArgs) error {
|
||||
|
||||
g.ErrorHandler = gui.PopupHandler.ErrorHandler
|
||||
|
||||
terminalName, terminalVersion := g.Terminal()
|
||||
gui.c.Log.Infof("Terminal: %s %s", terminalName, terminalVersion)
|
||||
gui.c.Log.Infof("Terminal color scheme: %s", g.DetectedColorScheme())
|
||||
g.SetColorSchemeChangeHandler(func(colorScheme gocui.DetectedColorScheme) error {
|
||||
gui.c.Log.Infof("Terminal color scheme changed: %s", colorScheme)
|
||||
|
||||
@@ -59,6 +59,7 @@ func GetCommitListDisplayStrings(
|
||||
startIdx int,
|
||||
endIdx int,
|
||||
showGraph bool,
|
||||
graphSymbolSet graph.SymbolSet,
|
||||
bisectInfo *git_commands.BisectInfo,
|
||||
) [][]string {
|
||||
mutex.Lock()
|
||||
@@ -107,6 +108,7 @@ func GetCommitListDisplayStrings(
|
||||
graphPipeSets,
|
||||
graphCommits,
|
||||
selectedCommitHashPtr,
|
||||
graphSymbolSet,
|
||||
)
|
||||
allGraphLines = append(allGraphLines, graphLines...)
|
||||
}
|
||||
@@ -124,6 +126,7 @@ func GetCommitListDisplayStrings(
|
||||
graphPipeSets,
|
||||
graphCommits,
|
||||
selectedCommitHashPtr,
|
||||
graphSymbolSet,
|
||||
)
|
||||
allGraphLines = append(allGraphLines, graphLines...)
|
||||
}
|
||||
@@ -145,6 +148,7 @@ func GetCommitListDisplayStrings(
|
||||
graphPipeSets,
|
||||
graphCommits,
|
||||
selectedCommitHashPtr,
|
||||
graphSymbolSet,
|
||||
)
|
||||
getGraphLine = func(idx int) string {
|
||||
if idx >= graphOffset {
|
||||
|
||||
@@ -611,6 +611,7 @@ func TestGetCommitListDisplayStrings(t *testing.T) {
|
||||
s.startIdx,
|
||||
s.endIdx,
|
||||
s.showGraph,
|
||||
graph.BoxDrawingSymbols,
|
||||
s.bisectInfo,
|
||||
)
|
||||
|
||||
@@ -635,7 +636,7 @@ func TestGraphColorsFollowTheAuthorColors(t *testing.T) {
|
||||
models.NewCommit(hashPool, models.NewCommitOpts{Hash: "authorcolors2", AuthorName: "Jane Doe"}),
|
||||
}
|
||||
renderGraph := func() string {
|
||||
return strings.Join(graph.RenderAux(loadPipesets(commits), commits, nil), "\n")
|
||||
return strings.Join(graph.RenderAux(loadPipesets(commits), commits, nil, graph.BoxDrawingSymbols), "\n")
|
||||
}
|
||||
|
||||
authors.SetCustomAuthors(map[string]string{"Jane Doe": "red"})
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
package graph
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// The branch drawing symbols are characters in the Unicode Private Use Area
|
||||
// (U+F5D0 to U+F60D) for drawing git graphs, introduced by kitty in
|
||||
// https://github.com/kovidgoyal/kitty/pull/7681. Unlike the box drawing
|
||||
// characters, they can show how the lines in a cell connect. For example, there
|
||||
// is a symbol for a cell in which a line from above bends to the left and a
|
||||
// line from the left bends down.
|
||||
|
||||
const branchDrawingHorizontal = "\uf5d0"
|
||||
|
||||
// The lines in a cell that isn't a commit
|
||||
type lineCourses struct {
|
||||
up, down verticalLine
|
||||
horizontal bool
|
||||
}
|
||||
|
||||
var branchDrawingConnections = map[lineCourses]string{
|
||||
{}: " ",
|
||||
{horizontal: true}: branchDrawingHorizontal,
|
||||
// A horizontal line passing behind a vertical one only shows in the
|
||||
// neighbouring cells
|
||||
{up: straightLine, down: straightLine}: "\uf5d1", // │
|
||||
{up: straightLine, down: straightLine, horizontal: true}: "\uf5d1", // │
|
||||
{up: lineToLeft}: "\uf5d9", // ╯
|
||||
{up: lineToRight}: "\uf5d8", // ╰
|
||||
{down: lineToLeft}: "\uf5d7", // ╮
|
||||
{down: lineToRight}: "\uf5d6", // ╭
|
||||
{up: lineToLeft, horizontal: true}: "\uf5e3", // ╯ on ─
|
||||
{up: lineToRight, horizontal: true}: "\uf5e4", // ╰ on ─
|
||||
{down: lineToLeft, horizontal: true}: "\uf5e0", // ╮ on ─
|
||||
{down: lineToRight, horizontal: true}: "\uf5e1", // ╭ on ─
|
||||
{up: lineToLeft, down: lineToLeft}: "\uf5df", // ╯ and ╮
|
||||
{up: lineToRight, down: lineToRight}: "\uf5dc", // ╰ and ╭
|
||||
{up: lineToLeft, down: lineToLeft, horizontal: true}: "\uf5e8", // ╯ and ╮ on ─
|
||||
{up: lineToRight, down: lineToRight, horizontal: true}: "\uf5e9", // ╰ and ╭ on ─
|
||||
{up: lineToLeft, down: lineToRight, horizontal: true}: "\uf5ec", // ╯ and ╭ on ─
|
||||
{up: lineToRight, down: lineToLeft, horizontal: true}: "\uf5ed", // ╰ and ╮ on ─
|
||||
}
|
||||
|
||||
// The edges of a commit's cell that its lines touch
|
||||
type commitEdges struct {
|
||||
up, down, left, right bool
|
||||
}
|
||||
|
||||
// Commits are drawn as hollow circles, merge commits as filled ones. Both
|
||||
// connect to the lines at the given edges.
|
||||
var branchDrawingCommitSymbols = map[commitEdges]struct{ commit, merge string }{
|
||||
{}: {"\uf5ef", "\uf5ee"},
|
||||
{right: true}: {"\uf5f1", "\uf5f0"},
|
||||
{left: true}: {"\uf5f3", "\uf5f2"},
|
||||
{left: true, right: true}: {"\uf5f5", "\uf5f4"},
|
||||
{down: true}: {"\uf5f7", "\uf5f6"},
|
||||
{up: true}: {"\uf5f9", "\uf5f8"},
|
||||
{up: true, down: true}: {"\uf5fb", "\uf5fa"},
|
||||
{down: true, right: true}: {"\uf5fd", "\uf5fc"},
|
||||
{down: true, left: true}: {"\uf5ff", "\uf5fe"},
|
||||
{up: true, right: true}: {"\uf601", "\uf600"},
|
||||
{up: true, left: true}: {"\uf603", "\uf602"},
|
||||
{up: true, down: true, right: true}: {"\uf605", "\uf604"},
|
||||
{up: true, down: true, left: true}: {"\uf607", "\uf606"},
|
||||
{down: true, left: true, right: true}: {"\uf609", "\uf608"},
|
||||
{up: true, left: true, right: true}: {"\uf60b", "\uf60a"},
|
||||
{up: true, down: true, left: true, right: true}: {"\uf60d", "\uf60c"},
|
||||
}
|
||||
|
||||
func (cell *Cell) branchDrawingChars() (string, string) {
|
||||
second := " "
|
||||
if cell.right {
|
||||
second = branchDrawingHorizontal
|
||||
}
|
||||
|
||||
switch cell.cellType {
|
||||
case COMMIT, MERGE:
|
||||
symbols := branchDrawingCommitSymbols[commitEdges{
|
||||
up: cell.up != noLine,
|
||||
down: cell.down != noLine,
|
||||
left: cell.left,
|
||||
right: cell.right,
|
||||
}]
|
||||
if cell.cellType == MERGE {
|
||||
return symbols.merge, second
|
||||
}
|
||||
return symbols.commit, second
|
||||
default:
|
||||
if cell.horizontalOnTop {
|
||||
return branchDrawingHorizontal, second
|
||||
}
|
||||
if first, ok := branchDrawingConnections[lineCourses{cell.up, cell.down, cell.horizontal}]; ok {
|
||||
return first, second
|
||||
}
|
||||
// There is no symbol for lines that the layout of the graph never
|
||||
// produces, such as a lone half of a line
|
||||
first, _ := getBoxDrawingChars(cell.up != noLine, cell.down != noLine, cell.left, cell.right)
|
||||
return first, second
|
||||
}
|
||||
}
|
||||
|
||||
// The terminals that draw the branch drawing symbols themselves, with the
|
||||
// first version that draws all of them
|
||||
var terminalsWithBranchDrawingSymbols = map[string][]int{
|
||||
"kitty": {0, 36, 2},
|
||||
"ghostty": {1, 0, 0},
|
||||
}
|
||||
|
||||
// TerminalDrawsBranchDrawingSymbols says whether a terminal is known to draw
|
||||
// the branch drawing symbols itself, given the name and version it reports
|
||||
func TerminalDrawsBranchDrawingSymbols(name, version string) bool {
|
||||
minVersion, ok := terminalsWithBranchDrawingSymbols[strings.ToLower(name)]
|
||||
return ok && slices.Compare(versionNumbers(version), minVersion) >= 0
|
||||
}
|
||||
|
||||
// The numbers at the start of the dot-separated parts of a version, e.g.
|
||||
// [1 3 0] for "1.3.0-dev+abc"
|
||||
func versionNumbers(version string) []int {
|
||||
var numbers []int
|
||||
for part := range strings.SplitSeq(version, ".") {
|
||||
digits := part[:len(part)-len(strings.TrimLeft(part, "0123456789"))]
|
||||
number, err := strconv.Atoi(digits)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
numbers = append(numbers, number)
|
||||
}
|
||||
return numbers
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package graph
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestTerminalDrawsBranchDrawingSymbols(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
version string
|
||||
expected bool
|
||||
}{
|
||||
{name: "kitty", version: "0.36.1", expected: false},
|
||||
{name: "kitty", version: "0.36.2", expected: true},
|
||||
{name: "kitty", version: "0.44.0", expected: true},
|
||||
{name: "ghostty", version: "0.9.0", expected: false},
|
||||
{name: "ghostty", version: "1.0.0", expected: true},
|
||||
{name: "ghostty", version: "1.3.0-main+0123abcd", expected: true},
|
||||
{name: "WezTerm", version: "20250601-102030-89abcdef", expected: false},
|
||||
{name: "tmux", version: "3.5a", expected: false},
|
||||
{name: "iTerm2", version: "3.6.4", expected: false},
|
||||
{name: "kitty", version: "", expected: false},
|
||||
{name: "", version: "", expected: false},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name+" "+test.version, func(t *testing.T) {
|
||||
assert.Equal(t, test.expected, TerminalDrawsBranchDrawingSymbols(test.name, test.version))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,14 @@ const (
|
||||
CommitSymbol = '○'
|
||||
)
|
||||
|
||||
// The characters that the commit graph is drawn with
|
||||
type SymbolSet uint8
|
||||
|
||||
const (
|
||||
BoxDrawingSymbols SymbolSet = iota
|
||||
BranchDrawingSymbols
|
||||
)
|
||||
|
||||
type cellType int
|
||||
|
||||
const (
|
||||
@@ -21,25 +29,40 @@ const (
|
||||
MERGE
|
||||
)
|
||||
|
||||
// How a line that touches the top or bottom edge of a cell runs within it
|
||||
type verticalLine uint8
|
||||
|
||||
const (
|
||||
noLine verticalLine = iota
|
||||
// On to the opposite edge, or into the commit symbol
|
||||
straightLine
|
||||
// Bends towards the left edge
|
||||
lineToLeft
|
||||
// Bends towards the right edge
|
||||
lineToRight
|
||||
)
|
||||
|
||||
type Cell struct {
|
||||
up, down, left, right bool
|
||||
cellType cellType
|
||||
rightStyle *style.TextStyle
|
||||
style *style.TextStyle
|
||||
// The lines that touch the top and bottom edges
|
||||
up, down verticalLine
|
||||
// Whether lines touch the left and right edges
|
||||
left, right bool
|
||||
// Whether a line passes through from the left edge to the right edge
|
||||
horizontal bool
|
||||
// Whether that line is drawn over the vertical line that it crosses
|
||||
horizontalOnTop bool
|
||||
cellType cellType
|
||||
rightStyle *style.TextStyle
|
||||
style *style.TextStyle
|
||||
}
|
||||
|
||||
func (cell *Cell) render(writer io.StringWriter) {
|
||||
up, down, left, right := cell.up, cell.down, cell.left, cell.right
|
||||
|
||||
first, second := getBoxDrawingChars(up, down, left, right)
|
||||
var adjustedFirst string
|
||||
switch cell.cellType {
|
||||
case CONNECTION:
|
||||
adjustedFirst = first
|
||||
case COMMIT:
|
||||
adjustedFirst = string(CommitSymbol)
|
||||
case MERGE:
|
||||
adjustedFirst = string(MergeSymbol)
|
||||
func (cell *Cell) render(writer io.StringWriter, symbolSet SymbolSet) {
|
||||
var first, second string
|
||||
switch symbolSet {
|
||||
case BoxDrawingSymbols:
|
||||
first, second = cell.boxDrawingChars()
|
||||
case BranchDrawingSymbols:
|
||||
first, second = cell.branchDrawingChars()
|
||||
}
|
||||
|
||||
var rightStyle *style.TextStyle
|
||||
@@ -59,10 +82,22 @@ func (cell *Cell) render(writer io.StringWriter) {
|
||||
styledSecondChar = cachedSprint(*rightStyle, second)
|
||||
}
|
||||
|
||||
_, _ = writer.WriteString(cachedSprint(*cell.style, adjustedFirst))
|
||||
_, _ = writer.WriteString(cachedSprint(*cell.style, first))
|
||||
_, _ = writer.WriteString(styledSecondChar)
|
||||
}
|
||||
|
||||
func (cell *Cell) boxDrawingChars() (string, string) {
|
||||
first, second := getBoxDrawingChars(cell.up != noLine, cell.down != noLine, cell.left, cell.right)
|
||||
switch cell.cellType {
|
||||
case COMMIT:
|
||||
return string(CommitSymbol), second
|
||||
case MERGE:
|
||||
return string(MergeSymbol), second
|
||||
default:
|
||||
return first, second
|
||||
}
|
||||
}
|
||||
|
||||
type rgbCacheKey struct {
|
||||
*color.RGBStyle
|
||||
str string
|
||||
@@ -98,34 +133,35 @@ func cachedSprint(style style.TextStyle, str string) string {
|
||||
}
|
||||
|
||||
func (cell *Cell) reset() {
|
||||
cell.up = false
|
||||
cell.down = false
|
||||
cell.up = noLine
|
||||
cell.down = noLine
|
||||
cell.left = false
|
||||
cell.right = false
|
||||
cell.horizontal = false
|
||||
cell.horizontalOnTop = false
|
||||
}
|
||||
|
||||
func (cell *Cell) setUp(style *style.TextStyle) *Cell {
|
||||
cell.up = true
|
||||
func (cell *Cell) setUp(style *style.TextStyle, line verticalLine) *Cell {
|
||||
cell.up = line
|
||||
cell.style = style
|
||||
return cell
|
||||
}
|
||||
|
||||
func (cell *Cell) setDown(style *style.TextStyle) *Cell {
|
||||
cell.down = true
|
||||
func (cell *Cell) setDown(style *style.TextStyle, line verticalLine) *Cell {
|
||||
cell.down = line
|
||||
cell.style = style
|
||||
return cell
|
||||
}
|
||||
|
||||
func (cell *Cell) setLeft(style *style.TextStyle) *Cell {
|
||||
cell.left = true
|
||||
if !cell.up && !cell.down {
|
||||
if cell.up == noLine && cell.down == noLine {
|
||||
// vertical trumps left
|
||||
cell.style = style
|
||||
}
|
||||
return cell
|
||||
}
|
||||
|
||||
//nolint:unparam
|
||||
func (cell *Cell) setRight(style *style.TextStyle, override bool) *Cell {
|
||||
cell.right = true
|
||||
if cell.rightStyle == nil || override {
|
||||
@@ -134,6 +170,11 @@ func (cell *Cell) setRight(style *style.TextStyle, override bool) *Cell {
|
||||
return cell
|
||||
}
|
||||
|
||||
func (cell *Cell) setHorizontal(style *style.TextStyle, overrideRightStyle bool) *Cell {
|
||||
cell.horizontal = true
|
||||
return cell.setLeft(style).setRight(style, overrideRightStyle)
|
||||
}
|
||||
|
||||
func (cell *Cell) setStyle(style *style.TextStyle) *Cell {
|
||||
cell.style = style
|
||||
return cell
|
||||
|
||||
@@ -45,13 +45,13 @@ func (self Pipe) right() int16 {
|
||||
return max(self.fromPos, self.toPos)
|
||||
}
|
||||
|
||||
func RenderCommitGraph(commits []*models.Commit, selectedCommitHashPtr *string, getStyle func(c *models.Commit) *style.TextStyle) []string {
|
||||
func RenderCommitGraph(commits []*models.Commit, selectedCommitHashPtr *string, getStyle func(c *models.Commit) *style.TextStyle, symbolSet SymbolSet) []string {
|
||||
pipeSets := GetPipeSets(commits, getStyle)
|
||||
if len(pipeSets) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
lines := RenderAux(pipeSets, commits, selectedCommitHashPtr)
|
||||
lines := RenderAux(pipeSets, commits, selectedCommitHashPtr, symbolSet)
|
||||
|
||||
return lines
|
||||
}
|
||||
@@ -69,7 +69,7 @@ func GetPipeSets(commits []*models.Commit, getStyle func(c *models.Commit) *styl
|
||||
})
|
||||
}
|
||||
|
||||
func RenderAux(pipeSets [][]Pipe, commits []*models.Commit, selectedCommitHashPtr *string) []string {
|
||||
func RenderAux(pipeSets [][]Pipe, commits []*models.Commit, selectedCommitHashPtr *string, symbolSet SymbolSet) []string {
|
||||
maxProcs := runtime.GOMAXPROCS(0)
|
||||
|
||||
// splitting up the rendering of the graph into multiple goroutines allows us to render the graph in parallel
|
||||
@@ -93,7 +93,7 @@ func RenderAux(pipeSets [][]Pipe, commits []*models.Commit, selectedCommitHashPt
|
||||
if k > 0 {
|
||||
prevCommit = commits[k-1]
|
||||
}
|
||||
line := renderPipeSet(pipeSet, selectedCommitHashPtr, prevCommit)
|
||||
line := renderPipeSet(pipeSet, selectedCommitHashPtr, prevCommit, symbolSet)
|
||||
innerLines = append(innerLines, line)
|
||||
}
|
||||
chunks[i] = innerLines
|
||||
@@ -107,19 +107,18 @@ func RenderAux(pipeSets [][]Pipe, commits []*models.Commit, selectedCommitHashPt
|
||||
}
|
||||
|
||||
func getNextPipes(prevPipes []Pipe, commit *models.Commit, getStyle func(c *models.Commit) *style.TextStyle) []Pipe {
|
||||
maxPos := int16(0)
|
||||
for _, pipe := range prevPipes {
|
||||
if pipe.toPos > maxPos {
|
||||
maxPos = pipe.toPos
|
||||
}
|
||||
}
|
||||
|
||||
// a pipe that terminated in the previous line has no bearing on the current line
|
||||
// so we'll filter those out
|
||||
// so we'll filter those out. The same goes for the pipe from a root commit to
|
||||
// the empty tree, because no commit comes after a root commit.
|
||||
currentPipes := lo.Filter(prevPipes, func(pipe Pipe, _ int) bool {
|
||||
return pipe.kind != TERMINATES
|
||||
return pipe.kind != TERMINATES && !equalHashes(pipe.toHash, &EmptyTreeCommitHash)
|
||||
})
|
||||
|
||||
maxPos := int16(-1)
|
||||
for _, pipe := range currentPipes {
|
||||
maxPos = max(maxPos, pipe.toPos)
|
||||
}
|
||||
|
||||
newPipes := make([]Pipe, 0, len(currentPipes)+len(commit.ParentPtrs()))
|
||||
// start by assuming that we've got a brand new commit not related to any preceding commit.
|
||||
// (this only happens when we're doing `git log --all`). These will be tacked onto the far end.
|
||||
@@ -276,6 +275,7 @@ func renderPipeSet(
|
||||
pipes []Pipe,
|
||||
selectedCommitHashPtr *string,
|
||||
prevCommit *models.Commit,
|
||||
symbolSet SymbolSet,
|
||||
) string {
|
||||
maxPos := int16(0)
|
||||
commitPos := int16(0)
|
||||
@@ -304,17 +304,17 @@ func renderPipeSet(
|
||||
|
||||
if left != right {
|
||||
for i := left + 1; i < right; i++ {
|
||||
cells[i].setLeft(style).setRight(style, overrideRightStyle)
|
||||
cells[i].setHorizontal(style, overrideRightStyle)
|
||||
}
|
||||
cells[left].setRight(style, overrideRightStyle)
|
||||
cells[right].setLeft(style)
|
||||
}
|
||||
|
||||
if pipe.kind == STARTS || pipe.kind == CONTINUES {
|
||||
cells[pipe.toPos].setDown(style)
|
||||
cells[pipe.toPos].setDown(style, lineTowards(pipe.toPos, pipe.fromPos))
|
||||
}
|
||||
if pipe.kind == TERMINATES || pipe.kind == CONTINUES {
|
||||
cells[pipe.fromPos].setUp(style)
|
||||
cells[pipe.fromPos].setUp(style, lineTowards(pipe.fromPos, pipe.toPos))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -343,14 +343,31 @@ func renderPipeSet(
|
||||
}
|
||||
|
||||
for _, pipe := range nonSelectedPipes {
|
||||
if pipe.kind != STARTS && !(pipe.kind == TERMINATES && pipe.fromPos == commitPos && pipe.toPos == commitPos) {
|
||||
renderPipe(&pipe, pipe.style, false)
|
||||
if pipe.kind == STARTS {
|
||||
continue
|
||||
}
|
||||
if pipe.kind == TERMINATES && pipe.fromPos == commitPos && pipe.toPos == commitPos {
|
||||
// Record this line without drawing it, so that the commit symbol
|
||||
// keeps the style of the commit itself. The branch drawing commit
|
||||
// symbols connect to it. The pipe from above the first commit
|
||||
// doesn't come from a commit, so it has no line.
|
||||
if !equalHashes(pipe.fromHash, &StartCommitHash) {
|
||||
cells[commitPos].up = straightLine
|
||||
}
|
||||
continue
|
||||
}
|
||||
renderPipe(&pipe, pipe.style, false)
|
||||
}
|
||||
|
||||
for _, pipe := range selectedPipes {
|
||||
for i := pipe.left(); i <= pipe.right(); i++ {
|
||||
cells[i].reset()
|
||||
// Box drawing characters can't show every combination of lines in a cell,
|
||||
// so the cells that the selected commit's lines run through show only
|
||||
// those lines. The branch drawing symbols can, so they keep the other
|
||||
// lines too.
|
||||
if symbolSet == BoxDrawingSymbols {
|
||||
for _, pipe := range selectedPipes {
|
||||
for i := pipe.left(); i <= pipe.right(); i++ {
|
||||
cells[i].reset()
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, pipe := range selectedPipes {
|
||||
@@ -360,6 +377,30 @@ func renderPipeSet(
|
||||
}
|
||||
}
|
||||
|
||||
// The pipe from a root commit to the empty tree gives the commit symbol the
|
||||
// style of the commit, but there is no line below a root commit
|
||||
for _, pipe := range pipes {
|
||||
if pipe.kind == STARTS && equalHashes(pipe.toHash, &EmptyTreeCommitHash) {
|
||||
cells[pipe.toPos].down = noLine
|
||||
}
|
||||
}
|
||||
|
||||
// Where a line of the selected commit crosses the vertical line of another
|
||||
// commit, draw it over that line, so that it reads as one line. The
|
||||
// vertical line visibly continues above and below. A vertical line in a
|
||||
// cell that doesn't have the highlight style belongs to another commit.
|
||||
if symbolSet == BranchDrawingSymbols {
|
||||
for _, pipe := range selectedPipes {
|
||||
for i := pipe.left() + 1; i < pipe.right(); i++ {
|
||||
cell := cells[i]
|
||||
if cell.up == straightLine && cell.down == straightLine && cell.style != &highlightStyle {
|
||||
cell.horizontalOnTop = true
|
||||
cell.style = &highlightStyle
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cType := COMMIT
|
||||
if isMerge {
|
||||
cType = MERGE
|
||||
@@ -371,11 +412,24 @@ func renderPipeSet(
|
||||
writer := &strings.Builder{}
|
||||
writer.Grow(len(cells) * 2)
|
||||
for _, cell := range cells {
|
||||
cell.render(writer)
|
||||
cell.render(writer, symbolSet)
|
||||
}
|
||||
return writer.String()
|
||||
}
|
||||
|
||||
// The course of a line through the top or bottom edge of the cell at pos,
|
||||
// given the column of the pipe's other end
|
||||
func lineTowards(pos, otherPos int16) verticalLine {
|
||||
switch {
|
||||
case otherPos < pos:
|
||||
return lineToLeft
|
||||
case otherPos > pos:
|
||||
return lineToRight
|
||||
default:
|
||||
return straightLine
|
||||
}
|
||||
}
|
||||
|
||||
func equalHashes(a, b *string) bool {
|
||||
// if our selectedCommitHashPtr is nil, there is no selected commit
|
||||
if a == nil || b == nil {
|
||||
|
||||
@@ -94,6 +94,34 @@ func TestRenderCommitGraph(t *testing.T) {
|
||||
5 ○───╯ │
|
||||
6 ○ ╭───╯`,
|
||||
},
|
||||
{
|
||||
name: "with a root commit followed by an unrelated history",
|
||||
commitOpts: []models.NewCommitOpts{
|
||||
{Hash: "1", Parents: []string{"2"}},
|
||||
{Hash: "2"},
|
||||
{Hash: "A", Parents: []string{"B"}},
|
||||
{Hash: "B"},
|
||||
},
|
||||
expectedOutput: `
|
||||
1 ○
|
||||
2 ○
|
||||
A ○
|
||||
B ○`,
|
||||
},
|
||||
{
|
||||
name: "with a merge of an unrelated history",
|
||||
commitOpts: []models.NewCommitOpts{
|
||||
{Hash: "1", Parents: []string{"2", "A"}},
|
||||
{Hash: "2", Parents: []string{"3"}},
|
||||
{Hash: "A"},
|
||||
{Hash: "3"},
|
||||
},
|
||||
expectedOutput: `
|
||||
1 ◎─╮
|
||||
2 ○ │
|
||||
A │ ○
|
||||
3 ○`,
|
||||
},
|
||||
{
|
||||
name: "with a path that has room to move to the left and continues",
|
||||
commitOpts: []models.NewCommitOpts{
|
||||
@@ -224,7 +252,7 @@ func TestRenderCommitGraph(t *testing.T) {
|
||||
getStyle := func(c *models.Commit) *style.TextStyle { return &style.FgDefault }
|
||||
commits := lo.Map(test.commitOpts,
|
||||
func(opts models.NewCommitOpts, _ int) *models.Commit { return models.NewCommit(hashPool, opts) })
|
||||
lines := RenderCommitGraph(commits, hashPool.Add("blah"), getStyle)
|
||||
lines := RenderCommitGraph(commits, hashPool.Add("blah"), getStyle, BoxDrawingSymbols)
|
||||
|
||||
trimmedExpectedOutput := ""
|
||||
for line := range strings.SplitSeq(strings.TrimPrefix(test.expectedOutput, "\n"), "\n") {
|
||||
@@ -247,6 +275,345 @@ func TestRenderCommitGraph(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Box drawing look-alikes for the branch drawing symbols, so that the expected
|
||||
// output in tests is readable. The box drawing characters of the graph never
|
||||
// use ┤ and ┼; here they stand for the symbols that have two bends. Commit
|
||||
// symbols are shown as ○ and ◎, whichever lines they connect to.
|
||||
var branchDrawingLookAlikes = func() map[rune]rune {
|
||||
lookAlikes := map[rune]rune{
|
||||
'\uf5d0': '─',
|
||||
'\uf5d1': '│',
|
||||
'\uf5d6': '╭',
|
||||
'\uf5d7': '╮',
|
||||
'\uf5d8': '╰',
|
||||
'\uf5d9': '╯',
|
||||
'\uf5e0': '┬', // ╮ on ─
|
||||
'\uf5e3': '┴', // ╯ on ─
|
||||
'\uf5df': '┤', // ╯ and ╮
|
||||
'\uf5e8': '┼', // ╯ and ╮ on ─
|
||||
}
|
||||
for _, symbols := range branchDrawingCommitSymbols {
|
||||
lookAlikes[[]rune(symbols.commit)[0]] = CommitSymbol
|
||||
lookAlikes[[]rune(symbols.merge)[0]] = MergeSymbol
|
||||
}
|
||||
return lookAlikes
|
||||
}()
|
||||
|
||||
func TestRenderCommitGraphWithBranchDrawingSymbols(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
commitOpts []models.NewCommitOpts
|
||||
selectedHash string
|
||||
expectedOutput string
|
||||
}{
|
||||
{
|
||||
name: "branch forked off a merge commit",
|
||||
commitOpts: []models.NewCommitOpts{
|
||||
{Hash: "1", Parents: []string{"2", "3"}},
|
||||
{Hash: "3", Parents: []string{"5"}},
|
||||
{Hash: "2", Parents: []string{"5", "4"}},
|
||||
{Hash: "4", Parents: []string{"5"}},
|
||||
{Hash: "5", Parents: []string{"6", "7"}},
|
||||
{Hash: "7", Parents: []string{"6"}},
|
||||
{Hash: "6", Parents: []string{"8"}},
|
||||
},
|
||||
expectedOutput: `
|
||||
1 ◎─╮
|
||||
3 │ ○
|
||||
2 ◎─│─╮
|
||||
4 │ │ ○
|
||||
5 ◎─┼─╯
|
||||
7 │ ○
|
||||
6 ○─╯`,
|
||||
},
|
||||
{
|
||||
name: "branch forked off a merge commit, with the merge commit selected",
|
||||
commitOpts: []models.NewCommitOpts{
|
||||
{Hash: "1", Parents: []string{"2", "3"}},
|
||||
{Hash: "3", Parents: []string{"5"}},
|
||||
{Hash: "2", Parents: []string{"5", "4"}},
|
||||
{Hash: "4", Parents: []string{"5"}},
|
||||
{Hash: "5", Parents: []string{"6", "7"}},
|
||||
{Hash: "7", Parents: []string{"6"}},
|
||||
{Hash: "6", Parents: []string{"8"}},
|
||||
},
|
||||
selectedHash: "5",
|
||||
expectedOutput: `
|
||||
1 ◎─╮
|
||||
3 │ ○
|
||||
2 ◎─│─╮
|
||||
4 │ │ ○
|
||||
5 ◎─┼─╯
|
||||
7 │ ○
|
||||
6 ○─╯`,
|
||||
},
|
||||
{
|
||||
name: "branch forked off a merge commit, with the merge commit above it selected",
|
||||
commitOpts: []models.NewCommitOpts{
|
||||
{Hash: "1", Parents: []string{"2", "3"}},
|
||||
{Hash: "3", Parents: []string{"5"}},
|
||||
{Hash: "2", Parents: []string{"5", "4"}},
|
||||
{Hash: "4", Parents: []string{"5"}},
|
||||
{Hash: "5", Parents: []string{"6", "7"}},
|
||||
{Hash: "7", Parents: []string{"6"}},
|
||||
{Hash: "6", Parents: []string{"8"}},
|
||||
},
|
||||
selectedHash: "2",
|
||||
expectedOutput: `
|
||||
1 ◎─╮
|
||||
3 │ ○
|
||||
2 ◎───╮
|
||||
4 │ │ ○
|
||||
5 ◎─┼─╯
|
||||
7 │ ○
|
||||
6 ○─╯`,
|
||||
},
|
||||
{
|
||||
name: "branch forked off a merge commit, with no line passing through",
|
||||
commitOpts: []models.NewCommitOpts{
|
||||
{Hash: "1", Parents: []string{"2", "3"}},
|
||||
{Hash: "3", Parents: []string{"2"}},
|
||||
{Hash: "2", Parents: []string{"4", "5"}},
|
||||
{Hash: "4", Parents: []string{"6", "7"}},
|
||||
{Hash: "6", Parents: []string{"8"}},
|
||||
},
|
||||
expectedOutput: `
|
||||
1 ◎─╮
|
||||
3 │ ○
|
||||
2 ◎─┤
|
||||
4 ◎─│─╮
|
||||
6 ○ │ │`,
|
||||
},
|
||||
{
|
||||
name: "several lines ending in a commit",
|
||||
commitOpts: []models.NewCommitOpts{
|
||||
{Hash: "1", Parents: []string{"2"}},
|
||||
{Hash: "2", Parents: []string{"3", "4"}},
|
||||
{Hash: "3", Parents: []string{"5", "4"}},
|
||||
{Hash: "5", Parents: []string{"7", "8"}},
|
||||
{Hash: "7", Parents: []string{"4", "A"}},
|
||||
{Hash: "4", Parents: []string{"B"}},
|
||||
{Hash: "B", Parents: []string{"C"}},
|
||||
},
|
||||
expectedOutput: `
|
||||
1 ○
|
||||
2 ◎─╮
|
||||
3 ◎─│─╮
|
||||
5 ◎─│─│─╮
|
||||
7 ◎─│─│─│─╮
|
||||
4 ○─┴─╯ │ │
|
||||
B ○ ╭───╯ │`,
|
||||
},
|
||||
}
|
||||
|
||||
oldColorLevel := color.ForceSetColorLevel(terminfo.ColorLevelMillions)
|
||||
defer color.ForceSetColorLevel(oldColorLevel)
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
hashPool := &utils.StringPool{}
|
||||
|
||||
getStyle := func(c *models.Commit) *style.TextStyle { return &style.FgDefault }
|
||||
commits := lo.Map(test.commitOpts,
|
||||
func(opts models.NewCommitOpts, _ int) *models.Commit { return models.NewCommit(hashPool, opts) })
|
||||
lines := RenderCommitGraph(commits, hashPool.Add(test.selectedHash), getStyle, BranchDrawingSymbols)
|
||||
|
||||
trimmedExpectedOutput := ""
|
||||
for line := range strings.SplitSeq(strings.TrimPrefix(test.expectedOutput, "\n"), "\n") {
|
||||
trimmedExpectedOutput += strings.TrimSpace(line) + "\n"
|
||||
}
|
||||
|
||||
output := ""
|
||||
for i, line := range lines {
|
||||
lookAlikes := strings.Map(func(r rune) rune {
|
||||
return lo.ValueOr(branchDrawingLookAlikes, r, r)
|
||||
}, utils.Decolorise(line))
|
||||
output += strings.TrimSpace(test.commitOpts[i].Hash+" "+lookAlikes) + "\n"
|
||||
}
|
||||
|
||||
assert.Equal(t, trimmedExpectedOutput, output)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderPipeSetWithBranchDrawingSymbols(t *testing.T) {
|
||||
cyan := style.FgCyan
|
||||
red := style.FgRed
|
||||
green := style.FgGreen
|
||||
yellow := style.FgYellow
|
||||
magenta := style.FgMagenta
|
||||
nothing := style.Nothing
|
||||
|
||||
hashPool := &utils.StringPool{}
|
||||
pool := func(s string) *string { return hashPool.Add(s) }
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
pipes []Pipe
|
||||
prevCommit *models.Commit
|
||||
expectedStr string
|
||||
expectedStyles []style.TextStyle
|
||||
}{
|
||||
{
|
||||
name: "commit with lines above and below",
|
||||
pipes: []Pipe{
|
||||
{fromPos: 0, toPos: 0, fromHash: pool("a"), toHash: pool("b"), kind: TERMINATES, style: &cyan},
|
||||
{fromPos: 0, toPos: 0, fromHash: pool("b"), toHash: pool("c"), kind: STARTS, style: &green},
|
||||
},
|
||||
prevCommit: models.NewCommit(hashPool, models.NewCommitOpts{Hash: "a"}),
|
||||
expectedStr: "\uf5fb", // ○ with lines up and down
|
||||
expectedStyles: []style.TextStyle{green},
|
||||
},
|
||||
{
|
||||
name: "first commit",
|
||||
pipes: []Pipe{
|
||||
{fromPos: 0, toPos: 0, fromHash: &StartCommitHash, toHash: pool("b"), kind: TERMINATES, style: &cyan},
|
||||
{fromPos: 0, toPos: 0, fromHash: pool("b"), toHash: pool("c"), kind: STARTS, style: &green},
|
||||
},
|
||||
expectedStr: "\uf5f7", // ○ with a line down
|
||||
expectedStyles: []style.TextStyle{green},
|
||||
},
|
||||
{
|
||||
name: "root commit",
|
||||
pipes: []Pipe{
|
||||
{fromPos: 0, toPos: 0, fromHash: pool("a"), toHash: pool("b"), kind: TERMINATES, style: &cyan},
|
||||
{fromPos: 0, toPos: 0, fromHash: pool("b"), toHash: &EmptyTreeCommitHash, kind: STARTS, style: &green},
|
||||
},
|
||||
prevCommit: models.NewCommit(hashPool, models.NewCommitOpts{Hash: "a"}),
|
||||
expectedStr: "\uf5f9", // ○ with a line up
|
||||
expectedStyles: []style.TextStyle{green},
|
||||
},
|
||||
{
|
||||
name: "selected root commit",
|
||||
pipes: []Pipe{
|
||||
{fromPos: 0, toPos: 0, fromHash: pool("a"), toHash: pool("selected"), kind: TERMINATES, style: &cyan},
|
||||
{fromPos: 0, toPos: 0, fromHash: pool("selected"), toHash: &EmptyTreeCommitHash, kind: STARTS, style: &green},
|
||||
},
|
||||
prevCommit: models.NewCommit(hashPool, models.NewCommitOpts{Hash: "a"}),
|
||||
expectedStr: "\uf5f9", // ○ with a line up
|
||||
expectedStyles: []style.TextStyle{highlightStyle},
|
||||
},
|
||||
{
|
||||
name: "selected commit",
|
||||
pipes: []Pipe{
|
||||
{fromPos: 0, toPos: 0, fromHash: pool("a"), toHash: pool("selected"), kind: TERMINATES, style: &cyan},
|
||||
{fromPos: 0, toPos: 0, fromHash: pool("selected"), toHash: pool("c"), kind: STARTS, style: &green},
|
||||
},
|
||||
prevCommit: models.NewCommit(hashPool, models.NewCommitOpts{Hash: "a"}),
|
||||
expectedStr: "\uf5fb", // ○ with lines up and down
|
||||
expectedStyles: []style.TextStyle{highlightStyle},
|
||||
},
|
||||
{
|
||||
name: "commit whose previous commit is selected and is a merge commit",
|
||||
pipes: []Pipe{
|
||||
{fromPos: 0, toPos: 0, fromHash: pool("selected"), toHash: pool("a2"), kind: TERMINATES, style: &red},
|
||||
{fromPos: 0, toPos: 0, fromHash: pool("a2"), toHash: pool("a3"), kind: STARTS, style: &green},
|
||||
{fromPos: 1, toPos: 1, fromHash: pool("selected"), toHash: pool("b3"), kind: CONTINUES, style: &red},
|
||||
},
|
||||
prevCommit: models.NewCommit(hashPool, models.NewCommitOpts{Hash: "selected"}),
|
||||
expectedStr: "\uf5fb \uf5d1", // ○ with lines up and down, │
|
||||
expectedStyles: []style.TextStyle{
|
||||
highlightStyle, nothing, highlightStyle,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "merge commit that a branch forks off",
|
||||
pipes: []Pipe{
|
||||
{fromPos: 0, toPos: 0, fromHash: pool("a1"), toHash: pool("a2"), kind: TERMINATES, style: &red},
|
||||
{fromPos: 1, toPos: 0, fromHash: pool("b1"), toHash: pool("a2"), kind: TERMINATES, style: &magenta},
|
||||
{fromPos: 2, toPos: 0, fromHash: pool("c1"), toHash: pool("a2"), kind: TERMINATES, style: &cyan},
|
||||
{fromPos: 0, toPos: 0, fromHash: pool("a2"), toHash: pool("a3"), kind: STARTS, style: &green},
|
||||
{fromPos: 0, toPos: 1, fromHash: pool("a2"), toHash: pool("b3"), kind: STARTS, style: &green},
|
||||
},
|
||||
prevCommit: models.NewCommit(hashPool, models.NewCommitOpts{Hash: "a1"}),
|
||||
expectedStr: "\uf604\uf5d0\uf5e8\uf5d0\uf5d9", // ◎ with lines up, down and right, ─, ╯ and ╮ on ─, ─, ╯
|
||||
expectedStyles: []style.TextStyle{
|
||||
green, green, magenta, cyan, cyan,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "merge commit that a branch forks off, with the branch's first commit selected",
|
||||
pipes: []Pipe{
|
||||
{fromPos: 0, toPos: 0, fromHash: pool("a1"), toHash: pool("a2"), kind: TERMINATES, style: &red},
|
||||
{fromPos: 1, toPos: 0, fromHash: pool("selected"), toHash: pool("a2"), kind: TERMINATES, style: &magenta},
|
||||
{fromPos: 2, toPos: 0, fromHash: pool("c1"), toHash: pool("a2"), kind: TERMINATES, style: &cyan},
|
||||
{fromPos: 0, toPos: 0, fromHash: pool("a2"), toHash: pool("a3"), kind: STARTS, style: &green},
|
||||
{fromPos: 0, toPos: 1, fromHash: pool("a2"), toHash: pool("b3"), kind: STARTS, style: &green},
|
||||
},
|
||||
prevCommit: models.NewCommit(hashPool, models.NewCommitOpts{Hash: "a1"}),
|
||||
expectedStr: "\uf604\uf5d0\uf5e8\uf5d0\uf5d9", // ◎ with lines up, down and right, ─, ╯ and ╮ on ─, ─, ╯
|
||||
expectedStyles: []style.TextStyle{
|
||||
highlightStyle, highlightStyle, highlightStyle, cyan, cyan,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "line of the selected commit crossing another line",
|
||||
pipes: []Pipe{
|
||||
{fromPos: 0, toPos: 0, fromHash: pool("a1"), toHash: pool("selected"), kind: TERMINATES, style: &red},
|
||||
{fromPos: 0, toPos: 0, fromHash: pool("selected"), toHash: pool("a3"), kind: STARTS, style: &yellow},
|
||||
{fromPos: 1, toPos: 1, fromHash: pool("b1"), toHash: pool("b2"), kind: CONTINUES, style: &magenta},
|
||||
{fromPos: 3, toPos: 0, fromHash: pool("e1"), toHash: pool("selected"), kind: TERMINATES, style: &green},
|
||||
{fromPos: 0, toPos: 2, fromHash: pool("selected"), toHash: pool("c3"), kind: STARTS, style: &yellow},
|
||||
},
|
||||
prevCommit: models.NewCommit(hashPool, models.NewCommitOpts{Hash: "a1"}),
|
||||
expectedStr: "\uf604\uf5d0\uf5d0\uf5d0\uf5e0\uf5d0\uf5d9", // ◎ with lines up, down and right, ─, ─, ─, ╮ on ─, ─, ╯
|
||||
expectedStyles: []style.TextStyle{
|
||||
highlightStyle, highlightStyle, highlightStyle, highlightStyle, highlightStyle, green, green,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "line of the selected commit passing through a cell in which other lines bend",
|
||||
pipes: []Pipe{
|
||||
{fromPos: 0, toPos: 0, fromHash: pool("a1"), toHash: pool("a2"), kind: TERMINATES, style: &red},
|
||||
{fromPos: 1, toPos: 0, fromHash: pool("b1"), toHash: pool("a2"), kind: TERMINATES, style: &magenta},
|
||||
{fromPos: 2, toPos: 0, fromHash: pool("selected"), toHash: pool("a2"), kind: TERMINATES, style: &cyan},
|
||||
{fromPos: 0, toPos: 0, fromHash: pool("a2"), toHash: pool("a3"), kind: STARTS, style: &green},
|
||||
{fromPos: 0, toPos: 1, fromHash: pool("a2"), toHash: pool("b3"), kind: STARTS, style: &green},
|
||||
},
|
||||
prevCommit: models.NewCommit(hashPool, models.NewCommitOpts{Hash: "a1"}),
|
||||
expectedStr: "\uf604\uf5d0\uf5e8\uf5d0\uf5d9", // ◎ with lines up, down and right, ─, ╯ and ╮ on ─, ─, ╯
|
||||
expectedStyles: []style.TextStyle{
|
||||
highlightStyle, highlightStyle, magenta, highlightStyle, highlightStyle,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
oldColorLevel := color.ForceSetColorLevel(terminfo.ColorLevelMillions)
|
||||
defer color.ForceSetColorLevel(oldColorLevel)
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
actualStr := renderPipeSet(test.pipes, pool("selected"), test.prevCommit, BranchDrawingSymbols)
|
||||
if len([]rune(test.expectedStr)) != len(test.expectedStyles) {
|
||||
t.Fatalf("Error in test setup: you have %d characters in the expected output but have specified %d styles", len([]rune(test.expectedStr)), len(test.expectedStyles))
|
||||
}
|
||||
expectedStr := ""
|
||||
for i, char := range []rune(test.expectedStr) {
|
||||
expectedStr += test.expectedStyles[i].Sprint(string(char))
|
||||
}
|
||||
expectedStr += " "
|
||||
|
||||
assert.Equal(t, expectedStr, actualStr)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBranchDrawingSymbolsCoverAllCells(t *testing.T) {
|
||||
hashPool := &utils.StringPool{}
|
||||
commits := generateCommits(hashPool, 1000)
|
||||
getStyle := func(commit *models.Commit) *style.TextStyle { return &style.FgDefault }
|
||||
isBoxDrawingChar := func(r rune) bool { return r >= '\u2500' && r <= '\u257f' }
|
||||
|
||||
for _, selectedHash := range []string{"none", commits[10].Hash(), commits[500].Hash()} {
|
||||
lines := RenderCommitGraph(commits, hashPool.Add(selectedHash), getStyle, BranchDrawingSymbols)
|
||||
for i, line := range lines {
|
||||
line = utils.Decolorise(line)
|
||||
assert.False(t, strings.ContainsFunc(line, isBoxDrawingChar),
|
||||
"row %d falls back to box drawing characters: %q", i, line)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderPipeSet(t *testing.T) {
|
||||
cyan := style.FgCyan
|
||||
red := style.FgRed
|
||||
@@ -462,7 +829,7 @@ func TestRenderPipeSet(t *testing.T) {
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
actualStr := renderPipeSet(test.pipes, pool("selected"), test.prevCommit)
|
||||
actualStr := renderPipeSet(test.pipes, pool("selected"), test.prevCommit, BoxDrawingSymbols)
|
||||
t.Log("actual cells:")
|
||||
t.Log(actualStr)
|
||||
expectedStr := ""
|
||||
@@ -528,7 +895,7 @@ func TestGetNextPipes(t *testing.T) {
|
||||
Parents: []string{},
|
||||
}),
|
||||
expected: []Pipe{
|
||||
{fromPos: 1, toPos: 1, fromHash: pool("root"), toHash: pool(models.EmptyTreeCommitHash), kind: STARTS, style: &style.FgDefault},
|
||||
{fromPos: 0, toPos: 0, fromHash: pool("root"), toHash: pool(models.EmptyTreeCommitHash), kind: STARTS, style: &style.FgDefault},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -540,8 +907,8 @@ func TestGetNextPipes(t *testing.T) {
|
||||
getStyle := func(c *models.Commit) *style.TextStyle { return &style.FgDefault }
|
||||
pipes := getNextPipes(test.prevPipes, test.commit, getStyle)
|
||||
// rendering cells so that it's easier to see what went wrong
|
||||
actualStr := renderPipeSet(pipes, pool("selected"), nil)
|
||||
expectedStr := renderPipeSet(test.expected, pool("selected"), nil)
|
||||
actualStr := renderPipeSet(pipes, pool("selected"), nil, BoxDrawingSymbols)
|
||||
expectedStr := renderPipeSet(test.expected, pool("selected"), nil, BoxDrawingSymbols)
|
||||
t.Log("expected cells:")
|
||||
t.Log(expectedStr)
|
||||
t.Log("actual cells:")
|
||||
@@ -562,7 +929,7 @@ func BenchmarkRenderCommitGraph(b *testing.B) {
|
||||
}
|
||||
b.ResetTimer()
|
||||
for b.Loop() {
|
||||
RenderCommitGraph(commits, hashPool.Add("selected"), getStyle)
|
||||
RenderCommitGraph(commits, hashPool.Add("selected"), getStyle, BoxDrawingSymbols)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1221,6 +1221,16 @@
|
||||
"description": "If true (default), file icons are shown in the file views. Only relevant if NerdFontsVersion is not empty.",
|
||||
"default": true
|
||||
},
|
||||
"commitGraphStyle": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"auto",
|
||||
"classic",
|
||||
"detailed"
|
||||
],
|
||||
"description": "How the commit graph is drawn.\nOne of: 'auto' (default) | 'classic' | 'detailed'\n'detailed' connects the lines to the commit circles, and shows exactly where branches fork off and merge. It draws the graph with the git branch drawing symbols (U+F5D0 to U+F60D), so it needs a terminal that draws these itself: kitty, Ghostty, WezTerm (nightly builds), Contour, or VS Code's terminal with GPU acceleration. Other terminals need a font that contains them, such as https://github.com/rbong/flog-symbols.\n'auto' uses 'detailed' if lazygit recognizes the terminal as one that draws these symbols (kitty and Ghostty), and 'classic' otherwise.",
|
||||
"default": "auto"
|
||||
},
|
||||
"commitAuthorShortLength": {
|
||||
"type": "integer",
|
||||
"description": "Length of author name in (non-expanded) commits view. 2 means show initials only.",
|
||||
|
||||
@@ -12,6 +12,9 @@ gui:
|
||||
- black
|
||||
# Not important in tests but it creates clutter in demos
|
||||
showRandomTip: false
|
||||
# The expected output of the tests draws the commit graph with box drawing
|
||||
# characters, whichever terminal a test runs in
|
||||
commitGraphStyle: classic
|
||||
animateExplosion: false # takes too long
|
||||
git:
|
||||
# We don't want to run any periodic background git commands because it'll introduce race conditions and flakiness.
|
||||
|
||||
Reference in New Issue
Block a user