summaryrefslogtreecommitdiffstats
path: root/pkg
diff options
context:
space:
mode:
authorJesse Duffield <jessedduffield@gmail.com>2021-11-02 16:39:15 +1100
committerJesse Duffield <jessedduffield@gmail.com>2021-11-05 07:58:21 +1100
commit802cfb1a0436568c72fc998249f10f8150b352a3 (patch)
tree599f8a8bd52b786312a11f3b3cac2a2d5b7c597e /pkg
parent2fc1498517523a20a3080816ec50ee9e7fbe533d (diff)
render commit graph
Diffstat (limited to 'pkg')
-rw-r--r--pkg/commands/loading_commits.go2
-rw-r--r--pkg/gui/commits_panel.go5
-rw-r--r--pkg/gui/gui.go2
-rw-r--r--pkg/gui/list_context.go11
-rw-r--r--pkg/gui/list_context_config.go22
-rw-r--r--pkg/gui/presentation/commits.go164
-rw-r--r--pkg/gui/presentation/graph/cell.go85
-rw-r--r--pkg/gui/presentation/graph/graph.go87
-rw-r--r--pkg/gui/presentation/graph/graph_test.go121
-rw-r--r--pkg/gui/pty.go18
-rw-r--r--pkg/gui/style/style_test.go22
-rw-r--r--pkg/gui/style/text_style.go22
-rw-r--r--pkg/gui/tasks_adapter.go24
-rw-r--r--pkg/gui/view_helpers.go5
-rw-r--r--pkg/tasks/tasks.go29
-rw-r--r--pkg/utils/color.go13
16 files changed, 410 insertions, 222 deletions
diff --git a/pkg/commands/loading_commits.go b/pkg/commands/loading_commits.go
index d22c803de..22f6db6d2 100644
--- a/pkg/commands/loading_commits.go
+++ b/pkg/commands/loading_commits.go
@@ -406,7 +406,7 @@ func (c *CommitListBuilder) getLogCmd(opts GetCommitsOptions) *exec.Cmd {
return c.OSCommand.ExecutableFromString(
fmt.Sprintf(
- "git log %s --oneline %s %s --abbrev=%d %s",
+ "git log --topo-order %s --oneline %s %s --abbrev=%d %s",
c.OSCommand.Quote(opts.RefName),
prettyFormat,
limitFlag,
diff --git a/pkg/gui/commits_panel.go b/pkg/gui/commits_panel.go
index 9c2914330..7185f1695 100644
--- a/pkg/gui/commits_panel.go
+++ b/pkg/gui/commits_panel.go
@@ -10,6 +10,9 @@ import (
"github.com/jesseduffield/lazygit/pkg/utils"
)
+// after selecting the 200th commit, we'll load in all the rest
+const COMMIT_THRESHOLD = 200
+
// list panel functions
func (gui *Gui) getSelectedLocalCommit() *models.Commit {
@@ -23,7 +26,7 @@ func (gui *Gui) getSelectedLocalCommit() *models.Commit {
func (gui *Gui) handleCommitSelect() error {
state := gui.State.Panels.Commits
- if state.SelectedLineIdx > 290 && state.LimitCommits {
+ if state.SelectedLineIdx > COMMIT_THRESHOLD && state.LimitCommits {
state.LimitCommits = false
go utils.Safe(func() {
if err := gui.refreshCommitsWithLimit(); err != nil {
diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go
index a07e1887b..76df52959 100644
--- a/pkg/gui/gui.go
+++ b/pkg/gui/gui.go
@@ -395,7 +395,7 @@ func (gui *Gui) resetState(filterPath string, reuseState bool) {
Remotes: &remotePanelState{listPanelState{SelectedLineIdx: 0}},
RemoteBranches: &remoteBranchesState{listPanelState{SelectedLineIdx: -1}},
Tags: &tagsPanelState{listPanelState{SelectedLineIdx: -1}},
- Commits: &commitPanelState{listPanelState: listPanelState{SelectedLineIdx: -1}, LimitCommits: true},
+ Commits: &commitPanelState{listPanelState: listPanelState{SelectedLineIdx: 0}, LimitCommits: true},
ReflogCommits: &reflogCommitPanelState{listPanelState{SelectedLineIdx: 0}},
SubCommits: &subCommitPanelState{listPanelState: listPanelState{SelectedLineIdx: 0}, refName: ""},
CommitFiles: &commitFilesPanelState{listPanelState: listPanelState{SelectedLineIdx: -1}, refName: ""},
diff --git a/pkg/gui/list_context.go b/pkg/gui/list_context.go
index c8ef850a0..290ac3a37 100644
--- a/pkg/gui/list_context.go
+++ b/pkg/gui/list_context.go
@@ -14,6 +14,10 @@ type ListContext struct {
// the boolean here tells us whether the item is nil. This is needed because you can't work it out on the calling end once the pointer is wrapped in an interface (unless you want to use reflection)
SelectedItem func() (ListItem, bool)
OnGetPanelState func() IListPanelState
+ // if this is true, we'll call GetDisplayStrings for just the visible part of the
+ // view and re-render that. This is useful when you need to render different
+ // content based on the selection (e.g. for showing the selected commit)
+ RenderSelection bool
Gui *Gui
@@ -60,10 +64,17 @@ type ListItem interface {
func (self *ListContext) FocusLine() {
view, err := self.Gui.g.View(self.ViewName)
if err != nil {
+ // ignoring error for now
return
}
+ // we need a way of knowing whether we've rendered to the view yet.
view.FocusPoint(0, self.GetPanelState().GetSelectedLineIdx())
+ if self.RenderSelection {
+ _, originY := view.Origin()
+ displayStrings := self.GetDisplayStrings(originY, view.InnerHeight())
+ self.Gui.renderDisplayStringsAtPos(view, originY, displayStrings)
+ }
view.Footer = formatListFooter(self.GetPanelState().GetSelectedLineIdx(), self.GetItemsLength())
}
diff --git a/pkg/gui/list_context_config.go b/pkg/gui/list_context_config.go
index 6bd5a63c1..e9e585b37 100644
--- a/pkg/gui/list_context_config.go
+++ b/pkg/gui/list_context_config.go
@@ -157,18 +157,29 @@ func (gui *Gui) branchCommitsListContext() IListContext {
OnClickSelectedItem: gui.handleViewCommitFiles,
Gui: gui,
GetDisplayStrings: func(startIdx int, length int) [][]string {
+ selectedCommitSha := ""
+ if gui.currentContext().GetKey() == BRANCH_COMMITS_CONTEXT_KEY {
+ selectedCommit := gui.getSelectedLocalCommit()
+ if selectedCommit != nil {
+ selectedCommitSha = selectedCommit.Sha
+ }
+ }
return presentation.GetCommitListDisplayStrings(
gui.State.Commits,
gui.State.ScreenMode != SCREEN_NORMAL,
gui.cherryPickedCommitShaMap(),
gui.State.Modes.Diffing.Ref,
parseEmoji,
+ selectedCommitSha,
+ startIdx,
+ length,
)
},
SelectedItem: func() (ListItem, bool) {
item := gui.getSelectedLocalCommit()
return item, item != nil
},
+ RenderSelection: true,
}
}
@@ -215,18 +226,29 @@ func (gui *Gui) subCommitsListContext() IListContext {
OnFocus: gui.handleSubCommitSelect,
Gui: gui,
GetDisplayStrings: func(startIdx int, length int) [][]string {
+ selectedCommitSha := ""
+ if gui.currentContext().GetKey() == SUB_COMMITS_CONTEXT_KEY {
+ selectedCommit := gui.getSelectedSubCommit()
+ if selectedCommit != nil {
+ selectedCommitSha = selectedCommit.Sha
+ }
+ }
return presentation.GetCommitListDisplayStrings(
gui.State.SubCommits,
gui.State.ScreenMode != SCREEN_NORMAL,
gui.cherryPickedCommitShaMap(),
gui.State.Modes.Diffing.Ref,
parseEmoji,
+ selectedCommitSha,
+ 0,
+ len(gui.State.SubCommits),
)
},
SelectedItem: func() (ListItem, bool) {
item := gui.getSelectedSubCommit()
return item, item != nil
},
+ RenderSelection: true,
}
}
diff --git a/pkg/gui/presentation/commits.go b/pkg/gui/presentation/commits.go
index 2474a283e..2a24437c9 100644
--- a/pkg/gui/presentation/commits.go
+++ b/pkg/gui/presentation/commits.go
@@ -2,81 +2,133 @@ 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
+ shaColor := getShaColor(commit, diffName, cherryPickedCommitShaMap)
+
+ actionString := ""
+ if commit.Action != "" {
+ actionString = actionColorMap(commit.Action).Sprint(commit.Action) + " "
}
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) + " "
+ 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 := c.Name
+ name := commit.Name
if parseEmoji {
name = emoji.Sprint(name)
}
- return []string{
- shaColor.Sprint(c.ShortSha()),
- secondColumnString,
- authors.LongAuthor(c.Author),
- 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 getDisplayStringsForCommit(c *models.Commit, cherryPickedCommitShaMap map[string]bool, diffed, parseEmoji bool) []string {
+func getShaColor(commit *models.Commit, diffName string, cherryPickedCommitShaMap map[string]bool) style.TextStyle {
+ diffed := commit.Sha == diffName
shaColor := theme.DefaultTextColor
- switch c.Status {
+ switch commit.Status {
case "unpushed":
shaColor = style.FgRed
case "pushed":
@@ -91,31 +143,11 @@ func getDisplayStringsForCommit(c *models.Commit, cherryPickedCommitShaMap map[s
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
+ } else if cherryPickedCommitShaMap[commit.Sha] {
shaColor = theme.CherryPickedCommitTextStyle
}
- 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, " ")) + " "
- }
-
- name := c.Name
- if parseEmoji {
- name = emoji.Sprint(name)
- }
-
- return []string{
- shaColor.Sprint(c.ShortSha()),
- authors.ShortAuthor(c.Author),
- actionString + tagString + theme.DefaultTextColor.Sprint(name),
- }
+ return shaColor
}
func actionColorMap(str string) style.TextStyle {
diff --git a/pkg/gui/presentation/graph/cell.go b/pkg/gui/presentation/graph/cell.go
index e4f57bf80..84ff722ba 100644
--- a/pkg/gui/presentation/graph/cell.go
+++ b/pkg/gui/presentation/graph/cell.go
@@ -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")
}
diff --git a/pkg/gui/presentation/graph/graph.go b/pkg/gui/presentation/graph/graph.go
index 16d7a8ab1..0e193cba8 100644
--- a/pkg/gui/presentation/graph/graph.go
+++ b/pkg/gui/presentation/graph/graph.go
@@ -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)
+ }
+ 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)
}
- line := renderPipeSet(pipeSet, selectedCommitSha, prevCommit)
- lines[i] = 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 {
diff --git a/pkg/gui/presentation/graph/graph_test.go b/pkg/gui/presentation/graph/graph_test.go
index 180f5f2b7..416409b9f 100644
--- a/pkg/gui/presentation/graph/graph_test.go
+++ b/pkg/gui/presentation/graph/graph_test.go
@@ -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 pas