mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-10-06 22:16:22 -04:00
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>
974 lines
36 KiB
Go
974 lines
36 KiB
Go
package graph
|
|
|
|
import (
|
|
"fmt"
|
|
"math/rand"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/gookit/color"
|
|
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
|
"github.com/jesseduffield/lazygit/pkg/gui/presentation/authors"
|
|
"github.com/jesseduffield/lazygit/pkg/gui/style"
|
|
"github.com/jesseduffield/lazygit/pkg/utils"
|
|
"github.com/samber/lo"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/xo/terminfo"
|
|
)
|
|
|
|
func TestRenderCommitGraph(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
commitOpts []models.NewCommitOpts
|
|
expectedOutput string
|
|
}{
|
|
{
|
|
name: "with some merges",
|
|
commitOpts: []models.NewCommitOpts{
|
|
{Hash: "1", Parents: []string{"2"}},
|
|
{Hash: "2", Parents: []string{"3"}},
|
|
{Hash: "3", Parents: []string{"4"}},
|
|
{Hash: "4", Parents: []string{"5", "7"}},
|
|
{Hash: "7", Parents: []string{"5"}},
|
|
{Hash: "5", Parents: []string{"8"}},
|
|
{Hash: "8", Parents: []string{"9"}},
|
|
{Hash: "9", Parents: []string{"A", "B"}},
|
|
{Hash: "B", Parents: []string{"D"}},
|
|
{Hash: "D", Parents: []string{"D"}},
|
|
{Hash: "A", Parents: []string{"E"}},
|
|
{Hash: "E", Parents: []string{"F"}},
|
|
{Hash: "F", Parents: []string{"D"}},
|
|
{Hash: "D", Parents: []string{"G"}},
|
|
},
|
|
expectedOutput: `
|
|
1 ○
|
|
2 ○
|
|
3 ○
|
|
4 ◎─╮
|
|
7 │ ○
|
|
5 ○─╯
|
|
8 ○
|
|
9 ◎─╮
|
|
B │ ○
|
|
D │ ○
|
|
A ○ │
|
|
E ○ │
|
|
F ○ │
|
|
D ○─╯`,
|
|
},
|
|
{
|
|
name: "with a path that has room to move to the left",
|
|
commitOpts: []models.NewCommitOpts{
|
|
{Hash: "1", Parents: []string{"2"}},
|
|
{Hash: "2", Parents: []string{"3", "4"}},
|
|
{Hash: "4", Parents: []string{"3", "5"}},
|
|
{Hash: "3", Parents: []string{"5"}},
|
|
{Hash: "5", Parents: []string{"6"}},
|
|
{Hash: "6", Parents: []string{"7"}},
|
|
},
|
|
expectedOutput: `
|
|
1 ○
|
|
2 ◎─╮
|
|
4 │ ◎─╮
|
|
3 ○─╯ │
|
|
5 ○───╯
|
|
6 ○`,
|
|
},
|
|
{
|
|
name: "with a new commit",
|
|
commitOpts: []models.NewCommitOpts{
|
|
{Hash: "1", Parents: []string{"2"}},
|
|
{Hash: "2", Parents: []string{"3", "4"}},
|
|
{Hash: "4", Parents: []string{"3", "5"}},
|
|
{Hash: "Z", Parents: []string{"Z"}},
|
|
{Hash: "3", Parents: []string{"5"}},
|
|
{Hash: "5", Parents: []string{"6"}},
|
|
{Hash: "6", Parents: []string{"7"}},
|
|
},
|
|
expectedOutput: `
|
|
1 ○
|
|
2 ◎─╮
|
|
4 │ ◎─╮
|
|
Z │ │ │ ○
|
|
3 ○─╯ │ │
|
|
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{
|
|
{Hash: "1", Parents: []string{"2"}},
|
|
{Hash: "2", Parents: []string{"3", "4"}},
|
|
{Hash: "3", Parents: []string{"5", "4"}},
|
|
{Hash: "5", Parents: []string{"7", "8"}},
|
|
{Hash: "4", Parents: []string{"7"}},
|
|
{Hash: "7", Parents: []string{"11"}},
|
|
},
|
|
expectedOutput: `
|
|
1 ○
|
|
2 ◎─╮
|
|
3 ◎─│─╮
|
|
5 ◎─│─│─╮
|
|
4 │ ○─╯ │
|
|
7 ○─╯ ╭─╯`,
|
|
},
|
|
{
|
|
name: "with a path that has room to move to the left and continues",
|
|
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 ○ ╭───╯ │`,
|
|
},
|
|
{
|
|
name: "with a path that has room to move to the left and continues",
|
|
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: "new merge path fills gap before continuing path on right",
|
|
commitOpts: []models.NewCommitOpts{
|
|
{Hash: "1", Parents: []string{"2", "3", "4", "5"}},
|
|
{Hash: "4", Parents: []string{"2"}},
|
|
{Hash: "2", Parents: []string{"A"}},
|
|
{Hash: "A", Parents: []string{"6", "B"}},
|
|
{Hash: "B", Parents: []string{"C"}},
|
|
},
|
|
expectedOutput: `
|
|
1 ◎─┬─┬─╮
|
|
4 │ │ ○ │
|
|
2 ○─│─╯ │
|
|
A ◎─│─╮ │
|
|
B │ │ ○ │`,
|
|
},
|
|
{
|
|
name: "with a path that has room to move to the left and continues",
|
|
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"}},
|
|
{Hash: "C", Parents: []string{"D"}},
|
|
},
|
|
expectedOutput: `
|
|
1 ○
|
|
2 ◎─╮
|
|
3 ◎─│─╮
|
|
5 ◎─│─│─╮
|
|
7 ◎─│─│─│─╮
|
|
4 ○─┴─╯ │ │
|
|
B ○ ╭───╯ │
|
|
C ○ │ ╭───╯`,
|
|
},
|
|
{
|
|
name: "with a path that has room to move to the left and continues",
|
|
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", "G"}},
|
|
{Hash: "7", Parents: []string{"8", "A"}},
|
|
{Hash: "8", Parents: []string{"4", "E"}},
|
|
{Hash: "4", Parents: []string{"B"}},
|
|
{Hash: "B", Parents: []string{"C"}},
|
|
{Hash: "C", Parents: []string{"D"}},
|
|
{Hash: "D", Parents: []string{"F"}},
|
|
},
|
|
expectedOutput: `
|
|
1 ○
|
|
2 ◎─╮
|
|
3 ◎─│─╮
|
|
5 ◎─│─│─╮
|
|
7 ◎─│─│─│─╮
|
|
8 ◎─│─│─│─│─╮
|
|
4 ○─┴─╯ │ │ │
|
|
B ○ ╭───╯ │ │
|
|
C ○ │ ╭───╯ │
|
|
D ○ │ │ ╭───╯`,
|
|
},
|
|
}
|
|
|
|
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("blah"), getStyle, BoxDrawingSymbols)
|
|
|
|
trimmedExpectedOutput := ""
|
|
for line := range strings.SplitSeq(strings.TrimPrefix(test.expectedOutput, "\n"), "\n") {
|
|
trimmedExpectedOutput += strings.TrimSpace(line) + "\n"
|
|
}
|
|
|
|
t.Log("\nexpected: \n" + trimmedExpectedOutput)
|
|
|
|
output := ""
|
|
for i, line := range lines {
|
|
description := test.commitOpts[i].Hash
|
|
output += strings.TrimSpace(description+" "+utils.Decolorise(line)) + "\n"
|
|
}
|
|
t.Log("\nactual: \n" + output)
|
|
|
|
assert.Equal(t,
|
|
trimmedExpectedOutput,
|
|
output)
|
|
})
|
|
}
|
|
}
|
|
|
|
// 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
|
|
green := style.FgGreen
|
|
// blue := style.FgBlue
|
|
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
|
|
commit *models.Commit
|
|
prevCommit *models.Commit
|
|
expectedStr string
|
|
expectedStyles []style.TextStyle
|
|
}{
|
|
{
|
|
name: "single cell",
|
|
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: "○",
|
|
expectedStyles: []style.TextStyle{green},
|
|
},
|
|
{
|
|
name: "single cell, selected",
|
|
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: "○",
|
|
expectedStyles: []style.TextStyle{highlightStyle},
|
|
},
|
|
{
|
|
name: "terminating hook and starting hook, selected",
|
|
pipes: []Pipe{
|
|
{fromPos: 0, toPos: 0, fromHash: pool("a"), toHash: pool("selected"), kind: TERMINATES, style: &cyan},
|
|
{fromPos: 1, toPos: 0, fromHash: pool("c"), toHash: pool("selected"), kind: TERMINATES, style: &yellow},
|
|
{fromPos: 0, toPos: 0, fromHash: pool("selected"), toHash: pool("d"), kind: STARTS, style: &green},
|
|
{fromPos: 0, toPos: 1, fromHash: pool("selected"), toHash: pool("e"), kind: STARTS, style: &green},
|
|
},
|
|
prevCommit: models.NewCommit(hashPool, models.NewCommitOpts{Hash: "a"}),
|
|
expectedStr: "◎─╮",
|
|
expectedStyles: []style.TextStyle{
|
|
highlightStyle, highlightStyle, highlightStyle,
|
|
},
|
|
},
|
|
{
|
|
name: "terminating hook and starting hook, prioritise the terminating one",
|
|
pipes: []Pipe{
|
|
{fromPos: 0, toPos: 0, fromHash: pool("a"), toHash: pool("b"), kind: TERMINATES, style: &red},
|
|
{fromPos: 1, toPos: 0, fromHash: pool("c"), toHash: pool("b"), kind: TERMINATES, style: &magenta},
|
|
{fromPos: 0, toPos: 0, fromHash: pool("b"), toHash: pool("d"), kind: STARTS, style: &green},
|
|
{fromPos: 0, toPos: 1, fromHash: pool("b"), toHash: pool("e"), kind: STARTS, style: &green},
|
|
},
|
|
prevCommit: models.NewCommit(hashPool, models.NewCommitOpts{Hash: "a"}),
|
|
expectedStr: "◎─│",
|
|
expectedStyles: []style.TextStyle{
|
|
green, green, magenta,
|
|
},
|
|
},
|
|
{
|
|
name: "starting and terminating pipe sharing some space",
|
|
pipes: []Pipe{
|
|
{fromPos: 0, toPos: 0, fromHash: pool("a1"), toHash: pool("a2"), kind: TERMINATES, style: &red},
|
|
{fromPos: 0, toPos: 0, fromHash: pool("a2"), 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("a2"), kind: TERMINATES, style: &green},
|
|
{fromPos: 0, toPos: 2, fromHash: pool("a2"), toHash: pool("c3"), kind: STARTS, style: &yellow},
|
|
},
|
|
prevCommit: models.NewCommit(hashPool, models.NewCommitOpts{Hash: "a1"}),
|
|
expectedStr: "◎─│─┬─╯",
|
|
expectedStyles: []style.TextStyle{
|
|
yellow, yellow, magenta, yellow, yellow, green, green,
|
|
},
|
|
},
|
|
{
|
|
name: "starting and terminating pipe sharing some space, with selection",
|
|
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: "◎───╮ ╯",
|
|
expectedStyles: []style.TextStyle{
|
|
highlightStyle, highlightStyle, highlightStyle, highlightStyle, highlightStyle, nothing, green,
|
|
},
|
|
},
|
|
{
|
|
name: "many terminating pipes",
|
|
pipes: []Pipe{
|
|
{fromPos: 0, toPos: 0, fromHash: pool("a1"), toHash: pool("a2"), kind: TERMINATES, style: &red},
|
|
{fromPos: 0, toPos: 0, fromHash: pool("a2"), toHash: pool("a3"), kind: STARTS, style: &yellow},
|
|
{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: &green},
|
|
},
|
|
prevCommit: models.NewCommit(hashPool, models.NewCommitOpts{Hash: "a1"}),
|
|
expectedStr: "○─┴─╯",
|
|
expectedStyles: []style.TextStyle{
|
|
yellow, magenta, magenta, green, green,
|
|
},
|
|
},
|
|
{
|
|
name: "starting pipe passing through",
|
|
pipes: []Pipe{
|
|
{fromPos: 0, toPos: 0, fromHash: pool("a1"), toHash: pool("a2"), kind: TERMINATES, style: &red},
|
|
{fromPos: 0, toPos: 0, fromHash: pool("a2"), toHash: pool("a3"), kind: STARTS, style: &yellow},
|
|
{fromPos: 0, toPos: 3, fromHash: pool("a2"), toHash: pool("d3"), kind: STARTS, style: &yellow},
|
|
{fromPos: 1, toPos: 1, fromHash: pool("b1"), toHash: pool("b3"), kind: CONTINUES, style: &magenta},
|
|
{fromPos: 2, toPos: 2, fromHash: pool("c1"), toHash: pool("c3"), kind: CONTINUES, style: &green},
|
|
},
|
|
prevCommit: models.NewCommit(hashPool, models.NewCommitOpts{Hash: "a1"}),
|
|
expectedStr: "◎─│─│─╮",
|
|
expectedStyles: []style.TextStyle{
|
|
yellow, yellow, magenta, yellow, green, yellow, yellow,
|
|
},
|
|
},
|
|
{
|
|
name: "starting and terminating path crossing continuing path",
|
|
pipes: []Pipe{
|
|
{fromPos: 0, toPos: 0, fromHash: pool("a1"), toHash: pool("a2"), kind: TERMINATES, style: &red},
|
|
{fromPos: 0, toPos: 0, fromHash: pool("a2"), toHash: pool("a3"), kind: STARTS, style: &yellow},
|
|
{fromPos: 0, toPos: 1, fromHash: pool("a2"), toHash: pool("b3"), kind: STARTS, style: &yellow},
|
|
{fromPos: 1, toPos: 1, fromHash: pool("b1"), toHash: pool("a2"), kind: CONTINUES, style: &green},
|
|
{fromPos: 2, toPos: 0, fromHash: pool("c1"), toHash: pool("a2"), kind: TERMINATES, style: &magenta},
|
|
},
|
|
prevCommit: models.NewCommit(hashPool, models.NewCommitOpts{Hash: "a1"}),
|
|
expectedStr: "◎─│─╯",
|
|
expectedStyles: []style.TextStyle{
|
|
yellow, yellow, green, magenta, magenta,
|
|
},
|
|
},
|
|
{
|
|
name: "another clash of starting and terminating paths",
|
|
pipes: []Pipe{
|
|
{fromPos: 0, toPos: 0, fromHash: pool("a1"), toHash: pool("a2"), kind: TERMINATES, style: &red},
|
|
{fromPos: 0, toPos: 0, fromHash: pool("a2"), toHash: pool("a3"), kind: STARTS, style: &yellow},
|
|
{fromPos: 0, toPos: 1, fromHash: pool("a2"), toHash: pool("b3"), kind: STARTS, style: &yellow},
|
|
{fromPos: 2, toPos: 2, fromHash: pool("c1"), toHash: pool("c3"), kind: CONTINUES, style: &green},
|
|
{fromPos: 3, toPos: 0, fromHash: pool("d1"), toHash: pool("a2"), kind: TERMINATES, style: &magenta},
|
|
},
|
|
prevCommit: models.NewCommit(hashPool, models.NewCommitOpts{Hash: "a1"}),
|
|
expectedStr: "◎─┬─│─╯",
|
|
expectedStyles: []style.TextStyle{
|
|
yellow, yellow, yellow, magenta, green, magenta, magenta,
|
|
},
|
|
},
|
|
{
|
|
name: "commit whose previous commit is selected",
|
|
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: &yellow},
|
|
},
|
|
prevCommit: models.NewCommit(hashPool, models.NewCommitOpts{Hash: "selected"}),
|
|
expectedStr: "○",
|
|
expectedStyles: []style.TextStyle{
|
|
yellow,
|
|
},
|
|
},
|
|
{
|
|
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: 1, toPos: 1, fromHash: pool("selected"), toHash: pool("b3"), kind: CONTINUES, style: &red},
|
|
},
|
|
prevCommit: models.NewCommit(hashPool, models.NewCommitOpts{Hash: "selected"}),
|
|
expectedStr: "○ │",
|
|
expectedStyles: []style.TextStyle{
|
|
highlightStyle, nothing, highlightStyle,
|
|
},
|
|
},
|
|
{
|
|
name: "commit whose previous commit is selected and is a merge commit, with continuing pipe inbetween",
|
|
pipes: []Pipe{
|
|
{fromPos: 0, toPos: 0, fromHash: pool("selected"), toHash: pool("a2"), kind: TERMINATES, style: &red},
|
|
{fromPos: 1, toPos: 1, fromHash: pool("z1"), toHash: pool("z3"), kind: CONTINUES, style: &green},
|
|
{fromPos: 2, toPos: 2, fromHash: pool("selected"), toHash: pool("b3"), kind: CONTINUES, style: &red},
|
|
},
|
|
prevCommit: models.NewCommit(hashPool, models.NewCommitOpts{Hash: "selected"}),
|
|
expectedStr: "○ │ │",
|
|
expectedStyles: []style.TextStyle{
|
|
highlightStyle, nothing, green, nothing, highlightStyle,
|
|
},
|
|
},
|
|
{
|
|
name: "when previous commit is selected, not a merge commit, and spawns a continuing pipe",
|
|
pipes: []Pipe{
|
|
{fromPos: 0, toPos: 0, fromHash: pool("a1"), toHash: pool("a2"), kind: TERMINATES, style: &red},
|
|
{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},
|
|
{fromPos: 1, toPos: 0, fromHash: pool("selected"), toHash: pool("a2"), kind: TERMINATES, style: &yellow},
|
|
},
|
|
prevCommit: models.NewCommit(hashPool, models.NewCommitOpts{Hash: "selected"}),
|
|
expectedStr: "◎─╯",
|
|
expectedStyles: []style.TextStyle{
|
|
highlightStyle, 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, BoxDrawingSymbols)
|
|
t.Log("actual cells:")
|
|
t.Log(actualStr)
|
|
expectedStr := ""
|
|
if len([]rune(test.expectedStr)) != len(test.expectedStyles) {
|
|
t.Fatalf("Error in test setup: you have %d characters in the expected output (%s) but have specified %d styles", len([]rune(test.expectedStr)), test.expectedStr, len(test.expectedStyles))
|
|
}
|
|
for i, char := range []rune(test.expectedStr) {
|
|
expectedStr += test.expectedStyles[i].Sprint(string(char))
|
|
}
|
|
expectedStr += " "
|
|
t.Log("expected cells:")
|
|
t.Log(expectedStr)
|
|
|
|
assert.Equal(t, expectedStr, actualStr)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetNextPipes(t *testing.T) {
|
|
hashPool := &utils.StringPool{}
|
|
pool := func(s string) *string { return hashPool.Add(s) }
|
|
|
|
tests := []struct {
|
|
prevPipes []Pipe
|
|
commit *models.Commit
|
|
expected []Pipe
|
|
}{
|
|
{
|
|
prevPipes: []Pipe{
|
|
{fromPos: 0, toPos: 0, fromHash: pool("a"), toHash: pool("b"), kind: STARTS, style: &style.FgDefault},
|
|
},
|
|
commit: models.NewCommit(hashPool, models.NewCommitOpts{
|
|
Hash: "b",
|
|
Parents: []string{"c"},
|
|
}),
|
|
expected: []Pipe{
|
|
{fromPos: 0, toPos: 0, fromHash: pool("a"), toHash: pool("b"), kind: TERMINATES, style: &style.FgDefault},
|
|
{fromPos: 0, toPos: 0, fromHash: pool("b"), toHash: pool("c"), kind: STARTS, style: &style.FgDefault},
|
|
},
|
|
},
|
|
{
|
|
prevPipes: []Pipe{
|
|
{fromPos: 0, toPos: 0, fromHash: pool("a"), toHash: pool("b"), kind: TERMINATES, style: &style.FgDefault},
|
|
{fromPos: 0, toPos: 0, fromHash: pool("b"), toHash: pool("c"), kind: STARTS, style: &style.FgDefault},
|
|
{fromPos: 0, toPos: 1, fromHash: pool("b"), toHash: pool("d"), kind: STARTS, style: &style.FgDefault},
|
|
},
|
|
commit: models.NewCommit(hashPool, models.NewCommitOpts{
|
|
Hash: "d",
|
|
Parents: []string{"e"},
|
|
}),
|
|
expected: []Pipe{
|
|
{fromPos: 0, toPos: 0, fromHash: pool("b"), toHash: pool("c"), kind: CONTINUES, style: &style.FgDefault},
|
|
{fromPos: 1, toPos: 1, fromHash: pool("b"), toHash: pool("d"), kind: TERMINATES, style: &style.FgDefault},
|
|
{fromPos: 1, toPos: 1, fromHash: pool("d"), toHash: pool("e"), kind: STARTS, style: &style.FgDefault},
|
|
},
|
|
},
|
|
{
|
|
prevPipes: []Pipe{
|
|
{fromPos: 0, toPos: 0, fromHash: pool("a"), toHash: pool("root"), kind: TERMINATES, style: &style.FgDefault},
|
|
},
|
|
commit: models.NewCommit(hashPool, models.NewCommitOpts{
|
|
Hash: "root",
|
|
Parents: []string{},
|
|
}),
|
|
expected: []Pipe{
|
|
{fromPos: 0, toPos: 0, fromHash: pool("root"), toHash: pool(models.EmptyTreeCommitHash), kind: STARTS, style: &style.FgDefault},
|
|
},
|
|
},
|
|
}
|
|
|
|
oldColorLevel := color.ForceSetColorLevel(terminfo.ColorLevelMillions)
|
|
defer color.ForceSetColorLevel(oldColorLevel)
|
|
|
|
for _, test := range tests {
|
|
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, BoxDrawingSymbols)
|
|
expectedStr := renderPipeSet(test.expected, pool("selected"), nil, BoxDrawingSymbols)
|
|
t.Log("expected cells:")
|
|
t.Log(expectedStr)
|
|
t.Log("actual cells:")
|
|
t.Log(actualStr)
|
|
assert.EqualValues(t, test.expected, pipes)
|
|
}
|
|
}
|
|
|
|
func BenchmarkRenderCommitGraph(b *testing.B) {
|
|
oldColorLevel := color.ForceSetColorLevel(terminfo.ColorLevelMillions)
|
|
defer color.ForceSetColorLevel(oldColorLevel)
|
|
|
|
hashPool := &utils.StringPool{}
|
|
|
|
commits := generateCommits(hashPool, 50)
|
|
getStyle := func(commit *models.Commit) *style.TextStyle {
|
|
return authors.AuthorStyle(commit.AuthorName)
|
|
}
|
|
b.ResetTimer()
|
|
for b.Loop() {
|
|
RenderCommitGraph(commits, hashPool.Add("selected"), getStyle, BoxDrawingSymbols)
|
|
}
|
|
}
|
|
|
|
func generateCommits(hashPool *utils.StringPool, count int) []*models.Commit {
|
|
rnd := rand.New(rand.NewSource(1234))
|
|
pool := []*models.Commit{models.NewCommit(hashPool, models.NewCommitOpts{Hash: "a", AuthorName: "A"})}
|
|
commits := make([]*models.Commit, 0, count)
|
|
authorPool := []string{"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"}
|
|
for len(commits) < count {
|
|
currentCommitIdx := rnd.Intn(len(pool))
|
|
currentCommit := pool[currentCommitIdx]
|
|
pool = append(pool[0:currentCommitIdx], pool[currentCommitIdx+1:]...)
|
|
// I need to pick a random number of parents to add
|
|
parentCount := rnd.Intn(2) + 1
|
|
|
|
parentHashes := currentCommit.Parents()
|
|
for j := range parentCount {
|
|
reuseParent := rnd.Intn(6) != 1 && j <= len(pool)-1 && j != 0
|
|
var newParent *models.Commit
|
|
if reuseParent {
|
|
newParent = pool[j]
|
|
} else {
|
|
newParent = models.NewCommit(hashPool, models.NewCommitOpts{
|
|
Hash: fmt.Sprintf("%s%d", currentCommit.Hash(), j),
|
|
AuthorName: authorPool[rnd.Intn(len(authorPool))],
|
|
})
|
|
pool = append(pool, newParent)
|
|
}
|
|
parentHashes = append(parentHashes, newParent.Hash())
|
|
}
|
|
|
|
changedCommit := models.NewCommit(hashPool, models.NewCommitOpts{
|
|
Hash: currentCommit.Hash(),
|
|
AuthorName: currentCommit.AuthorName,
|
|
Parents: parentHashes,
|
|
})
|
|
commits = append(commits, changedCommit)
|
|
}
|
|
|
|
return commits
|
|
}
|