mirror of
https://github.com/jesseduffield/lazygit.git
synced 2025-11-29 22:48:24 +02:00
render commit graph
This commit is contained in:
@@ -2,120 +2,152 @@ package presentation
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/presentation/authors"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/presentation/graph"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/style"
|
||||
"github.com/jesseduffield/lazygit/pkg/theme"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/kyokomi/emoji/v2"
|
||||
)
|
||||
|
||||
func GetCommitListDisplayStrings(commits []*models.Commit, fullDescription bool, cherryPickedCommitShaMap map[string]bool, diffName string, parseEmoji bool) [][]string {
|
||||
lines := make([][]string, len(commits))
|
||||
type pipeSetCacheKey struct {
|
||||
commitSha string
|
||||
commitCount int
|
||||
}
|
||||
|
||||
var displayFunc func(*models.Commit, map[string]bool, bool, bool) []string
|
||||
if fullDescription {
|
||||
displayFunc = getFullDescriptionDisplayStringsForCommit
|
||||
} else {
|
||||
displayFunc = getDisplayStringsForCommit
|
||||
var pipeSetCache = make(map[pipeSetCacheKey][][]*graph.Pipe)
|
||||
var mutex sync.Mutex
|
||||
|
||||
func GetCommitListDisplayStrings(
|
||||
commits []*models.Commit,
|
||||
fullDescription bool,
|
||||
cherryPickedCommitShaMap map[string]bool,
|
||||
diffName string,
|
||||
parseEmoji bool,
|
||||
selectedCommitSha string,
|
||||
startIdx int,
|
||||
length int,
|
||||
) [][]string {
|
||||
mutex.Lock()
|
||||
defer mutex.Unlock()
|
||||
|
||||
if len(commits) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
for i := range commits {
|
||||
diffed := commits[i].Sha == diffName
|
||||
lines[i] = displayFunc(commits[i], cherryPickedCommitShaMap, diffed, parseEmoji)
|
||||
// given that our cache key is a commit sha and a commit count, it's very important that we don't actually try to render pipes
|
||||
// when dealing with things like filtered commits.
|
||||
cacheKey := pipeSetCacheKey{
|
||||
commitSha: commits[0].Sha,
|
||||
commitCount: len(commits),
|
||||
}
|
||||
|
||||
pipeSets, ok := pipeSetCache[cacheKey]
|
||||
if !ok {
|
||||
// pipe sets are unique to a commit head. and a commit count. Sometimes we haven't loaded everything for that.
|
||||
// so let's just cache it based on that.
|
||||
getStyle := func(commit *models.Commit) style.TextStyle {
|
||||
return authors.AuthorStyle(commit.Author)
|
||||
}
|
||||
pipeSets = graph.GetPipeSets(commits, getStyle)
|
||||
pipeSetCache[cacheKey] = pipeSets
|
||||
}
|
||||
|
||||
end := startIdx + length
|
||||
if end > len(commits)-1 {
|
||||
end = len(commits) - 1
|
||||
}
|
||||
|
||||
filteredPipeSets := pipeSets[startIdx : end+1]
|
||||
filteredCommits := commits[startIdx : end+1]
|
||||
graphLines := graph.RenderAux(filteredPipeSets, filteredCommits, selectedCommitSha)
|
||||
|
||||
lines := make([][]string, 0, len(graphLines))
|
||||
for i, commit := range filteredCommits {
|
||||
lines = append(lines, displayCommit(commit, cherryPickedCommitShaMap, diffName, parseEmoji, graphLines[i], fullDescription))
|
||||
}
|
||||
return lines
|
||||
}
|
||||
|
||||
func getFullDescriptionDisplayStringsForCommit(c *models.Commit, cherryPickedCommitShaMap map[string]bool, diffed, parseEmoji bool) []string {
|
||||
shaColor := theme.DefaultTextColor
|
||||
switch c.Status {
|
||||
case "unpushed":
|
||||
shaColor = style.FgRed
|
||||
case "pushed":
|
||||
shaColor = style.FgYellow
|
||||
case "merged":
|
||||
shaColor = style.FgGreen
|
||||
case "rebasing":
|
||||
shaColor = style.FgBlue
|
||||
case "reflog":
|
||||
shaColor = style.FgBlue
|
||||
}
|
||||
func displayCommit(
|
||||
commit *models.Commit,
|
||||
cherryPickedCommitShaMap map[string]bool,
|
||||
diffName string,
|
||||
parseEmoji bool,
|
||||
graphLine string,
|
||||
fullDescription bool,
|
||||
) []string {
|
||||
|
||||
if diffed {
|
||||
shaColor = theme.DiffTerminalColor
|
||||
} else if cherryPickedCommitShaMap[c.Sha] {
|
||||
// for some reason, setting the background to blue pads out the other commits
|
||||
// horizontally. For the sake of accessibility I'm considering this a feature,
|
||||
// not a bug
|
||||
shaColor = theme.CherryPickedCommitTextStyle
|
||||
}
|
||||
|
||||
tagString := ""
|
||||
secondColumnString := style.FgBlue.Sprint(utils.UnixToDate(c.UnixTimestamp))
|
||||
if c.Action != "" {
|
||||
secondColumnString = actionColorMap(c.Action).Sprint(c.Action)
|
||||
} else if c.ExtraInfo != "" {
|
||||
tagString = style.FgMagenta.SetBold().Sprint(c.ExtraInfo) + " "
|
||||
}
|
||||
|
||||
name := c.Name
|
||||
if parseEmoji {
|
||||
name = emoji.Sprint(name)
|
||||
}
|
||||
|
||||
return []string{
|
||||
shaColor.Sprint(c.ShortSha()),
|
||||
secondColumnString,
|
||||
authors.LongAuthor(c.Author),
|
||||
tagString + theme.DefaultTextColor.Sprint(name),
|
||||
}
|
||||
}
|
||||
|
||||
func getDisplayStringsForCommit(c *models.Commit, cherryPickedCommitShaMap map[string]bool, diffed, parseEmoji bool) []string {
|
||||
shaColor := theme.DefaultTextColor
|
||||
switch c.Status {
|
||||
case "unpushed":
|
||||
shaColor = style.FgRed
|
||||
case "pushed":
|
||||
shaColor = style.FgYellow
|
||||
case "merged":
|
||||
shaColor = style.FgGreen
|
||||
case "rebasing":
|
||||
shaColor = style.FgBlue
|
||||
case "reflog":
|
||||
shaColor = style.FgBlue
|
||||
}
|
||||
|
||||
if diffed {
|
||||
shaColor = theme.DiffTerminalColor
|
||||
} else if cherryPickedCommitShaMap[c.Sha] {
|
||||
// for some reason, setting the background to blue pads out the other commits
|
||||
// horizontally. For the sake of accessibility I'm considering this a feature,
|
||||
// not a bug
|
||||
shaColor = theme.CherryPickedCommitTextStyle
|
||||
}
|
||||
shaColor := getShaColor(commit, diffName, cherryPickedCommitShaMap)
|
||||
|
||||
actionString := ""
|
||||
tagString := ""
|
||||
if c.Action != "" {
|
||||
actionString = actionColorMap(c.Action).Sprint(utils.WithPadding(c.Action, 7)) + " "
|
||||
} else if len(c.Tags) > 0 {
|
||||
tagString = theme.DiffTerminalColor.SetBold().Sprint(strings.Join(c.Tags, " ")) + " "
|
||||
if commit.Action != "" {
|
||||
actionString = actionColorMap(commit.Action).Sprint(commit.Action) + " "
|
||||
}
|
||||
|
||||
name := c.Name
|
||||
tagString := ""
|
||||
if fullDescription {
|
||||
if commit.ExtraInfo != "" {
|
||||
tagString = style.FgMagenta.SetBold().Sprint(commit.ExtraInfo) + " "
|
||||
}
|
||||
} else {
|
||||
if len(commit.Tags) > 0 {
|
||||
tagString = theme.DiffTerminalColor.SetBold().Sprint(strings.Join(commit.Tags, " ")) + " "
|
||||
}
|
||||
}
|
||||
|
||||
name := commit.Name
|
||||
if parseEmoji {
|
||||
name = emoji.Sprint(name)
|
||||
}
|
||||
|
||||
return []string{
|
||||
shaColor.Sprint(c.ShortSha()),
|
||||
authors.ShortAuthor(c.Author),
|
||||
actionString + tagString + theme.DefaultTextColor.Sprint(name),
|
||||
authorFunc := authors.ShortAuthor
|
||||
if fullDescription {
|
||||
authorFunc = authors.LongAuthor
|
||||
}
|
||||
|
||||
cols := make([]string, 0, 5)
|
||||
cols = append(cols, shaColor.Sprint(commit.ShortSha()))
|
||||
if fullDescription {
|
||||
cols = append(cols, style.FgBlue.Sprint(utils.UnixToDate(commit.UnixTimestamp)))
|
||||
}
|
||||
cols = append(
|
||||
cols,
|
||||
actionString,
|
||||
authorFunc(commit.Author),
|
||||
graphLine+tagString+theme.DefaultTextColor.Sprint(name),
|
||||
)
|
||||
|
||||
return cols
|
||||
|
||||
}
|
||||
|
||||
func getShaColor(commit *models.Commit, diffName string, cherryPickedCommitShaMap map[string]bool) style.TextStyle {
|
||||
diffed := commit.Sha == diffName
|
||||
shaColor := theme.DefaultTextColor
|
||||
switch commit.Status {
|
||||
case "unpushed":
|
||||
shaColor = style.FgRed
|
||||
case "pushed":
|
||||
shaColor = style.FgYellow
|
||||
case "merged":
|
||||
shaColor = style.FgGreen
|
||||
case "rebasing":
|
||||
shaColor = style.FgBlue
|
||||
case "reflog":
|
||||
shaColor = style.FgBlue
|
||||
}
|
||||
|
||||
if diffed {
|
||||
shaColor = theme.DiffTerminalColor
|
||||
} else if cherryPickedCommitShaMap[commit.Sha] {
|
||||
shaColor = theme.CherryPickedCommitTextStyle
|
||||
}
|
||||
|
||||
return shaColor
|
||||
}
|
||||
|
||||
func actionColorMap(str string) style.TextStyle {
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
package graph
|
||||
|
||||
import (
|
||||
"io"
|
||||
"sync"
|
||||
|
||||
"github.com/gookit/color"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/style"
|
||||
)
|
||||
|
||||
const mergeSymbol = '⏣'
|
||||
const commitSymbol = '⎔'
|
||||
const mergeSymbol = "⏣"
|
||||
const commitSymbol = "⎔"
|
||||
|
||||
type cellType int
|
||||
|
||||
@@ -22,11 +26,11 @@ type Cell struct {
|
||||
style style.TextStyle
|
||||
}
|
||||
|
||||
func (cell *Cell) render() string {
|
||||
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 rune
|
||||
var adjustedFirst string
|
||||
switch cell.cellType {
|
||||
case CONNECTION:
|
||||
adjustedFirst = first
|
||||
@@ -47,13 +51,46 @@ func (cell *Cell) render() string {
|
||||
// assert on the style of a space given a space has no styling (assuming we
|
||||
// stick to only using foreground styles)
|
||||
var styledSecondChar string
|
||||
if second == ' ' {
|
||||
if second == " " {
|
||||
styledSecondChar = " "
|
||||
} else {
|
||||
styledSecondChar = rightStyle.Sprint(string(second))
|
||||
styledSecondChar = cachedSprint(*rightStyle, second)
|
||||
}
|
||||
|
||||
return cell.style.Sprint(string(adjustedFirst)) + styledSecondChar
|
||||
_, _ = writer.WriteString(cachedSprint(cell.style, adjustedFirst))
|
||||
_, _ = writer.WriteString(styledSecondChar)
|
||||
}
|
||||
|
||||
type rgbCacheKey struct {
|
||||
*color.RGBStyle
|
||||
str string
|
||||
}
|
||||
|
||||
var rgbCache = make(map[rgbCacheKey]string)
|
||||
var rgbCacheMutex sync.RWMutex
|
||||
|
||||
func cachedSprint(style style.TextStyle, str string) string {
|
||||
switch v := style.Style.(type) {
|
||||
case *color.RGBStyle:
|
||||
rgbCacheMutex.RLock()
|
||||
key := rgbCacheKey{v, str}
|
||||
value, ok := rgbCache[key]
|
||||
rgbCacheMutex.RUnlock()
|
||||
if ok {
|
||||
return value
|
||||
}
|
||||
value = style.Sprint(str)
|
||||
rgbCacheMutex.Lock()
|
||||
rgbCache[key] = value
|
||||
rgbCacheMutex.Unlock()
|
||||
return value
|
||||
case color.Basic:
|
||||
return style.Sprint(str)
|
||||
case color.Style:
|
||||
value := style.Sprint(str)
|
||||
return value
|
||||
}
|
||||
return style.Sprint(str)
|
||||
}
|
||||
|
||||
func (cell *Cell) reset() {
|
||||
@@ -102,39 +139,39 @@ func (cell *Cell) setType(cellType cellType) *Cell {
|
||||
return cell
|
||||
}
|
||||
|
||||
func getBoxDrawingChars(up, down, left, right bool) (rune, rune) {
|
||||
func getBoxDrawingChars(up, down, left, right bool) (string, string) {
|
||||
if up && down && left && right {
|
||||
return '│', '─'
|
||||
return "│", "─"
|
||||
} else if up && down && left && !right {
|
||||
return '│', ' '
|
||||
return "│", " "
|
||||
} else if up && down && !left && right {
|
||||
return '│', '─'
|
||||
return "│", "─"
|
||||
} else if up && down && !left && !right {
|
||||
return '│', ' '
|
||||
return "│", " "
|
||||
} else if up && !down && left && right {
|
||||
return '┴', '─'
|
||||
return "┴", "─"
|
||||
} else if up && !down && left && !right {
|
||||
return '╯', ' '
|
||||
return "╯", " "
|
||||
} else if up && !down && !left && right {
|
||||
return '╰', '─'
|
||||
return "╰", "─"
|
||||
} else if up && !down && !left && !right {
|
||||
return '╵', ' '
|
||||
return "╵", " "
|
||||
} else if !up && down && left && right {
|
||||
return '┬', '─'
|
||||
return "┬", "─"
|
||||
} else if !up && down && left && !right {
|
||||
return '╮', ' '
|
||||
return "╮", " "
|
||||
} else if !up && down && !left && right {
|
||||
return '╭', '─'
|
||||
return "╭", "─"
|
||||
} else if !up && down && !left && !right {
|
||||
return '╷', ' '
|
||||
return "╷", " "
|
||||
} else if !up && !down && left && right {
|
||||
return '─', '─'
|
||||
return "─", "─"
|
||||
} else if !up && !down && left && !right {
|
||||
return '─', ' '
|
||||
return "─", " "
|
||||
} else if !up && !down && !left && right {
|
||||
return '╶', '─'
|
||||
return "╶", "─"
|
||||
} else if !up && !down && !left && !right {
|
||||
return ' ', ' '
|
||||
return " ", " "
|
||||
} else {
|
||||
panic("should not be possible")
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package graph
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -29,7 +30,7 @@ type Pipe struct {
|
||||
|
||||
var highlightStyle = style.FgLightWhite.SetBold()
|
||||
|
||||
func ContainsCommitSha(pipes []Pipe, sha string) bool {
|
||||
func ContainsCommitSha(pipes []*Pipe, sha string) bool {
|
||||
for _, pipe := range pipes {
|
||||
if equalHashes(pipe.fromSha, sha) {
|
||||
return true
|
||||
@@ -57,14 +58,14 @@ func RenderCommitGraph(commits []*models.Commit, selectedCommitSha string, getSt
|
||||
return lines
|
||||
}
|
||||
|
||||
func GetPipeSets(commits []*models.Commit, getStyle func(c *models.Commit) style.TextStyle) [][]Pipe {
|
||||
func GetPipeSets(commits []*models.Commit, getStyle func(c *models.Commit) style.TextStyle) [][]*Pipe {
|
||||
if len(commits) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
pipes := []Pipe{{fromPos: 0, toPos: 0, fromSha: "START", toSha: commits[0].Sha, kind: STARTS, style: style.FgDefault}}
|
||||
pipes := []*Pipe{{fromPos: 0, toPos: 0, fromSha: "START", toSha: commits[0].Sha, kind: STARTS, style: style.FgDefault}}
|
||||
|
||||
pipeSets := [][]Pipe{}
|
||||
pipeSets := [][]*Pipe{}
|
||||
for _, commit := range commits {
|
||||
pipes = getNextPipes(pipes, commit, getStyle)
|
||||
pipeSets = append(pipeSets, pipes)
|
||||
@@ -73,29 +74,51 @@ func GetPipeSets(commits []*models.Commit, getStyle func(c *models.Commit) style
|
||||
return pipeSets
|
||||
}
|
||||
|
||||
func RenderAux(pipeSets [][]Pipe, commits []*models.Commit, selectedCommitSha string) []string {
|
||||
lines := make([]string, len(pipeSets))
|
||||
func RenderAux(pipeSets [][]*Pipe, commits []*models.Commit, selectedCommitSha string) []string {
|
||||
maxProcs := runtime.GOMAXPROCS(0)
|
||||
|
||||
lines := make([]string, 0, len(pipeSets))
|
||||
// splitting up the rendering of the graph into multiple goroutines allows us to render the graph in parallel
|
||||
chunks := make([][]string, maxProcs)
|
||||
perProc := len(pipeSets) / maxProcs
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(len(pipeSets))
|
||||
for i, pipeSet := range pipeSets {
|
||||
wg.Add(maxProcs)
|
||||
|
||||
for i := 0; i < maxProcs; i++ {
|
||||
i := i
|
||||
pipeSet := pipeSet
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
var prevCommit *models.Commit
|
||||
if i > 0 {
|
||||
prevCommit = commits[i-1]
|
||||
from := i * perProc
|
||||
to := (i + 1) * perProc
|
||||
if i == maxProcs-1 {
|
||||
to = len(pipeSets)
|
||||
}
|
||||
line := renderPipeSet(pipeSet, selectedCommitSha, prevCommit)
|
||||
lines[i] = line
|
||||
innerLines := make([]string, 0, to-from)
|
||||
for j, pipeSet := range pipeSets[from:to] {
|
||||
k := from + j
|
||||
var prevCommit *models.Commit
|
||||
if k > 0 {
|
||||
prevCommit = commits[k-1]
|
||||
}
|
||||
line := renderPipeSet(pipeSet, selectedCommitSha, prevCommit)
|
||||
innerLines = append(innerLines, line)
|
||||
}
|
||||
chunks[i] = innerLines
|
||||
wg.Done()
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
for _, chunk := range chunks {
|
||||
lines = append(lines, chunk...)
|
||||
}
|
||||
|
||||
return lines
|
||||
}
|
||||
|
||||
func getNextPipes(prevPipes []Pipe, commit *models.Commit, getStyle func(c *models.Commit) style.TextStyle) []Pipe {
|
||||
currentPipes := make([]Pipe, 0, len(prevPipes))
|
||||
func getNextPipes(prevPipes []*Pipe, commit *models.Commit, getStyle func(c *models.Commit) style.TextStyle) []*Pipe {
|
||||
currentPipes := make([]*Pipe, 0, len(prevPipes))
|
||||
maxPos := 0
|
||||
for _, pipe := range prevPipes {
|
||||
// a pipe that terminated in the previous line has no bearing on the current line
|
||||
@@ -106,7 +129,7 @@ func getNextPipes(prevPipes []Pipe, commit *models.Commit, getStyle func(c *mode
|
||||
maxPos = utils.Max(maxPos, pipe.toPos)
|
||||
}
|
||||
|
||||
newPipes := make([]Pipe, 0, len(currentPipes)+len(commit.Parents))
|
||||
newPipes := make([]*Pipe, 0, len(currentPipes)+len(commit.Parents))
|
||||
// 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.
|
||||
pos := maxPos + 1
|
||||
@@ -124,7 +147,7 @@ func getNextPipes(prevPipes []Pipe, commit *models.Commit, getStyle func(c *mode
|
||||
traversedSpots := make(map[int]bool)
|
||||
|
||||
if len(commit.Parents) > 0 {
|
||||
newPipes = append(newPipes, Pipe{
|
||||
newPipes = append(newPipes, &Pipe{
|
||||
fromPos: pos,
|
||||
toPos: pos,
|
||||
fromSha: commit.Sha,
|
||||
@@ -177,7 +200,7 @@ func getNextPipes(prevPipes []Pipe, commit *models.Commit, getStyle func(c *mode
|
||||
for _, pipe := range currentPipes {
|
||||
if equalHashes(pipe.toSha, commit.Sha) {
|
||||
// terminating here
|
||||
newPipes = append(newPipes, Pipe{
|
||||
newPipes = append(newPipes, &Pipe{
|
||||
fromPos: pipe.toPos,
|
||||
toPos: pos,
|
||||
fromSha: pipe.fromSha,
|
||||
@@ -189,7 +212,7 @@ func getNextPipes(prevPipes []Pipe, commit *models.Commit, getStyle func(c *mode
|
||||
} else if pipe.toPos < pos {
|
||||
// continuing here
|
||||
availablePos := getNextAvailablePosForContinuingPipe()
|
||||
newPipes = append(newPipes, Pipe{
|
||||
newPipes = append(newPipes, &Pipe{
|
||||
fromPos: pipe.toPos,
|
||||
toPos: availablePos,
|
||||
fromSha: pipe.fromSha,
|
||||
@@ -205,7 +228,7 @@ func getNextPipes(prevPipes []Pipe, commit *models.Commit, getStyle func(c *mode
|
||||
for _, parent := range commit.Parents[1:] {
|
||||
availablePos := getNextAvailablePosForNewPipe()
|
||||
// need to act as if continuing pipes are going to continue on the same line.
|
||||
newPipes = append(newPipes, Pipe{
|
||||
newPipes = append(newPipes, &Pipe{
|
||||
fromPos: pos,
|
||||
toPos: availablePos,
|
||||
fromSha: commit.Sha,
|
||||
@@ -229,7 +252,7 @@ func getNextPipes(prevPipes []Pipe, commit *models.Commit, getStyle func(c *mode
|
||||
last = i
|
||||
}
|
||||
}
|
||||
newPipes = append(newPipes, Pipe{
|
||||
newPipes = append(newPipes, &Pipe{
|
||||
fromPos: pipe.toPos,
|
||||
toPos: last,
|
||||
fromSha: pipe.fromSha,
|
||||
@@ -253,7 +276,7 @@ func getNextPipes(prevPipes []Pipe, commit *models.Commit, getStyle func(c *mode
|
||||
}
|
||||
|
||||
func renderPipeSet(
|
||||
pipes []Pipe,
|
||||
pipes []*Pipe,
|
||||
selectedCommitSha string,
|
||||
prevCommit *models.Commit,
|
||||
) string {
|
||||
@@ -279,7 +302,7 @@ func renderPipeSet(
|
||||
cells[i] = &Cell{cellType: CONNECTION, style: style.FgDefault}
|
||||
}
|
||||
|
||||
renderPipe := func(pipe Pipe, style style.TextStyle, overrideRightStyle bool) {
|
||||
renderPipe := func(pipe *Pipe, style style.TextStyle, overrideRightStyle bool) {
|
||||
left := pipe.left()
|
||||
right := pipe.right()
|
||||
|
||||
@@ -313,9 +336,9 @@ func renderPipeSet(
|
||||
|
||||
// so we have our commit pos again, now it's time to build the cells.
|
||||
// we'll handle the one that's sourced from our selected commit last so that it can override the other cells.
|
||||
selectedPipes := []Pipe{}
|
||||
selectedPipes := []*Pipe{}
|
||||
// pre-allocating this one because most of the time we'll only have non-selected pipes
|
||||
nonSelectedPipes := make([]Pipe, 0, len(pipes))
|
||||
nonSelectedPipes := make([]*Pipe, 0, len(pipes))
|
||||
|
||||
for _, pipe := range pipes {
|
||||
if highlight && equalHashes(pipe.fromSha, selectedCommitSha) {
|
||||
@@ -356,11 +379,13 @@ func renderPipeSet(
|
||||
|
||||
cells[commitPos].setType(cType)
|
||||
|
||||
renderedCells := make([]string, len(cells))
|
||||
for i, cell := range cells {
|
||||
renderedCells[i] = cell.render()
|
||||
// using a string builder here for the sake of performance
|
||||
writer := &strings.Builder{}
|
||||
writer.Grow(len(cells) * 2)
|
||||
for _, cell := range cells {
|
||||
cell.render(writer)
|
||||
}
|
||||
return strings.Join(renderedCells, "")
|
||||
return writer.String()
|
||||
}
|
||||
|
||||
func equalHashes(a, b string) bool {
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
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/stretchr/testify/assert"
|
||||
@@ -253,7 +256,7 @@ func TestRenderPipeSet(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
pipes []Pipe
|
||||
pipes []*Pipe
|
||||
commit *models.Commit
|
||||
prevCommit *models.Commit
|
||||
expectedStr string
|
||||
@@ -261,7 +264,7 @@ func TestRenderPipeSet(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
name: "single cell",
|
||||
pipes: []Pipe{
|
||||
pipes: []*Pipe{
|
||||
{fromPos: 0, toPos: 0, fromSha: "a", toSha: "b", kind: TERMINATES, style: cyan},
|
||||
{fromPos: 0, toPos: 0, fromSha: "b", toSha: "c", kind: STARTS, style: green},
|
||||
},
|
||||
@@ -271,7 +274,7 @@ func TestRenderPipeSet(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "single cell, selected",
|
||||
pipes: []Pipe{
|
||||
pipes: []*Pipe{
|
||||
{fromPos: 0, toPos: 0, fromSha: "a", toSha: "selected", kind: TERMINATES, style: cyan},
|
||||
{fromPos: 0, toPos: 0, fromSha: "selected", toSha: "c", kind: STARTS, style: green},
|
||||
},
|
||||
@@ -281,7 +284,7 @@ func TestRenderPipeSet(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "terminating hook and starting hook, selected",
|
||||
pipes: []Pipe{
|
||||
pipes: []*Pipe{
|
||||
{fromPos: 0, toPos: 0, fromSha: "a", toSha: "selected", kind: TERMINATES, style: cyan},
|
||||
{fromPos: 1, toPos: 0, fromSha: "c", toSha: "selected", kind: TERMINATES, style: yellow},
|
||||
{fromPos: 0, toPos: 0, fromSha: "selected", toSha: "d", kind: STARTS, style: green},
|
||||
@@ -294,8 +297,8 @@ func TestRenderPipeSet(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "terminating hook and starting hook, prioritise the starting one",
|
||||
pipes: []Pipe{
|
||||
name: "terminating hook and starting hook, prioritise the terminating one",
|
||||
pipes: []*Pipe{
|
||||
{fromPos: 0, toPos: 0, fromSha: "a", toSha: "b", kind: TERMINATES, style: red},
|
||||
{fromPos: 1, toPos: 0, fromSha: "c", toSha: "b", kind: TERMINATES, style: magenta},
|
||||
{fromPos: 0, toPos: 0, fromSha: "b", toSha: "d", kind: STARTS, style: green},
|
||||
@@ -309,7 +312,7 @@ func TestRenderPipeSet(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "starting and terminating pipe sharing some space",
|
||||
pipes: []Pipe{
|
||||
pipes: []*Pipe{
|
||||
{fromPos: 0, toPos: 0, fromSha: "a1", toSha: "a2", kind: TERMINATES, style: red},
|
||||
{fromPos: 0, toPos: 0, fromSha: "a2", toSha: "a3", kind: STARTS, style: yellow},
|
||||
{fromPos: 1, toPos: 1, fromSha: "b1", toSha: "b2", kind: CONTINUES, style: magenta},
|
||||
@@ -324,7 +327,7 @@ func TestRenderPipeSet(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "starting and terminating pipe sharing some space, with selection",
|
||||
pipes: []Pipe{
|
||||
pipes: []*Pipe{
|
||||
{fromPos: 0, toPos: 0, fromSha: "a1", toSha: "selected", kind: TERMINATES, style: red},
|
||||
{fromPos: 0, toPos: 0, fromSha: "selected", toSha: "a3", kind: STARTS, style: yellow},
|
||||
{fromPos: 1, toPos: 1, fromSha: "b1", toSha: "b2", kind: CONTINUES, style: magenta},
|
||||
@@ -339,7 +342,7 @@ func TestRenderPipeSet(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "many terminating pipes",
|
||||
pipes: []Pipe{
|
||||
pipes: []*Pipe{
|
||||
{fromPos: 0, toPos: 0, fromSha: "a1", toSha: "a2", kind: TERMINATES, style: red},
|
||||
{fromPos: 0, toPos: 0, fromSha: "a2", toSha: "a3", kind: STARTS, style: yellow},
|
||||
{fromPos: 1, toPos: 0, fromSha: "b1", toSha: "a2", kind: TERMINATES, style: magenta},
|
||||
@@ -353,7 +356,7 @@ func TestRenderPipeSet(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "starting pipe passing through",
|
||||
pipes: []Pipe{
|
||||
pipes: []*Pipe{
|
||||
{fromPos: 0, toPos: 0, fromSha: "a1", toSha: "a2", kind: TERMINATES, style: red},
|
||||
{fromPos: 0, toPos: 0, fromSha: "a2", toSha: "a3", kind: STARTS, style: yellow},
|
||||
{fromPos: 0, toPos: 3, fromSha: "a2", toSha: "d3", kind: STARTS, style: yellow},
|
||||
@@ -368,7 +371,7 @@ func TestRenderPipeSet(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "starting and terminating path crossing continuing path",
|
||||
pipes: []Pipe{
|
||||
pipes: []*Pipe{
|
||||
{fromPos: 0, toPos: 0, fromSha: "a1", toSha: "a2", kind: TERMINATES, style: red},
|
||||
{fromPos: 0, toPos: 0, fromSha: "a2", toSha: "a3", kind: STARTS, style: yellow},
|
||||
{fromPos: 0, toPos: 1, fromSha: "a2", toSha: "b3", kind: STARTS, style: yellow},
|
||||
@@ -383,7 +386,7 @@ func TestRenderPipeSet(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "another clash of starting and terminating paths",
|
||||
pipes: []Pipe{
|
||||
pipes: []*Pipe{
|
||||
{fromPos: 0, toPos: 0, fromSha: "a1", toSha: "a2", kind: TERMINATES, style: red},
|
||||
{fromPos: 0, toPos: 0, fromSha: "a2", toSha: "a3", kind: STARTS, style: yellow},
|
||||
{fromPos: 0, toPos: 1, fromSha: "a2", toSha: "b3", kind: STARTS, style: yellow},
|
||||
@@ -398,7 +401,7 @@ func TestRenderPipeSet(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "commit whose previous commit is selected",
|
||||
pipes: []Pipe{
|
||||
pipes: []*Pipe{
|
||||
{fromPos: 0, toPos: 0, fromSha: "selected", toSha: "a2", kind: TERMINATES, style: red},
|
||||
{fromPos: 0, toPos: 0, fromSha: "a2", toSha: "a3", kind: STARTS, style: yellow},
|
||||
},
|
||||
@@ -410,7 +413,7 @@ func TestRenderPipeSet(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "commit whose previous commit is selected and is a merge commit",
|
||||
pipes: []Pipe{
|
||||
pipes: []*Pipe{
|
||||
{fromPos: 0, toPos: 0, fromSha: "selected", toSha: "a2", kind: TERMINATES, style: red},
|
||||
{fromPos: 1, toPos: 1, fromSha: "selected", toSha: "b3", kind: CONTINUES, style: red},
|
||||
},
|
||||
@@ -422,7 +425,7 @@ func TestRenderPipeSet(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "commit whose previous commit is selected and is a merge commit, with continuing pipe inbetween",
|
||||
pipes: []Pipe{
|
||||
pipes: []*Pipe{
|
||||
{fromPos: 0, toPos: 0, fromSha: "selected", toSha: "a2", kind: TERMINATES, style: red},
|
||||
{fromPos: 1, toPos: 1, fromSha: "z1", toSha: "z3", kind: CONTINUES, style: green},
|
||||
{fromPos: 2, toPos: 2, fromSha: "selected", toSha: "b3", kind: CONTINUES, style: red},
|
||||
@@ -435,7 +438,7 @@ func TestRenderPipeSet(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "when previous commit is selected, not a merge commit, and spawns a continuing pipe",
|
||||
pipes: []Pipe{
|
||||
pipes: []*Pipe{
|
||||
{fromPos: 0, toPos: 0, fromSha: "a1", toSha: "a2", kind: TERMINATES, style: red},
|
||||
{fromPos: 0, toPos: 0, fromSha: "a2", toSha: "a3", kind: STARTS, style: green},
|
||||
{fromPos: 0, toPos: 1, fromSha: "a2", toSha: "b3", kind: STARTS, style: green},
|
||||
@@ -471,57 +474,27 @@ func TestRenderPipeSet(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCellRender(t *testing.T) {
|
||||
tests := []struct {
|
||||
cell *Cell
|
||||
expectedString string
|
||||
}{
|
||||
{
|
||||
cell: &Cell{
|
||||
up: true,
|
||||
down: true,
|
||||
cellType: CONNECTION,
|
||||
style: style.FgCyan,
|
||||
},
|
||||
expectedString: "\x1b[36m│\x1b[0m ",
|
||||
},
|
||||
{
|
||||
cell: &Cell{
|
||||
up: true,
|
||||
down: true,
|
||||
cellType: COMMIT,
|
||||
style: style.FgCyan,
|
||||
},
|
||||
expectedString: "\x1b[36m⎔\x1b[0m ",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
assert.EqualValues(t, test.expectedString, test.cell.render())
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetNextPipes(t *testing.T) {
|
||||
tests := []struct {
|
||||
prevPipes []Pipe
|
||||
prevPipes []*Pipe
|
||||
commit *models.Commit
|
||||
expected []Pipe
|
||||
expected []*Pipe
|
||||
}{
|
||||
{
|
||||
prevPipes: []Pipe{
|
||||
prevPipes: []*Pipe{
|
||||
{fromPos: 0, toPos: 0, fromSha: "a", toSha: "b", kind: STARTS, style: style.FgDefault},
|
||||
},
|
||||
commit: &models.Commit{
|
||||
Sha: "b",
|
||||
Parents: []string{"c"},
|
||||
},
|
||||
expected: []Pipe{
|
||||
expected: []*Pipe{
|
||||
{fromPos: 0, toPos: 0, fromSha: "a", toSha: "b", kind: TERMINATES, style: style.FgDefault},
|
||||
{fromPos: 0, toPos: 0, fromSha: "b", toSha: "c", kind: STARTS, style: style.FgDefault},
|
||||
},
|
||||
},
|
||||
{
|
||||
prevPipes: []Pipe{
|
||||
prevPipes: []*Pipe{
|
||||
{fromPos: 0, toPos: 0, fromSha: "a", toSha: "b", kind: TERMINATES, style: style.FgDefault},
|
||||
{fromPos: 0, toPos: 0, fromSha: "b", toSha: "c", kind: STARTS, style: style.FgDefault},
|
||||
{fromPos: 0, toPos: 1, fromSha: "b", toSha: "d", kind: STARTS, style: style.FgDefault},
|
||||
@@ -530,7 +503,7 @@ func TestGetNextPipes(t *testing.T) {
|
||||
Sha: "d",
|
||||
Parents: []string{"e"},
|
||||
},
|
||||
expected: []Pipe{
|
||||
expected: []*Pipe{
|
||||
{fromPos: 0, toPos: 0, fromSha: "b", toSha: "c", kind: CONTINUES, style: style.FgDefault},
|
||||
{fromPos: 1, toPos: 1, fromSha: "b", toSha: "d", kind: TERMINATES, style: style.FgDefault},
|
||||
{fromPos: 1, toPos: 1, fromSha: "d", toSha: "e", kind: STARTS, style: style.FgDefault},
|
||||
@@ -551,3 +524,47 @@ func TestGetNextPipes(t *testing.T) {
|
||||
assert.EqualValues(t, test.expected, pipes)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkRenderCommitGraph(b *testing.B) {
|
||||
commits := generateCommits(50)
|
||||
getStyle := func(commit *models.Commit) style.TextStyle {
|
||||
return authors.AuthorStyle(commit.Author)
|
||||
}
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
RenderCommitGraph(commits, "selected", getStyle)
|
||||
}
|
||||
}
|
||||
|
||||
func generateCommits(count int) []*models.Commit {
|
||||
rand.Seed(1234)
|
||||
pool := []*models.Commit{{Sha: "a", Author: "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 := rand.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 := rand.Intn(2) + 1
|
||||
|
||||
for j := 0; j < parentCount; j++ {
|
||||
reuseParent := rand.Intn(6) != 1 && j <= len(pool)-1 && j != 0
|
||||
var newParent *models.Commit
|
||||
if reuseParent {
|
||||
newParent = pool[j]
|
||||
} else {
|
||||
newParent = &models.Commit{
|
||||
Sha: fmt.Sprintf("%s%d", currentCommit.Sha, j),
|
||||
Author: authorPool[rand.Intn(len(authorPool))],
|
||||
}
|
||||
pool = append(pool, newParent)
|
||||
}
|
||||
currentCommit.Parents = append(currentCommit.Parents, newParent.Sha)
|
||||
}
|
||||
|
||||
commits = append(commits, currentCommit)
|
||||
}
|
||||
|
||||
return commits
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user