Compare commits

...
Author SHA1 Message Date
Stefan HallerandClaude Opus 5.5 88e7f4a8e0 Use the detailed commit graph automatically in terminals that draw it
The detailed commit graph is only drawn for users who set
gui.commitGraphStyle to 'detailed'. Many users of terminals that can
draw it will never find out about it. A terminal can't be asked whether
it draws a given character, but it does tell us its name and version
when tcell asks for them with XTVERSION at startup.

Add an 'auto' value and make it the default. It uses the detailed graph
in kitty from 0.36.2 and in Ghostty from 1.0.0 on. These are the first
versions that draw all of the symbols. Everywhere else it stays with
the classic graph. This includes tmux, because tmux answers XTVERSION
itself. So far, WezTerm draws the symbols only in its nightly builds, so
it isn't detected until a release has them. VS Code isn't detected
either, because it only draws the symbols with GPU acceleration.

The expected output of the integration tests has the classic graph, so
pin it in their config. Otherwise they would fail when run with a
visible UI in one of these terminals.

Log the terminal's name and version at startup, to make it possible to
find out why 'auto' picked what it did.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 14:21:07 +02:00
Stefan HallerandClaude Opus 5.5 ce16a37be9 Add an option to draw the commit graph with branch drawing symbols
If a branch forks off a merge commit, the graph can't show both lines
in the merge commit's row. The line from the branch's first commit ends
in the same cell in which the merge's line to its second parent starts.
The box drawing characters have no symbol for a line from above that
bends to the left combined with a line from the left that bends down,
so one of the two gets lost. Today the cell shows a plain vertical
line, and the branch looks as if it sat on the merge's second parent
(#5497).

The branch drawing symbols in the Unicode Private Use Area (U+F5D0 to
U+F60D) have a symbol for every way in which lines meet in a cell.
kitty introduced them for git graph viewers like vim-flog, and Ghostty,
Contour, the terminal of VS Code and nightly builds of WezTerm render
them too. Other terminals need a font that contains them.

Add the gui.commitGraphStyle config. Its 'detailed' style draws the
graph with these symbols, and the commit symbols of the set connect to
their lines. Commits are drawn as hollow circles and merge commits as
filled ones. The default stays the 'classic' style with the box drawing
characters, because not every terminal can display the others.

A cell has only one colour. Where two lines meet, the symbol takes the
colour of the line from above. With the box drawing characters,
highlighting the lines of the selected commit clears the cells they run
through. The branch drawing symbols keep the other lines. A cell in
which a highlighted line bends is highlighted as a whole. Where a
highlighted line crosses the vertical line of another commit, it is
drawn over that line, so that it reads as one line. In a cell in which
another line bends, the symbol keeps the colour of that line.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 14:21:07 +02:00
Stefan HallerandClaude Opus 5.5 c34becb0b4 Extract the choice of a graph cell's box drawing characters
This separates choosing the characters from writing them out with their
styles, so that the next commit can add another set of characters.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 14:21:07 +02:00
Stefan HallerandClaude Opus 5.5 88f258134b Record how lines run through the cells of the commit graph
A cell of the commit graph only records which of its edges are touched
by lines. That's all the box drawing characters can show, but it
doesn't say how the lines connect. A vertical line with a horizontal
line passing behind it touches all four edges. So does a cell in which
the line from above bends to the left, the line below comes in from the
left, and a horizontal line passes through.

For the lines at the top and bottom edges, record whether they run
straight on or bend to the left or right, and record whether a line
passes through horizontally. Nothing reads this yet. It prepares for
drawing the graph with symbols that can show these differences.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 14:21:07 +02:00
Stefan HallerandClaude Opus 5.5 b7f9b91e22 Stop drawing a line below root commits in the commit graph
If a root commit isn't the last one in the list, the graph draws a
vertical line below it in every row that follows. A root commit's pipe
to the empty tree never terminates, because no commit comes after it,
so it is carried on from row to row. The line also takes up its column,
and an unrelated commit after the root commit is placed to the right of
it.

Drop that pipe after the root commit's row. In its own row, it still
gives the commit symbol the style of the commit. Also, place a commit
that no line leads to next to the lines that go on into its row, not
next to the lines that ended in the row before. Their columns are free
again.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 14:21:07 +02:00
Stefan HallerandClaude Opus 5.5 d7c9ad90a7 Demonstrate that the graph continues the line below a root commit
If a root commit isn't the last one in the list, the graph draws a
vertical line below it in every row that follows, although nothing
comes after a root commit. This happens in repos with more than one
root commit, for example when showing all branches of a repo with a
gh-pages branch, or after merging an unrelated history.

Also, a commit that no line leads to is placed to the right of the
lines that ended in the row before it, although their columns are free
again.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 14:21:07 +02:00
16 changed files with 744 additions and 54 deletions
+12
View File
@@ -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
+6
View File
@@ -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,
+4
View File
@@ -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
+6
View File
@@ -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) {
+14
View File
@@ -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 {
+1
View File
@@ -75,6 +75,7 @@ func NewSubCommitsContext(
startIdx,
endIdx,
shouldShowGraph(c),
commitGraphSymbolSet(c),
git_commands.NewNullBisectInfo(),
)
}
+2
View File
@@ -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)
+4
View File
@@ -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 {
+2 -1
View File
@@ -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))
})
}
}
+66 -25
View File
@@ -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
+76 -22
View File
@@ -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 {
+373 -6
View File
@@ -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)
}
}
+10
View File
@@ -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.",
+3
View File
@@ -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.