mirror of
https://github.com/jesseduffield/lazygit.git
synced 2025-04-19 12:12:42 +02:00
add graph algorithm
This commit is contained in:
parent
927ee63106
commit
7a464ae5b7
@ -9,9 +9,11 @@ import (
|
|||||||
"github.com/xo/terminfo"
|
"github.com/xo/terminfo"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestRender(t *testing.T) {
|
func init() {
|
||||||
color.ForceSetColorLevel(terminfo.ColorLevelNone)
|
color.ForceSetColorLevel(terminfo.ColorLevelNone)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRender(t *testing.T) {
|
||||||
scenarios := []struct {
|
scenarios := []struct {
|
||||||
name string
|
name string
|
||||||
root *FileNode
|
root *FileNode
|
||||||
|
141
pkg/gui/presentation/graph/cell.go
Normal file
141
pkg/gui/presentation/graph/cell.go
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
package graph
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/gui/style"
|
||||||
|
)
|
||||||
|
|
||||||
|
const mergeSymbol = '⏣'
|
||||||
|
const commitSymbol = '⎔'
|
||||||
|
|
||||||
|
type cellType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
CONNECTION cellType = iota
|
||||||
|
COMMIT
|
||||||
|
MERGE
|
||||||
|
)
|
||||||
|
|
||||||
|
type Cell struct {
|
||||||
|
up, down, left, right bool
|
||||||
|
cellType cellType
|
||||||
|
rightStyle *style.TextStyle
|
||||||
|
style style.TextStyle
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cell *Cell) render() string {
|
||||||
|
up, down, left, right := cell.up, cell.down, cell.left, cell.right
|
||||||
|
|
||||||
|
first, second := getBoxDrawingChars(up, down, left, right)
|
||||||
|
var adjustedFirst rune
|
||||||
|
switch cell.cellType {
|
||||||
|
case CONNECTION:
|
||||||
|
adjustedFirst = first
|
||||||
|
case COMMIT:
|
||||||
|
adjustedFirst = commitSymbol
|
||||||
|
case MERGE:
|
||||||
|
adjustedFirst = mergeSymbol
|
||||||
|
}
|
||||||
|
|
||||||
|
var rightStyle *style.TextStyle
|
||||||
|
if cell.rightStyle == nil {
|
||||||
|
rightStyle = &cell.style
|
||||||
|
} else {
|
||||||
|
rightStyle = cell.rightStyle
|
||||||
|
}
|
||||||
|
|
||||||
|
// just doing this for the sake of easy testing, so that we don't need to
|
||||||
|
// 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 == ' ' {
|
||||||
|
styledSecondChar = " "
|
||||||
|
} else {
|
||||||
|
styledSecondChar = rightStyle.Sprint(string(second))
|
||||||
|
}
|
||||||
|
|
||||||
|
return cell.style.Sprint(string(adjustedFirst)) + styledSecondChar
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cell *Cell) reset() {
|
||||||
|
cell.up = false
|
||||||
|
cell.down = false
|
||||||
|
cell.left = false
|
||||||
|
cell.right = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cell *Cell) setUp(style style.TextStyle) *Cell {
|
||||||
|
cell.up = true
|
||||||
|
cell.style = style
|
||||||
|
return cell
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cell *Cell) setDown(style style.TextStyle) *Cell {
|
||||||
|
cell.down = true
|
||||||
|
cell.style = style
|
||||||
|
return cell
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cell *Cell) setLeft(style style.TextStyle) *Cell {
|
||||||
|
cell.left = true
|
||||||
|
if !cell.up && !cell.down {
|
||||||
|
// vertical trumps left
|
||||||
|
cell.style = style
|
||||||
|
}
|
||||||
|
return cell
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cell *Cell) setRight(style style.TextStyle, override bool) *Cell {
|
||||||
|
cell.right = true
|
||||||
|
if cell.rightStyle == nil || override {
|
||||||
|
cell.rightStyle = &style
|
||||||
|
}
|
||||||
|
return cell
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cell *Cell) setStyle(style style.TextStyle) *Cell {
|
||||||
|
cell.style = style
|
||||||
|
return cell
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cell *Cell) setType(cellType cellType) *Cell {
|
||||||
|
cell.cellType = cellType
|
||||||
|
return cell
|
||||||
|
}
|
||||||
|
|
||||||
|
func getBoxDrawingChars(up, down, left, right bool) (rune, rune) {
|
||||||
|
if up && down && left && right {
|
||||||
|
return '│', '─'
|
||||||
|
} else if up && down && left && !right {
|
||||||
|
return '│', ' '
|
||||||
|
} else if up && down && !left && right {
|
||||||
|
return '│', '─'
|
||||||
|
} else if up && down && !left && !right {
|
||||||
|
return '│', ' '
|
||||||
|
} else if up && !down && left && right {
|
||||||
|
return '┴', '─'
|
||||||
|
} else if up && !down && left && !right {
|
||||||
|
return '╯', ' '
|
||||||
|
} else if up && !down && !left && right {
|
||||||
|
return '╰', '─'
|
||||||
|
} else if up && !down && !left && !right {
|
||||||
|
return '╵', ' '
|
||||||
|
} else if !up && down && left && right {
|
||||||
|
return '┬', '─'
|
||||||
|
} else if !up && down && left && !right {
|
||||||
|
return '╮', ' '
|
||||||
|
} else if !up && down && !left && right {
|
||||||
|
return '╭', '─'
|
||||||
|
} else if !up && down && !left && !right {
|
||||||
|
return '╷', ' '
|
||||||
|
} else if !up && !down && left && right {
|
||||||
|
return '─', '─'
|
||||||
|
} else if !up && !down && left && !right {
|
||||||
|
return '─', ' '
|
||||||
|
} else if !up && !down && !left && right {
|
||||||
|
return '╶', '─'
|
||||||
|
} else if !up && !down && !left && !right {
|
||||||
|
return ' ', ' '
|
||||||
|
} else {
|
||||||
|
panic("should not be possible")
|
||||||
|
}
|
||||||
|
}
|
375
pkg/gui/presentation/graph/graph.go
Normal file
375
pkg/gui/presentation/graph/graph.go
Normal file
@ -0,0 +1,375 @@
|
|||||||
|
package graph
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/gui/style"
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PipeKind uint8
|
||||||
|
|
||||||
|
const (
|
||||||
|
TERMINATES PipeKind = iota
|
||||||
|
STARTS
|
||||||
|
CONTINUES
|
||||||
|
)
|
||||||
|
|
||||||
|
type Pipe struct {
|
||||||
|
fromPos int
|
||||||
|
toPos int
|
||||||
|
fromSha string
|
||||||
|
toSha string
|
||||||
|
kind PipeKind
|
||||||
|
style style.TextStyle
|
||||||
|
}
|
||||||
|
|
||||||
|
var highlightStyle = style.FgLightWhite.SetBold()
|
||||||
|
|
||||||
|
func ContainsCommitSha(pipes []Pipe, sha string) bool {
|
||||||
|
for _, pipe := range pipes {
|
||||||
|
if equalHashes(pipe.fromSha, sha) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self Pipe) left() int {
|
||||||
|
return utils.Min(self.fromPos, self.toPos)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self Pipe) right() int {
|
||||||
|
return utils.Max(self.fromPos, self.toPos)
|
||||||
|
}
|
||||||
|
|
||||||
|
func RenderCommitGraph(commits []*models.Commit, selectedCommitSha string, getStyle func(c *models.Commit) style.TextStyle) []string {
|
||||||
|
pipeSets := GetPipeSets(commits, getStyle)
|
||||||
|
if len(pipeSets) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
lines := RenderAux(pipeSets, commits, selectedCommitSha)
|
||||||
|
|
||||||
|
return lines
|
||||||
|
}
|
||||||
|
|
||||||
|
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}}
|
||||||
|
|
||||||
|
pipeSets := [][]Pipe{}
|
||||||
|
for _, commit := range commits {
|
||||||
|
pipes = getNextPipes(pipes, commit, getStyle)
|
||||||
|
pipeSets = append(pipeSets, pipes)
|
||||||
|
}
|
||||||
|
|
||||||
|
return pipeSets
|
||||||
|
}
|
||||||
|
|
||||||
|
func RenderAux(pipeSets [][]Pipe, commits []*models.Commit, selectedCommitSha string) []string {
|
||||||
|
lines := make([]string, len(pipeSets))
|
||||||
|
wg := sync.WaitGroup{}
|
||||||
|
wg.Add(len(pipeSets))
|
||||||
|
for i, pipeSet := range pipeSets {
|
||||||
|
i := i
|
||||||
|
pipeSet := pipeSet
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
var prevCommit *models.Commit
|
||||||
|
if i > 0 {
|
||||||
|
prevCommit = commits[i-1]
|
||||||
|
}
|
||||||
|
line := renderPipeSet(pipeSet, selectedCommitSha, prevCommit)
|
||||||
|
lines[i] = line
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
return lines
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
// so we'll filter those out
|
||||||
|
if pipe.kind != TERMINATES {
|
||||||
|
currentPipes = append(currentPipes, pipe)
|
||||||
|
}
|
||||||
|
maxPos = utils.Max(maxPos, pipe.toPos)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
for _, pipe := range currentPipes {
|
||||||
|
if equalHashes(pipe.toSha, commit.Sha) {
|
||||||
|
// turns out this commit does have a descendant so we'll place it right under the first instance
|
||||||
|
pos = pipe.toPos
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// a taken spot is one where a current pipe is ending on
|
||||||
|
takenSpots := make(map[int]bool)
|
||||||
|
// a traversed spot is one where a current pipe is starting on, ending on, or passing through
|
||||||
|
traversedSpots := make(map[int]bool)
|
||||||
|
|
||||||
|
if len(commit.Parents) > 0 {
|
||||||
|
newPipes = append(newPipes, Pipe{
|
||||||
|
fromPos: pos,
|
||||||
|
toPos: pos,
|
||||||
|
fromSha: commit.Sha,
|
||||||
|
toSha: commit.Parents[0],
|
||||||
|
kind: STARTS,
|
||||||
|
style: getStyle(commit),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
traversedSpotsForContinuingPipes := make(map[int]bool)
|
||||||
|
for _, pipe := range currentPipes {
|
||||||
|
if !equalHashes(pipe.toSha, commit.Sha) {
|
||||||
|
traversedSpotsForContinuingPipes[pipe.toPos] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getNextAvailablePosForContinuingPipe := func() int {
|
||||||
|
i := 0
|
||||||
|
for {
|
||||||
|
if !traversedSpots[i] {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getNextAvailablePosForNewPipe := func() int {
|
||||||
|
i := 0
|
||||||
|
for {
|
||||||
|
// a newly created pipe is not allowed to end on a spot that's already taken,
|
||||||
|
// nor on a spot that's been traversed by a continuing pipe.
|
||||||
|
if !takenSpots[i] && !traversedSpotsForContinuingPipes[i] {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
traverse := func(from, to int) {
|
||||||
|
left, right := from, to
|
||||||
|
if left > right {
|
||||||
|
left, right = right, left
|
||||||
|
}
|
||||||
|
for i := left; i <= right; i++ {
|
||||||
|
traversedSpots[i] = true
|
||||||
|
}
|
||||||
|
takenSpots[to] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, pipe := range currentPipes {
|
||||||
|
if equalHashes(pipe.toSha, commit.Sha) {
|
||||||
|
// terminating here
|
||||||
|
newPipes = append(newPipes, Pipe{
|
||||||
|
fromPos: pipe.toPos,
|
||||||
|
toPos: pos,
|
||||||
|
fromSha: pipe.fromSha,
|
||||||
|
toSha: pipe.toSha,
|
||||||
|
kind: TERMINATES,
|
||||||
|
style: pipe.style,
|
||||||
|
})
|
||||||
|
traverse(pipe.toPos, pos)
|
||||||
|
} else if pipe.toPos < pos {
|
||||||
|
// continuing here
|
||||||
|
availablePos := getNextAvailablePosForContinuingPipe()
|
||||||
|
newPipes = append(newPipes, Pipe{
|
||||||
|
fromPos: pipe.toPos,
|
||||||
|
toPos: availablePos,
|
||||||
|
fromSha: pipe.fromSha,
|
||||||
|
toSha: pipe.toSha,
|
||||||
|
kind: CONTINUES,
|
||||||
|
style: pipe.style,
|
||||||
|
})
|
||||||
|
traverse(pipe.toPos, availablePos)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if commit.IsMerge() {
|
||||||
|
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{
|
||||||
|
fromPos: pos,
|
||||||
|
toPos: availablePos,
|
||||||
|
fromSha: commit.Sha,
|
||||||
|
toSha: parent,
|
||||||
|
kind: STARTS,
|
||||||
|
style: getStyle(commit),
|
||||||
|
})
|
||||||
|
|
||||||
|
takenSpots[availablePos] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, pipe := range currentPipes {
|
||||||
|
if !equalHashes(pipe.toSha, commit.Sha) && pipe.toPos > pos {
|
||||||
|
// continuing on, potentially moving left to fill in a blank spot
|
||||||
|
last := pipe.toPos
|
||||||
|
for i := pipe.toPos; i > pos; i-- {
|
||||||
|
if takenSpots[i] || traversedSpots[i] {
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
last = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newPipes = append(newPipes, Pipe{
|
||||||
|
fromPos: pipe.toPos,
|
||||||
|
toPos: last,
|
||||||
|
fromSha: pipe.fromSha,
|
||||||
|
toSha: pipe.toSha,
|
||||||
|
kind: CONTINUES,
|
||||||
|
style: pipe.style,
|
||||||
|
})
|
||||||
|
traverse(pipe.toPos, last)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// not efficient but doing it for now: sorting my pipes by toPos, then by kind
|
||||||
|
sort.Slice(newPipes, func(i, j int) bool {
|
||||||
|
if newPipes[i].toPos == newPipes[j].toPos {
|
||||||
|
return newPipes[i].kind < newPipes[j].kind
|
||||||
|
}
|
||||||
|
return newPipes[i].toPos < newPipes[j].toPos
|
||||||
|
})
|
||||||
|
|
||||||
|
return newPipes
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderPipeSet(
|
||||||
|
pipes []Pipe,
|
||||||
|
selectedCommitSha string,
|
||||||
|
prevCommit *models.Commit,
|
||||||
|
) string {
|
||||||
|
maxPos := 0
|
||||||
|
commitPos := 0
|
||||||
|
startCount := 0
|
||||||
|
for _, pipe := range pipes {
|
||||||
|
if pipe.kind == STARTS {
|
||||||
|
startCount++
|
||||||
|
commitPos = pipe.fromPos
|
||||||
|
} else if pipe.kind == TERMINATES {
|
||||||
|
commitPos = pipe.toPos
|
||||||
|
}
|
||||||
|
|
||||||
|
if pipe.right() > maxPos {
|
||||||
|
maxPos = pipe.right()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
isMerge := startCount > 1
|
||||||
|
|
||||||
|
cells := make([]*Cell, maxPos+1)
|
||||||
|
for i := range cells {
|
||||||
|
cells[i] = &Cell{cellType: CONNECTION, style: style.FgDefault}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderPipe := func(pipe Pipe, style style.TextStyle, overrideRightStyle bool) {
|
||||||
|
left := pipe.left()
|
||||||
|
right := pipe.right()
|
||||||
|
|
||||||
|
if left != right {
|
||||||
|
for i := left + 1; i < right; i++ {
|
||||||
|
cells[i].setLeft(style).setRight(style, overrideRightStyle)
|
||||||
|
}
|
||||||
|
cells[left].setRight(style, overrideRightStyle)
|
||||||
|
cells[right].setLeft(style)
|
||||||
|
}
|
||||||
|
|
||||||
|
if pipe.kind == STARTS || pipe.kind == CONTINUES {
|
||||||
|
cells[pipe.toPos].setDown(style)
|
||||||
|
}
|
||||||
|
if pipe.kind == TERMINATES || pipe.kind == CONTINUES {
|
||||||
|
cells[pipe.fromPos].setUp(style)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// we don't want to highlight two commits if they're contiguous. We only want
|
||||||
|
// to highlight multiple things if there's an actual visible pipe involved.
|
||||||
|
highlight := true
|
||||||
|
if prevCommit != nil && equalHashes(prevCommit.Sha, selectedCommitSha) {
|
||||||
|
highlight = false
|
||||||
|
for _, pipe := range pipes {
|
||||||
|
if equalHashes(pipe.fromSha, selectedCommitSha) && (pipe.kind != TERMINATES || pipe.fromPos != pipe.toPos) {
|
||||||
|
highlight = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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{}
|
||||||
|
// pre-allocating this one because most of the time we'll only have non-selected pipes
|
||||||
|
nonSelectedPipes := make([]Pipe, 0, len(pipes))
|
||||||
|
|
||||||
|
for _, pipe := range pipes {
|
||||||
|
if highlight && equalHashes(pipe.fromSha, selectedCommitSha) {
|
||||||
|
selectedPipes = append(selectedPipes, pipe)
|
||||||
|
} else {
|
||||||
|
nonSelectedPipes = append(nonSelectedPipes, pipe)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, pipe := range nonSelectedPipes {
|
||||||
|
if pipe.kind == STARTS {
|
||||||
|
renderPipe(pipe, pipe.style, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, pipe := range nonSelectedPipes {
|
||||||
|
if pipe.kind != STARTS && !(pipe.kind == TERMINATES && pipe.fromPos == commitPos && pipe.toPos == commitPos) {
|
||||||
|
renderPipe(pipe, pipe.style, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, pipe := range selectedPipes {
|
||||||
|
for i := pipe.left(); i <= pipe.right(); i++ {
|
||||||
|
cells[i].reset()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, pipe := range selectedPipes {
|
||||||
|
renderPipe(pipe, highlightStyle, true)
|
||||||
|
if pipe.toPos == commitPos {
|
||||||
|
cells[pipe.toPos].setStyle(highlightStyle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cType := COMMIT
|
||||||
|
if isMerge {
|
||||||
|
cType = MERGE
|
||||||
|
}
|
||||||
|
|
||||||
|
cells[commitPos].setType(cType)
|
||||||
|
|
||||||
|
renderedCells := make([]string, len(cells))
|
||||||
|
for i, cell := range cells {
|
||||||
|
renderedCells[i] = cell.render()
|
||||||
|
}
|
||||||
|
return strings.Join(renderedCells, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func equalHashes(a, b string) bool {
|
||||||
|
// if our selectedCommitSha is an empty string we treat that as meaning there is no selected commit sha
|
||||||
|
if a == "" || b == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
length := utils.Min(len(a), len(b))
|
||||||
|
// parent hashes are only stored up to 20 characters for some reason so we'll truncate to that for comparison
|
||||||
|
return a[:length] == b[:length]
|
||||||
|
}
|
553
pkg/gui/presentation/graph/graph_test.go
Normal file
553
pkg/gui/presentation/graph/graph_test.go
Normal file
@ -0,0 +1,553 @@
|
|||||||
|
package graph
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/gookit/color"
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/gui/style"
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/xo/terminfo"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// on CI we've got no color capability so we're forcing it here
|
||||||
|
color.ForceSetColorLevel(terminfo.ColorLevelMillions)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRenderCommitGraph(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
commits []*models.Commit
|
||||||
|
expectedOutput string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "with some merges",
|
||||||
|
commits: []*models.Commit{
|
||||||
|
{Sha: "1", Parents: []string{"2"}},
|
||||||
|
{Sha: "2", Parents: []string{"3"}},
|
||||||
|
{Sha: "3", Parents: []string{"4"}},
|
||||||
|
{Sha: "4", Parents: []string{"5", "7"}},
|
||||||
|
{Sha: "7", Parents: []string{"5"}},
|
||||||
|
{Sha: "5", Parents: []string{"8"}},
|
||||||
|
{Sha: "8", Parents: []string{"9"}},
|
||||||
|
{Sha: "9", Parents: []string{"A", "B"}},
|
||||||
|
{Sha: "B", Parents: []string{"D"}},
|
||||||
|
{Sha: "D", Parents: []string{"D"}},
|
||||||
|
{Sha: "A", Parents: []string{"E"}},
|
||||||
|
{Sha: "E", Parents: []string{"F"}},
|
||||||
|
{Sha: "F", Parents: []string{"D"}},
|
||||||
|
{Sha: "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",
|
||||||
|
commits: []*models.Commit{
|
||||||
|
{Sha: "1", Parents: []string{"2"}},
|
||||||
|
{Sha: "2", Parents: []string{"3", "4"}},
|
||||||
|
{Sha: "4", Parents: []string{"3", "5"}},
|
||||||
|
{Sha: "3", Parents: []string{"5"}},
|
||||||
|
{Sha: "5", Parents: []string{"6"}},
|
||||||
|
{Sha: "6", Parents: []string{"7"}},
|
||||||
|
},
|
||||||
|
expectedOutput: `
|
||||||
|
1 ⎔
|
||||||
|
2 ⏣─╮
|
||||||
|
4 │ ⏣─╮
|
||||||
|
3 ⎔─╯ │
|
||||||
|
5 ⎔───╯
|
||||||
|
6 ⎔`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with a new commit",
|
||||||
|
commits: []*models.Commit{
|
||||||
|
{Sha: "1", Parents: []string{"2"}},
|
||||||
|
{Sha: "2", Parents: []string{"3", "4"}},
|
||||||
|
{Sha: "4", Parents: []string{"3", "5"}},
|
||||||
|
{Sha: "Z", Parents: []string{"Z"}},
|
||||||
|
{Sha: "3", Parents: []string{"5"}},
|
||||||
|
{Sha: "5", Parents: []string{"6"}},
|
||||||
|
{Sha: "6", Parents: []string{"7"}},
|
||||||
|
},
|
||||||
|
expectedOutput: `
|
||||||
|
1 ⎔
|
||||||
|
2 ⏣─╮
|
||||||
|
4 │ ⏣─╮
|
||||||
|
Z │ │ │ ⎔
|
||||||
|
3 ⎔─╯ │ │
|
||||||
|
5 ⎔───╯ │
|
||||||
|
6 ⎔ ╭───╯`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with a path that has room to move to the left and continues",
|
||||||
|
commits: []*models.Commit{
|
||||||
|
{Sha: "1", Parents: []string{"2"}},
|
||||||
|
{Sha: "2", Parents: []string{"3", "4"}},
|
||||||
|
{Sha: "3", Parents: []string{"5", "4"}},
|
||||||
|
{Sha: "5", Parents: []string{"7", "8"}},
|
||||||
|
{Sha: "4", Parents: []string{"7"}},
|
||||||
|
{Sha: "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",
|
||||||
|
commits: []*models.Commit{
|
||||||
|
{Sha: "1", Parents: []string{"2"}},
|
||||||
|
{Sha: "2", Parents: []string{"3", "4"}},
|
||||||
|
{Sha: "3", Parents: []string{"5", "4"}},
|
||||||
|
{Sha: "5", Parents: []string{"7", "8"}},
|
||||||
|
{Sha: "7", Parents: []string{"4", "A"}},
|
||||||
|
{Sha: "4", Parents: []string{"B"}},
|
||||||
|
{Sha: "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",
|
||||||
|
commits: []*models.Commit{
|
||||||
|
{Sha: "1", Parents: []string{"2", "3"}},
|
||||||
|
{Sha: "3", Parents: []string{"2"}},
|
||||||
|
{Sha: "2", Parents: []string{"4", "5"}},
|
||||||
|
{Sha: "4", Parents: []string{"6", "7"}},
|
||||||
|
{Sha: "6", Parents: []string{"8"}},
|
||||||
|
},
|
||||||
|
expectedOutput: `
|
||||||
|
1 ⏣─╮
|
||||||
|
3 │ ⎔
|
||||||
|
2 ⏣─│
|
||||||
|
4 ⏣─│─╮
|
||||||
|
6 ⎔ │ │`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "new merge path fills gap before continuing path on right",
|
||||||
|
commits: []*models.Commit{
|
||||||
|
{Sha: "1", Parents: []string{"2", "3", "4", "5"}},
|
||||||
|
{Sha: "4", Parents: []string{"2"}},
|
||||||
|
{Sha: "2", Parents: []string{"A"}},
|
||||||
|
{Sha: "A", Parents: []string{"6", "B"}},
|
||||||
|
{Sha: "B", Parents: []string{"C"}},
|
||||||
|
},
|
||||||
|
expectedOutput: `
|
||||||
|
1 ⏣─┬─┬─╮
|
||||||
|
4 │ │ ⎔ │
|
||||||
|
2 ⎔─│─╯ │
|
||||||
|
A ⏣─│─╮ │
|
||||||
|
B │ │ ⎔ │`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with a path that has room to move to the left and continues",
|
||||||
|
commits: []*models.Commit{
|
||||||
|
{Sha: "1", Parents: []string{"2"}},
|
||||||
|
{Sha: "2", Parents: []string{"3", "4"}},
|
||||||
|
{Sha: "3", Parents: []string{"5", "4"}},
|
||||||
|
{Sha: "5", Parents: []string{"7", "8"}},
|
||||||
|
{Sha: "7", Parents: []string{"4", "A"}},
|
||||||
|
{Sha: "4", Parents: []string{"B"}},
|
||||||
|
{Sha: "B", Parents: []string{"C"}},
|
||||||
|
{Sha: "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",
|
||||||
|
commits: []*models.Commit{
|
||||||
|
{Sha: "1", Parents: []string{"2"}},
|
||||||
|
{Sha: "2", Parents: []string{"3", "4"}},
|
||||||
|
{Sha: "3", Parents: []string{"5", "4"}},
|
||||||
|
{Sha: "5", Parents: []string{"7", "G"}},
|
||||||
|
{Sha: "7", Parents: []string{"8", "A"}},
|
||||||
|
{Sha: "8", Parents: []string{"4", "E"}},
|
||||||
|
{Sha: "4", Parents: []string{"B"}},
|
||||||
|
{Sha: "B", Parents: []string{"C"}},
|
||||||
|
{Sha: "C", Parents: []string{"D"}},
|
||||||
|
{Sha: "D", Parents: []string{"F"}},
|
||||||
|
},
|
||||||
|
expectedOutput: `
|
||||||
|
1 ⎔
|
||||||
|
2 ⏣─╮
|
||||||
|
3 ⏣─│─╮
|
||||||
|
5 ⏣─│─│─╮
|
||||||
|
7 ⏣─│─│─│─╮
|
||||||
|
8 ⏣─│─│─│─│─╮
|
||||||
|
4 ⎔─┴─╯ │ │ │
|
||||||
|
B ⎔ ╭───╯ │ │
|
||||||
|
C ⎔ │ ╭───╯ │
|
||||||
|
D ⎔ │ │ ╭───╯`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
test := test
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
getStyle := func(c *models.Commit) style.TextStyle { return style.FgDefault }
|
||||||
|
lines := RenderCommitGraph(test.commits, "blah", getStyle)
|
||||||
|
|
||||||
|
trimmedExpectedOutput := ""
|
||||||
|
for _, line := range strings.Split(strings.TrimPrefix(test.expectedOutput, "\n"), "\n") {
|
||||||
|
trimmedExpectedOutput += strings.TrimSpace(line) + "\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Log("\nexpected: \n" + trimmedExpectedOutput)
|
||||||
|
|
||||||
|
output := ""
|
||||||
|
for i, line := range lines {
|
||||||
|
description := test.commits[i].Sha
|
||||||
|
output += strings.TrimSpace(description+" "+utils.Decolorise(line)) + "\n"
|
||||||
|
}
|
||||||
|
t.Log("\nactual: \n" + output)
|
||||||
|
|
||||||
|
assert.Equal(t,
|
||||||
|
trimmedExpectedOutput,
|
||||||
|
output)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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, fromSha: "a", toSha: "b", kind: TERMINATES, style: cyan},
|
||||||
|
{fromPos: 0, toPos: 0, fromSha: "b", toSha: "c", kind: STARTS, style: green},
|
||||||
|
},
|
||||||
|
prevCommit: &models.Commit{Sha: "a"},
|
||||||
|
expectedStr: "⎔",
|
||||||
|
expectedStyles: []style.TextStyle{green},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "single cell, selected",
|
||||||
|
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},
|
||||||
|
},
|
||||||
|
prevCommit: &models.Commit{Sha: "a"},
|
||||||
|
expectedStr: "⎔",
|
||||||
|
expectedStyles: []style.TextStyle{highlightStyle},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "terminating hook and starting hook, selected",
|
||||||
|
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},
|
||||||
|
{fromPos: 0, toPos: 1, fromSha: "selected", toSha: "e", kind: STARTS, style: green},
|
||||||
|
},
|
||||||
|
prevCommit: &models.Commit{Sha: "a"},
|
||||||
|
expectedStr: "⏣─╮",
|
||||||
|
expectedStyles: []style.TextStyle{
|
||||||
|
highlightStyle, highlightStyle, highlightStyle,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "terminating hook and starting hook, prioritise the starting 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},
|
||||||
|
{fromPos: 0, toPos: 1, fromSha: "b", toSha: "e", kind: STARTS, style: green},
|
||||||
|
},
|
||||||
|
prevCommit: &models.Commit{Sha: "a"},
|
||||||
|
expectedStr: "⏣─│",
|
||||||
|
expectedStyles: []style.TextStyle{
|
||||||
|
green, green, magenta,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "starting and terminating pipe sharing some space",
|
||||||
|
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},
|
||||||
|
{fromPos: 3, toPos: 0, fromSha: "e1", toSha: "a2", kind: TERMINATES, style: green},
|
||||||
|
{fromPos: 0, toPos: 2, fromSha: "a2", toSha: "c3", kind: STARTS, style: yellow},
|
||||||
|
},
|
||||||
|
prevCommit: &models.Commit{Sha: "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, 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},
|
||||||
|
{fromPos: 3, toPos: 0, fromSha: "e1", toSha: "selected", kind: TERMINATES, style: green},
|
||||||
|
{fromPos: 0, toPos: 2, fromSha: "selected", toSha: "c3", kind: STARTS, style: yellow},
|
||||||
|
},
|
||||||
|
prevCommit: &models.Commit{Sha: "a1"},
|
||||||
|
expectedStr: "⏣───╮ ╯",
|
||||||
|
expectedStyles: []style.TextStyle{
|
||||||
|
highlightStyle, highlightStyle, highlightStyle, highlightStyle, highlightStyle, nothing, green,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "many terminating pipes",
|
||||||
|
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},
|
||||||
|
{fromPos: 2, toPos: 0, fromSha: "c1", toSha: "a2", kind: TERMINATES, style: green},
|
||||||
|
},
|
||||||
|
prevCommit: &models.Commit{Sha: "a1"},
|
||||||
|
expectedStr: "⎔─┴─╯",
|
||||||
|
expectedStyles: []style.TextStyle{
|
||||||
|
yellow, magenta, magenta, green, green,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "starting pipe passing through",
|
||||||
|
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},
|
||||||
|
{fromPos: 1, toPos: 1, fromSha: "b1", toSha: "b3", kind: CONTINUES, style: magenta},
|
||||||
|
{fromPos: 2, toPos: 2, fromSha: "c1", toSha: "c3", kind: CONTINUES, style: green},
|
||||||
|
},
|
||||||
|
prevCommit: &models.Commit{Sha: "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, 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},
|
||||||
|
{fromPos: 1, toPos: 1, fromSha: "b1", toSha: "a2", kind: CONTINUES, style: green},
|
||||||
|
{fromPos: 2, toPos: 0, fromSha: "c1", toSha: "a2", kind: TERMINATES, style: magenta},
|
||||||
|
},
|
||||||
|
prevCommit: &models.Commit{Sha: "a1"},
|
||||||
|
expectedStr: "⏣─│─╯",
|
||||||
|
expectedStyles: []style.TextStyle{
|
||||||
|
yellow, yellow, green, magenta, magenta,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "another clash of starting and terminating paths",
|
||||||
|
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},
|
||||||
|
{fromPos: 2, toPos: 2, fromSha: "c1", toSha: "c3", kind: CONTINUES, style: green},
|
||||||
|
{fromPos: 3, toPos: 0, fromSha: "d1", toSha: "a2", kind: TERMINATES, style: magenta},
|
||||||
|
},
|
||||||
|
prevCommit: &models.Commit{Sha: "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, fromSha: "selected", toSha: "a2", kind: TERMINATES, style: red},
|
||||||
|
{fromPos: 0, toPos: 0, fromSha: "a2", toSha: "a3", kind: STARTS, style: yellow},
|
||||||
|
},
|
||||||
|
prevCommit: &models.Commit{Sha: "selected"},
|
||||||
|
expectedStr: "⎔",
|
||||||
|
expectedStyles: []style.TextStyle{
|
||||||
|
yellow,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "commit whose previous commit is selected and is a merge commit",
|
||||||
|
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},
|
||||||
|
},
|
||||||
|
prevCommit: &models.Commit{Sha: "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, 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},
|
||||||
|
},
|
||||||
|
prevCommit: &models.Commit{Sha: "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, 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},
|
||||||
|
{fromPos: 1, toPos: 0, fromSha: "selected", toSha: "a2", kind: TERMINATES, style: yellow},
|
||||||
|
},
|
||||||
|
prevCommit: &models.Commit{Sha: "selected"},
|
||||||
|
expectedStr: "⏣─╯",
|
||||||
|
expectedStyles: []style.TextStyle{
|
||||||
|
highlightStyle, highlightStyle, highlightStyle,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
test := test
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
actualStr := renderPipeSet(test.pipes, "selected", test.prevCommit)
|
||||||
|
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 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
|
||||||
|
commit *models.Commit
|
||||||
|
expected []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{
|
||||||
|
{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{
|
||||||
|
{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},
|
||||||
|
},
|
||||||
|
commit: &models.Commit{
|
||||||
|
Sha: "d",
|
||||||
|
Parents: []string{"e"},
|
||||||
|
},
|
||||||
|
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},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
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, "selected", nil)
|
||||||
|
expectedStr := renderPipeSet(test.expected, "selected", nil)
|
||||||
|
t.Log("expected cells:")
|
||||||
|
t.Log(expectedStr)
|
||||||
|
t.Log("actual cells:")
|
||||||
|
t.Log(actualStr)
|
||||||
|
assert.EqualValues(t, test.expected, pipes)
|
||||||
|
}
|
||||||
|
}
|
@ -1,8 +1,9 @@
|
|||||||
package style
|
package style
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/gookit/color"
|
|
||||||
"text/template"
|
"text/template"
|
||||||
|
|
||||||
|
"github.com/gookit/color"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -16,6 +17,7 @@ var (
|
|||||||
FgBlue = FromBasicFg(color.FgBlue)
|
FgBlue = FromBasicFg(color.FgBlue)
|
||||||
FgYellow = FromBasicFg(color.FgYellow)
|
FgYellow = FromBasicFg(color.FgYellow)
|
||||||
FgMagenta = FromBasicFg(color.FgMagenta)
|
FgMagenta = FromBasicFg(color.FgMagenta)
|
||||||
|
FgDefault = FromBasicFg(color.FgDefault)
|
||||||
|
|
||||||
BgWhite = FromBasicBg(color.BgWhite)
|
BgWhite = FromBasicBg(color.BgWhite)
|
||||||
BgBlack = FromBasicBg(color.BgBlack)
|
BgBlack = FromBasicBg(color.BgBlack)
|
||||||
@ -26,6 +28,9 @@ var (
|
|||||||
BgMagenta = FromBasicBg(color.BgMagenta)
|
BgMagenta = FromBasicBg(color.BgMagenta)
|
||||||
BgCyan = FromBasicBg(color.BgCyan)
|
BgCyan = FromBasicBg(color.BgCyan)
|
||||||
|
|
||||||
|
// will not print any colour escape codes, including the reset escape code
|
||||||
|
Nothing = New()
|
||||||
|
|
||||||
AttrUnderline = New().SetUnderline()
|
AttrUnderline = New().SetUnderline()
|
||||||
AttrBold = New().SetBold()
|
AttrBold = New().SetBold()
|
||||||
|
|
||||||
|
@ -10,6 +10,11 @@ import (
|
|||||||
"github.com/xo/terminfo"
|
"github.com/xo/terminfo"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// on CI we've got no color capability so we're forcing it here
|
||||||
|
color.ForceSetColorLevel(terminfo.ColorLevelMillions)
|
||||||
|
}
|
||||||
|
|
||||||
func TestMerge(t *testing.T) {
|
func TestMerge(t *testing.T) {
|
||||||
type scenario struct {
|
type scenario struct {
|
||||||
name string
|
name string
|
||||||
@ -18,9 +23,6 @@ func TestMerge(t *testing.T) {
|
|||||||
expectedStr string
|
expectedStr string
|
||||||
}
|
}
|
||||||
|
|
||||||
// on CI we've got no color capability so we're forcing it here
|
|
||||||
color.ForceSetColorLevel(terminfo.ColorLevelMillions)
|
|
||||||
|
|
||||||
fgRed := color.FgRed
|
fgRed := color.FgRed
|
||||||
bgRed := color.BgRed
|
bgRed := color.BgRed
|
||||||
fgBlue := color.FgBlue
|
fgBlue := color.FgBlue
|
||||||
|
Loading…
x
Reference in New Issue
Block a user