Store fromHash/toHash in Pipe struct as pointers

Now that commit hashes are stored in a pool and referenced by pointer by the
commits, we can use those same pointers in the pipes.
This commit is contained in:
Stefan Haller 2025-04-15 14:57:40 +02:00
parent 8d834e2eab
commit 13c21365c0
7 changed files with 126 additions and 112 deletions

View file

@ -93,6 +93,10 @@ func (c *Commit) Hash() string {
return *c.hash return *c.hash
} }
func (c *Commit) HashPtr() *string {
return c.hash
}
func (c *Commit) ShortHash() string { func (c *Commit) ShortHash() string {
return utils.ShortHash(c.Hash()) return utils.ShortHash(c.Hash())
} }
@ -120,6 +124,10 @@ func (c *Commit) Parents() []string {
return lo.Map(c.parents, func(s *string, _ int) string { return *s }) return lo.Map(c.parents, func(s *string, _ int) string { return *s })
} }
func (c *Commit) ParentPtrs() []*string {
return c.parents
}
func (c *Commit) IsFirstCommit() bool { func (c *Commit) IsFirstCommit() bool {
return len(c.parents) == 0 return len(c.parents) == 0
} }

View file

@ -32,12 +32,12 @@ func NewLocalCommitsContext(c *ContextCommon) *LocalCommitsContext {
) )
getDisplayStrings := func(startIdx int, endIdx int) [][]string { getDisplayStrings := func(startIdx int, endIdx int) [][]string {
selectedCommitHash := "" var selectedCommitHashPtr *string
if c.Context().Current().GetKey() == LOCAL_COMMITS_CONTEXT_KEY { if c.Context().Current().GetKey() == LOCAL_COMMITS_CONTEXT_KEY {
selectedCommit := viewModel.GetSelected() selectedCommit := viewModel.GetSelected()
if selectedCommit != nil { if selectedCommit != nil {
selectedCommitHash = selectedCommit.Hash() selectedCommitHashPtr = selectedCommit.HashPtr()
} }
} }
@ -57,7 +57,7 @@ func NewLocalCommitsContext(c *ContextCommon) *LocalCommitsContext {
c.UserConfig().Gui.ShortTimeFormat, c.UserConfig().Gui.ShortTimeFormat,
time.Now(), time.Now(),
c.UserConfig().Git.ParseEmoji, c.UserConfig().Git.ParseEmoji,
selectedCommitHash, selectedCommitHashPtr,
startIdx, startIdx,
endIdx, endIdx,
shouldShowGraph(c), shouldShowGraph(c),

View file

@ -46,11 +46,11 @@ func NewSubCommitsContext(
return [][]string{} return [][]string{}
} }
selectedCommitHash := "" var selectedCommitHashPtr *string
if c.Context().Current().GetKey() == SUB_COMMITS_CONTEXT_KEY { if c.Context().Current().GetKey() == SUB_COMMITS_CONTEXT_KEY {
selectedCommit := viewModel.GetSelected() selectedCommit := viewModel.GetSelected()
if selectedCommit != nil { if selectedCommit != nil {
selectedCommitHash = selectedCommit.Hash() selectedCommitHashPtr = selectedCommit.HashPtr()
} }
} }
branches := []*models.Branch{} branches := []*models.Branch{}
@ -72,7 +72,7 @@ func NewSubCommitsContext(
c.UserConfig().Gui.ShortTimeFormat, c.UserConfig().Gui.ShortTimeFormat,
time.Now(), time.Now(),
c.UserConfig().Git.ParseEmoji, c.UserConfig().Git.ParseEmoji,
selectedCommitHash, selectedCommitHashPtr,
startIdx, startIdx,
endIdx, endIdx,
shouldShowGraph(c), shouldShowGraph(c),

View file

@ -51,7 +51,7 @@ func GetCommitListDisplayStrings(
shortTimeFormat string, shortTimeFormat string,
now time.Time, now time.Time,
parseEmoji bool, parseEmoji bool,
selectedCommitHash string, selectedCommitHashPtr *string,
startIdx int, startIdx int,
endIdx int, endIdx int,
showGraph bool, showGraph bool,
@ -102,7 +102,7 @@ func GetCommitListDisplayStrings(
graphLines := graph.RenderAux( graphLines := graph.RenderAux(
graphPipeSets, graphPipeSets,
graphCommits, graphCommits,
selectedCommitHash, selectedCommitHashPtr,
) )
allGraphLines = append(allGraphLines, graphLines...) allGraphLines = append(allGraphLines, graphLines...)
} }
@ -119,7 +119,7 @@ func GetCommitListDisplayStrings(
graphLines := graph.RenderAux( graphLines := graph.RenderAux(
graphPipeSets, graphPipeSets,
graphCommits, graphCommits,
selectedCommitHash, selectedCommitHashPtr,
) )
allGraphLines = append(allGraphLines, graphLines...) allGraphLines = append(allGraphLines, graphLines...)
} }
@ -140,7 +140,7 @@ func GetCommitListDisplayStrings(
graphLines := graph.RenderAux( graphLines := graph.RenderAux(
graphPipeSets, graphPipeSets,
graphCommits, graphCommits,
selectedCommitHash, selectedCommitHashPtr,
) )
getGraphLine = func(idx int) string { getGraphLine = func(idx int) string {
if idx >= graphOffset { if idx >= graphOffset {

View file

@ -36,7 +36,7 @@ func TestGetCommitListDisplayStrings(t *testing.T) {
shortTimeFormat string shortTimeFormat string
now time.Time now time.Time
parseEmoji bool parseEmoji bool
selectedCommitHash string selectedCommitHashPtr *string
startIdx int startIdx int
endIdx int endIdx int
showGraph bool showGraph bool
@ -563,7 +563,7 @@ func TestGetCommitListDisplayStrings(t *testing.T) {
s.shortTimeFormat, s.shortTimeFormat,
s.now, s.now,
s.parseEmoji, s.parseEmoji,
s.selectedCommitHash, s.selectedCommitHashPtr,
s.startIdx, s.startIdx,
s.endIdx, s.endIdx,
s.showGraph, s.showGraph,

View file

@ -25,13 +25,17 @@ const (
type Pipe struct { type Pipe struct {
fromPos int fromPos int
toPos int toPos int
fromHash string fromHash *string
toHash string toHash *string
kind PipeKind kind PipeKind
style style.TextStyle style style.TextStyle
} }
var highlightStyle = style.FgLightWhite.SetBold() var (
highlightStyle = style.FgLightWhite.SetBold()
EmptyTreeCommitHash = models.EmptyTreeCommitHash
StartCommitHash = "START"
)
func (self Pipe) left() int { func (self Pipe) left() int {
return min(self.fromPos, self.toPos) return min(self.fromPos, self.toPos)
@ -41,13 +45,13 @@ func (self Pipe) right() int {
return max(self.fromPos, self.toPos) return max(self.fromPos, self.toPos)
} }
func RenderCommitGraph(commits []*models.Commit, selectedCommitHash string, getStyle func(c *models.Commit) style.TextStyle) []string { func RenderCommitGraph(commits []*models.Commit, selectedCommitHashPtr *string, getStyle func(c *models.Commit) style.TextStyle) []string {
pipeSets := GetPipeSets(commits, getStyle) pipeSets := GetPipeSets(commits, getStyle)
if len(pipeSets) == 0 { if len(pipeSets) == 0 {
return nil return nil
} }
lines := RenderAux(pipeSets, commits, selectedCommitHash) lines := RenderAux(pipeSets, commits, selectedCommitHashPtr)
return lines return lines
} }
@ -57,7 +61,7 @@ func GetPipeSets(commits []*models.Commit, getStyle func(c *models.Commit) style
return nil return nil
} }
pipes := []*Pipe{{fromPos: 0, toPos: 0, fromHash: "START", toHash: commits[0].Hash(), kind: STARTS, style: style.FgDefault}} pipes := []*Pipe{{fromPos: 0, toPos: 0, fromHash: &StartCommitHash, toHash: commits[0].HashPtr(), kind: STARTS, style: style.FgDefault}}
return lo.Map(commits, func(commit *models.Commit, _ int) []*Pipe { return lo.Map(commits, func(commit *models.Commit, _ int) []*Pipe {
pipes = getNextPipes(pipes, commit, getStyle) pipes = getNextPipes(pipes, commit, getStyle)
@ -65,7 +69,7 @@ func GetPipeSets(commits []*models.Commit, getStyle func(c *models.Commit) style
}) })
} }
func RenderAux(pipeSets [][]*Pipe, commits []*models.Commit, selectedCommitHash string) []string { func RenderAux(pipeSets [][]*Pipe, commits []*models.Commit, selectedCommitHashPtr *string) []string {
maxProcs := runtime.GOMAXPROCS(0) maxProcs := runtime.GOMAXPROCS(0)
// splitting up the rendering of the graph into multiple goroutines allows us to render the graph in parallel // splitting up the rendering of the graph into multiple goroutines allows us to render the graph in parallel
@ -89,7 +93,7 @@ func RenderAux(pipeSets [][]*Pipe, commits []*models.Commit, selectedCommitHash
if k > 0 { if k > 0 {
prevCommit = commits[k-1] prevCommit = commits[k-1]
} }
line := renderPipeSet(pipeSet, selectedCommitHash, prevCommit) line := renderPipeSet(pipeSet, selectedCommitHashPtr, prevCommit)
innerLines = append(innerLines, line) innerLines = append(innerLines, line)
} }
chunks[i] = innerLines chunks[i] = innerLines
@ -116,12 +120,12 @@ func getNextPipes(prevPipes []*Pipe, commit *models.Commit, getStyle func(c *mod
return pipe.kind != TERMINATES return pipe.kind != TERMINATES
}) })
newPipes := make([]*Pipe, 0, len(currentPipes)+len(commit.Parents())) newPipes := make([]*Pipe, 0, len(currentPipes)+len(commit.ParentPtrs()))
// start by assuming that we've got a brand new commit not related to any preceding commit. // 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. // (this only happens when we're doing `git log --all`). These will be tacked onto the far end.
pos := maxPos + 1 pos := maxPos + 1
for _, pipe := range currentPipes { for _, pipe := range currentPipes {
if equalHashes(pipe.toHash, commit.Hash()) { if equalHashes(pipe.toHash, commit.HashPtr()) {
// turns out this commit does have a descendant so we'll place it right under the first instance // turns out this commit does have a descendant so we'll place it right under the first instance
pos = pipe.toPos pos = pipe.toPos
break break
@ -133,16 +137,16 @@ func getNextPipes(prevPipes []*Pipe, commit *models.Commit, getStyle func(c *mod
// a traversed spot is one where a current pipe is starting on, ending on, or passing through // a traversed spot is one where a current pipe is starting on, ending on, or passing through
traversedSpots := set.New[int]() traversedSpots := set.New[int]()
var toHash string var toHash *string
if commit.IsFirstCommit() { if commit.IsFirstCommit() {
toHash = models.EmptyTreeCommitHash toHash = &EmptyTreeCommitHash
} else { } else {
toHash = commit.Parents()[0] toHash = commit.ParentPtrs()[0]
} }
newPipes = append(newPipes, &Pipe{ newPipes = append(newPipes, &Pipe{
fromPos: pos, fromPos: pos,
toPos: pos, toPos: pos,
fromHash: commit.Hash(), fromHash: commit.HashPtr(),
toHash: toHash, toHash: toHash,
kind: STARTS, kind: STARTS,
style: getStyle(commit), style: getStyle(commit),
@ -150,7 +154,7 @@ func getNextPipes(prevPipes []*Pipe, commit *models.Commit, getStyle func(c *mod
traversedSpotsForContinuingPipes := set.New[int]() traversedSpotsForContinuingPipes := set.New[int]()
for _, pipe := range currentPipes { for _, pipe := range currentPipes {
if !equalHashes(pipe.toHash, commit.Hash()) { if !equalHashes(pipe.toHash, commit.HashPtr()) {
traversedSpotsForContinuingPipes.Add(pipe.toPos) traversedSpotsForContinuingPipes.Add(pipe.toPos)
} }
} }
@ -189,7 +193,7 @@ func getNextPipes(prevPipes []*Pipe, commit *models.Commit, getStyle func(c *mod
} }
for _, pipe := range currentPipes { for _, pipe := range currentPipes {
if equalHashes(pipe.toHash, commit.Hash()) { if equalHashes(pipe.toHash, commit.HashPtr()) {
// terminating here // terminating here
newPipes = append(newPipes, &Pipe{ newPipes = append(newPipes, &Pipe{
fromPos: pipe.toPos, fromPos: pipe.toPos,
@ -216,13 +220,13 @@ func getNextPipes(prevPipes []*Pipe, commit *models.Commit, getStyle func(c *mod
} }
if commit.IsMerge() { if commit.IsMerge() {
for _, parent := range commit.Parents()[1:] { for _, parent := range commit.ParentPtrs()[1:] {
availablePos := getNextAvailablePosForNewPipe() availablePos := getNextAvailablePosForNewPipe()
// need to act as if continuing pipes are going to continue on the same line. // 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, fromPos: pos,
toPos: availablePos, toPos: availablePos,
fromHash: commit.Hash(), fromHash: commit.HashPtr(),
toHash: parent, toHash: parent,
kind: STARTS, kind: STARTS,
style: getStyle(commit), style: getStyle(commit),
@ -233,7 +237,7 @@ func getNextPipes(prevPipes []*Pipe, commit *models.Commit, getStyle func(c *mod
} }
for _, pipe := range currentPipes { for _, pipe := range currentPipes {
if !equalHashes(pipe.toHash, commit.Hash()) && pipe.toPos > pos { if !equalHashes(pipe.toHash, commit.HashPtr()) && pipe.toPos > pos {
// continuing on, potentially moving left to fill in a blank spot // continuing on, potentially moving left to fill in a blank spot
last := pipe.toPos last := pipe.toPos
for i := pipe.toPos; i > pos; i-- { for i := pipe.toPos; i > pos; i-- {
@ -268,7 +272,7 @@ func getNextPipes(prevPipes []*Pipe, commit *models.Commit, getStyle func(c *mod
func renderPipeSet( func renderPipeSet(
pipes []*Pipe, pipes []*Pipe,
selectedCommitHash string, selectedCommitHashPtr *string,
prevCommit *models.Commit, prevCommit *models.Commit,
) string { ) string {
maxPos := 0 maxPos := 0
@ -315,10 +319,10 @@ func renderPipeSet(
// we don't want to highlight two commits if they're contiguous. We only want // 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. // to highlight multiple things if there's an actual visible pipe involved.
highlight := true highlight := true
if prevCommit != nil && equalHashes(prevCommit.Hash(), selectedCommitHash) { if prevCommit != nil && equalHashes(prevCommit.HashPtr(), selectedCommitHashPtr) {
highlight = false highlight = false
for _, pipe := range pipes { for _, pipe := range pipes {
if equalHashes(pipe.fromHash, selectedCommitHash) && (pipe.kind != TERMINATES || pipe.fromPos != pipe.toPos) { if equalHashes(pipe.fromHash, selectedCommitHashPtr) && (pipe.kind != TERMINATES || pipe.fromPos != pipe.toPos) {
highlight = true highlight = true
} }
} }
@ -327,7 +331,7 @@ func renderPipeSet(
// so we have our commit pos again, now it's time to build the cells. // 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. // we'll handle the one that's sourced from our selected commit last so that it can override the other cells.
selectedPipes, nonSelectedPipes := utils.Partition(pipes, func(pipe *Pipe) bool { selectedPipes, nonSelectedPipes := utils.Partition(pipes, func(pipe *Pipe) bool {
return highlight && equalHashes(pipe.fromHash, selectedCommitHash) return highlight && equalHashes(pipe.fromHash, selectedCommitHashPtr)
}) })
for _, pipe := range nonSelectedPipes { for _, pipe := range nonSelectedPipes {
@ -370,13 +374,13 @@ func renderPipeSet(
return writer.String() return writer.String()
} }
func equalHashes(a, b string) bool { func equalHashes(a, b *string) bool {
// if our selectedCommitHash is an empty string we treat that as meaning there is no selected commit hash // if our selectedCommitHashPtr is nil, there is no selected commit
if a == "" || b == "" { if a == nil || b == nil {
return false return false
} }
length := min(len(a), len(b)) length := 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 // 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] return (*a)[:length] == (*b)[:length]
} }

View file

@ -224,7 +224,7 @@ func TestRenderCommitGraph(t *testing.T) {
getStyle := func(c *models.Commit) style.TextStyle { return style.FgDefault } getStyle := func(c *models.Commit) style.TextStyle { return style.FgDefault }
commits := lo.Map(test.commitOpts, commits := lo.Map(test.commitOpts,
func(opts models.NewCommitOpts, _ int) *models.Commit { return models.NewCommit(hashPool, opts) }) func(opts models.NewCommitOpts, _ int) *models.Commit { return models.NewCommit(hashPool, opts) })
lines := RenderCommitGraph(commits, "blah", getStyle) lines := RenderCommitGraph(commits, hashPool.Add("blah"), getStyle)
trimmedExpectedOutput := "" trimmedExpectedOutput := ""
for _, line := range strings.Split(strings.TrimPrefix(test.expectedOutput, "\n"), "\n") { for _, line := range strings.Split(strings.TrimPrefix(test.expectedOutput, "\n"), "\n") {
@ -257,6 +257,7 @@ func TestRenderPipeSet(t *testing.T) {
nothing := style.Nothing nothing := style.Nothing
hashPool := &utils.StringPool{} hashPool := &utils.StringPool{}
pool := func(s string) *string { return hashPool.Add(s) }
tests := []struct { tests := []struct {
name string name string
@ -269,8 +270,8 @@ func TestRenderPipeSet(t *testing.T) {
{ {
name: "single cell", name: "single cell",
pipes: []*Pipe{ pipes: []*Pipe{
{fromPos: 0, toPos: 0, fromHash: "a", toHash: "b", kind: TERMINATES, style: cyan}, {fromPos: 0, toPos: 0, fromHash: pool("a"), toHash: pool("b"), kind: TERMINATES, style: cyan},
{fromPos: 0, toPos: 0, fromHash: "b", toHash: "c", kind: STARTS, style: green}, {fromPos: 0, toPos: 0, fromHash: pool("b"), toHash: pool("c"), kind: STARTS, style: green},
}, },
prevCommit: models.NewCommit(hashPool, models.NewCommitOpts{Hash: "a"}), prevCommit: models.NewCommit(hashPool, models.NewCommitOpts{Hash: "a"}),
expectedStr: "◯", expectedStr: "◯",
@ -279,8 +280,8 @@ func TestRenderPipeSet(t *testing.T) {
{ {
name: "single cell, selected", name: "single cell, selected",
pipes: []*Pipe{ pipes: []*Pipe{
{fromPos: 0, toPos: 0, fromHash: "a", toHash: "selected", kind: TERMINATES, style: cyan}, {fromPos: 0, toPos: 0, fromHash: pool("a"), toHash: pool("selected"), kind: TERMINATES, style: cyan},
{fromPos: 0, toPos: 0, fromHash: "selected", toHash: "c", kind: STARTS, style: green}, {fromPos: 0, toPos: 0, fromHash: pool("selected"), toHash: pool("c"), kind: STARTS, style: green},
}, },
prevCommit: models.NewCommit(hashPool, models.NewCommitOpts{Hash: "a"}), prevCommit: models.NewCommit(hashPool, models.NewCommitOpts{Hash: "a"}),
expectedStr: "◯", expectedStr: "◯",
@ -289,10 +290,10 @@ func TestRenderPipeSet(t *testing.T) {
{ {
name: "terminating hook and starting hook, selected", name: "terminating hook and starting hook, selected",
pipes: []*Pipe{ pipes: []*Pipe{
{fromPos: 0, toPos: 0, fromHash: "a", toHash: "selected", kind: TERMINATES, style: cyan}, {fromPos: 0, toPos: 0, fromHash: pool("a"), toHash: pool("selected"), kind: TERMINATES, style: cyan},
{fromPos: 1, toPos: 0, fromHash: "c", toHash: "selected", kind: TERMINATES, style: yellow}, {fromPos: 1, toPos: 0, fromHash: pool("c"), toHash: pool("selected"), kind: TERMINATES, style: yellow},
{fromPos: 0, toPos: 0, fromHash: "selected", toHash: "d", kind: STARTS, style: green}, {fromPos: 0, toPos: 0, fromHash: pool("selected"), toHash: pool("d"), kind: STARTS, style: green},
{fromPos: 0, toPos: 1, fromHash: "selected", toHash: "e", kind: STARTS, style: green}, {fromPos: 0, toPos: 1, fromHash: pool("selected"), toHash: pool("e"), kind: STARTS, style: green},
}, },
prevCommit: models.NewCommit(hashPool, models.NewCommitOpts{Hash: "a"}), prevCommit: models.NewCommit(hashPool, models.NewCommitOpts{Hash: "a"}),
expectedStr: "⏣─╮", expectedStr: "⏣─╮",
@ -303,10 +304,10 @@ func TestRenderPipeSet(t *testing.T) {
{ {
name: "terminating hook and starting hook, prioritise the terminating one", name: "terminating hook and starting hook, prioritise the terminating one",
pipes: []*Pipe{ pipes: []*Pipe{
{fromPos: 0, toPos: 0, fromHash: "a", toHash: "b", kind: TERMINATES, style: red}, {fromPos: 0, toPos: 0, fromHash: pool("a"), toHash: pool("b"), kind: TERMINATES, style: red},
{fromPos: 1, toPos: 0, fromHash: "c", toHash: "b", kind: TERMINATES, style: magenta}, {fromPos: 1, toPos: 0, fromHash: pool("c"), toHash: pool("b"), kind: TERMINATES, style: magenta},
{fromPos: 0, toPos: 0, fromHash: "b", toHash: "d", kind: STARTS, style: green}, {fromPos: 0, toPos: 0, fromHash: pool("b"), toHash: pool("d"), kind: STARTS, style: green},
{fromPos: 0, toPos: 1, fromHash: "b", toHash: "e", kind: STARTS, style: green}, {fromPos: 0, toPos: 1, fromHash: pool("b"), toHash: pool("e"), kind: STARTS, style: green},
}, },
prevCommit: models.NewCommit(hashPool, models.NewCommitOpts{Hash: "a"}), prevCommit: models.NewCommit(hashPool, models.NewCommitOpts{Hash: "a"}),
expectedStr: "⏣─│", expectedStr: "⏣─│",
@ -317,11 +318,11 @@ func TestRenderPipeSet(t *testing.T) {
{ {
name: "starting and terminating pipe sharing some space", name: "starting and terminating pipe sharing some space",
pipes: []*Pipe{ pipes: []*Pipe{
{fromPos: 0, toPos: 0, fromHash: "a1", toHash: "a2", kind: TERMINATES, style: red}, {fromPos: 0, toPos: 0, fromHash: pool("a1"), toHash: pool("a2"), kind: TERMINATES, style: red},
{fromPos: 0, toPos: 0, fromHash: "a2", toHash: "a3", kind: STARTS, style: yellow}, {fromPos: 0, toPos: 0, fromHash: pool("a2"), toHash: pool("a3"), kind: STARTS, style: yellow},
{fromPos: 1, toPos: 1, fromHash: "b1", toHash: "b2", kind: CONTINUES, style: magenta}, {fromPos: 1, toPos: 1, fromHash: pool("b1"), toHash: pool("b2"), kind: CONTINUES, style: magenta},
{fromPos: 3, toPos: 0, fromHash: "e1", toHash: "a2", kind: TERMINATES, style: green}, {fromPos: 3, toPos: 0, fromHash: pool("e1"), toHash: pool("a2"), kind: TERMINATES, style: green},
{fromPos: 0, toPos: 2, fromHash: "a2", toHash: "c3", kind: STARTS, style: yellow}, {fromPos: 0, toPos: 2, fromHash: pool("a2"), toHash: pool("c3"), kind: STARTS, style: yellow},
}, },
prevCommit: models.NewCommit(hashPool, models.NewCommitOpts{Hash: "a1"}), prevCommit: models.NewCommit(hashPool, models.NewCommitOpts{Hash: "a1"}),
expectedStr: "⏣─│─┬─╯", expectedStr: "⏣─│─┬─╯",
@ -332,11 +333,11 @@ func TestRenderPipeSet(t *testing.T) {
{ {
name: "starting and terminating pipe sharing some space, with selection", name: "starting and terminating pipe sharing some space, with selection",
pipes: []*Pipe{ pipes: []*Pipe{
{fromPos: 0, toPos: 0, fromHash: "a1", toHash: "selected", kind: TERMINATES, style: red}, {fromPos: 0, toPos: 0, fromHash: pool("a1"), toHash: pool("selected"), kind: TERMINATES, style: red},
{fromPos: 0, toPos: 0, fromHash: "selected", toHash: "a3", kind: STARTS, style: yellow}, {fromPos: 0, toPos: 0, fromHash: pool("selected"), toHash: pool("a3"), kind: STARTS, style: yellow},
{fromPos: 1, toPos: 1, fromHash: "b1", toHash: "b2", kind: CONTINUES, style: magenta}, {fromPos: 1, toPos: 1, fromHash: pool("b1"), toHash: pool("b2"), kind: CONTINUES, style: magenta},
{fromPos: 3, toPos: 0, fromHash: "e1", toHash: "selected", kind: TERMINATES, style: green}, {fromPos: 3, toPos: 0, fromHash: pool("e1"), toHash: pool("selected"), kind: TERMINATES, style: green},
{fromPos: 0, toPos: 2, fromHash: "selected", toHash: "c3", kind: STARTS, style: yellow}, {fromPos: 0, toPos: 2, fromHash: pool("selected"), toHash: pool("c3"), kind: STARTS, style: yellow},
}, },
prevCommit: models.NewCommit(hashPool, models.NewCommitOpts{Hash: "a1"}), prevCommit: models.NewCommit(hashPool, models.NewCommitOpts{Hash: "a1"}),
expectedStr: "⏣───╮ ╯", expectedStr: "⏣───╮ ╯",
@ -347,10 +348,10 @@ func TestRenderPipeSet(t *testing.T) {
{ {
name: "many terminating pipes", name: "many terminating pipes",
pipes: []*Pipe{ pipes: []*Pipe{
{fromPos: 0, toPos: 0, fromHash: "a1", toHash: "a2", kind: TERMINATES, style: red}, {fromPos: 0, toPos: 0, fromHash: pool("a1"), toHash: pool("a2"), kind: TERMINATES, style: red},
{fromPos: 0, toPos: 0, fromHash: "a2", toHash: "a3", kind: STARTS, style: yellow}, {fromPos: 0, toPos: 0, fromHash: pool("a2"), toHash: pool("a3"), kind: STARTS, style: yellow},
{fromPos: 1, toPos: 0, fromHash: "b1", toHash: "a2", kind: TERMINATES, style: magenta}, {fromPos: 1, toPos: 0, fromHash: pool("b1"), toHash: pool("a2"), kind: TERMINATES, style: magenta},
{fromPos: 2, toPos: 0, fromHash: "c1", toHash: "a2", kind: TERMINATES, style: green}, {fromPos: 2, toPos: 0, fromHash: pool("c1"), toHash: pool("a2"), kind: TERMINATES, style: green},
}, },
prevCommit: models.NewCommit(hashPool, models.NewCommitOpts{Hash: "a1"}), prevCommit: models.NewCommit(hashPool, models.NewCommitOpts{Hash: "a1"}),
expectedStr: "◯─┴─╯", expectedStr: "◯─┴─╯",
@ -361,11 +362,11 @@ func TestRenderPipeSet(t *testing.T) {
{ {
name: "starting pipe passing through", name: "starting pipe passing through",
pipes: []*Pipe{ pipes: []*Pipe{
{fromPos: 0, toPos: 0, fromHash: "a1", toHash: "a2", kind: TERMINATES, style: red}, {fromPos: 0, toPos: 0, fromHash: pool("a1"), toHash: pool("a2"), kind: TERMINATES, style: red},
{fromPos: 0, toPos: 0, fromHash: "a2", toHash: "a3", kind: STARTS, style: yellow}, {fromPos: 0, toPos: 0, fromHash: pool("a2"), toHash: pool("a3"), kind: STARTS, style: yellow},
{fromPos: 0, toPos: 3, fromHash: "a2", toHash: "d3", kind: STARTS, style: yellow}, {fromPos: 0, toPos: 3, fromHash: pool("a2"), toHash: pool("d3"), kind: STARTS, style: yellow},
{fromPos: 1, toPos: 1, fromHash: "b1", toHash: "b3", kind: CONTINUES, style: magenta}, {fromPos: 1, toPos: 1, fromHash: pool("b1"), toHash: pool("b3"), kind: CONTINUES, style: magenta},
{fromPos: 2, toPos: 2, fromHash: "c1", toHash: "c3", kind: CONTINUES, style: green}, {fromPos: 2, toPos: 2, fromHash: pool("c1"), toHash: pool("c3"), kind: CONTINUES, style: green},
}, },
prevCommit: models.NewCommit(hashPool, models.NewCommitOpts{Hash: "a1"}), prevCommit: models.NewCommit(hashPool, models.NewCommitOpts{Hash: "a1"}),
expectedStr: "⏣─│─│─╮", expectedStr: "⏣─│─│─╮",
@ -376,11 +377,11 @@ func TestRenderPipeSet(t *testing.T) {
{ {
name: "starting and terminating path crossing continuing path", name: "starting and terminating path crossing continuing path",
pipes: []*Pipe{ pipes: []*Pipe{
{fromPos: 0, toPos: 0, fromHash: "a1", toHash: "a2", kind: TERMINATES, style: red}, {fromPos: 0, toPos: 0, fromHash: pool("a1"), toHash: pool("a2"), kind: TERMINATES, style: red},
{fromPos: 0, toPos: 0, fromHash: "a2", toHash: "a3", kind: STARTS, style: yellow}, {fromPos: 0, toPos: 0, fromHash: pool("a2"), toHash: pool("a3"), kind: STARTS, style: yellow},
{fromPos: 0, toPos: 1, fromHash: "a2", toHash: "b3", kind: STARTS, style: yellow}, {fromPos: 0, toPos: 1, fromHash: pool("a2"), toHash: pool("b3"), kind: STARTS, style: yellow},
{fromPos: 1, toPos: 1, fromHash: "b1", toHash: "a2", kind: CONTINUES, style: green}, {fromPos: 1, toPos: 1, fromHash: pool("b1"), toHash: pool("a2"), kind: CONTINUES, style: green},
{fromPos: 2, toPos: 0, fromHash: "c1", toHash: "a2", kind: TERMINATES, style: magenta}, {fromPos: 2, toPos: 0, fromHash: pool("c1"), toHash: pool("a2"), kind: TERMINATES, style: magenta},
}, },
prevCommit: models.NewCommit(hashPool, models.NewCommitOpts{Hash: "a1"}), prevCommit: models.NewCommit(hashPool, models.NewCommitOpts{Hash: "a1"}),
expectedStr: "⏣─│─╯", expectedStr: "⏣─│─╯",
@ -391,11 +392,11 @@ func TestRenderPipeSet(t *testing.T) {
{ {
name: "another clash of starting and terminating paths", name: "another clash of starting and terminating paths",
pipes: []*Pipe{ pipes: []*Pipe{
{fromPos: 0, toPos: 0, fromHash: "a1", toHash: "a2", kind: TERMINATES, style: red}, {fromPos: 0, toPos: 0, fromHash: pool("a1"), toHash: pool("a2"), kind: TERMINATES, style: red},
{fromPos: 0, toPos: 0, fromHash: "a2", toHash: "a3", kind: STARTS, style: yellow}, {fromPos: 0, toPos: 0, fromHash: pool("a2"), toHash: pool("a3"), kind: STARTS, style: yellow},
{fromPos: 0, toPos: 1, fromHash: "a2", toHash: "b3", kind: STARTS, style: yellow}, {fromPos: 0, toPos: 1, fromHash: pool("a2"), toHash: pool("b3"), kind: STARTS, style: yellow},
{fromPos: 2, toPos: 2, fromHash: "c1", toHash: "c3", kind: CONTINUES, style: green}, {fromPos: 2, toPos: 2, fromHash: pool("c1"), toHash: pool("c3"), kind: CONTINUES, style: green},
{fromPos: 3, toPos: 0, fromHash: "d1", toHash: "a2", kind: TERMINATES, style: magenta}, {fromPos: 3, toPos: 0, fromHash: pool("d1"), toHash: pool("a2"), kind: TERMINATES, style: magenta},
}, },
prevCommit: models.NewCommit(hashPool, models.NewCommitOpts{Hash: "a1"}), prevCommit: models.NewCommit(hashPool, models.NewCommitOpts{Hash: "a1"}),
expectedStr: "⏣─┬─│─╯", expectedStr: "⏣─┬─│─╯",
@ -406,8 +407,8 @@ func TestRenderPipeSet(t *testing.T) {
{ {
name: "commit whose previous commit is selected", name: "commit whose previous commit is selected",
pipes: []*Pipe{ pipes: []*Pipe{
{fromPos: 0, toPos: 0, fromHash: "selected", toHash: "a2", kind: TERMINATES, style: red}, {fromPos: 0, toPos: 0, fromHash: pool("selected"), toHash: pool("a2"), kind: TERMINATES, style: red},
{fromPos: 0, toPos: 0, fromHash: "a2", toHash: "a3", kind: STARTS, style: yellow}, {fromPos: 0, toPos: 0, fromHash: pool("a2"), toHash: pool("a3"), kind: STARTS, style: yellow},
}, },
prevCommit: models.NewCommit(hashPool, models.NewCommitOpts{Hash: "selected"}), prevCommit: models.NewCommit(hashPool, models.NewCommitOpts{Hash: "selected"}),
expectedStr: "◯", expectedStr: "◯",
@ -418,8 +419,8 @@ func TestRenderPipeSet(t *testing.T) {
{ {
name: "commit whose previous commit is selected and is a merge commit", name: "commit whose previous commit is selected and is a merge commit",
pipes: []*Pipe{ pipes: []*Pipe{
{fromPos: 0, toPos: 0, fromHash: "selected", toHash: "a2", kind: TERMINATES, style: red}, {fromPos: 0, toPos: 0, fromHash: pool("selected"), toHash: pool("a2"), kind: TERMINATES, style: red},
{fromPos: 1, toPos: 1, fromHash: "selected", toHash: "b3", kind: CONTINUES, style: red}, {fromPos: 1, toPos: 1, fromHash: pool("selected"), toHash: pool("b3"), kind: CONTINUES, style: red},
}, },
prevCommit: models.NewCommit(hashPool, models.NewCommitOpts{Hash: "selected"}), prevCommit: models.NewCommit(hashPool, models.NewCommitOpts{Hash: "selected"}),
expectedStr: "◯ │", expectedStr: "◯ │",
@ -430,9 +431,9 @@ func TestRenderPipeSet(t *testing.T) {
{ {
name: "commit whose previous commit is selected and is a merge commit, with continuing pipe inbetween", name: "commit whose previous commit is selected and is a merge commit, with continuing pipe inbetween",
pipes: []*Pipe{ pipes: []*Pipe{
{fromPos: 0, toPos: 0, fromHash: "selected", toHash: "a2", kind: TERMINATES, style: red}, {fromPos: 0, toPos: 0, fromHash: pool("selected"), toHash: pool("a2"), kind: TERMINATES, style: red},
{fromPos: 1, toPos: 1, fromHash: "z1", toHash: "z3", kind: CONTINUES, style: green}, {fromPos: 1, toPos: 1, fromHash: pool("z1"), toHash: pool("z3"), kind: CONTINUES, style: green},
{fromPos: 2, toPos: 2, fromHash: "selected", toHash: "b3", kind: CONTINUES, style: red}, {fromPos: 2, toPos: 2, fromHash: pool("selected"), toHash: pool("b3"), kind: CONTINUES, style: red},
}, },
prevCommit: models.NewCommit(hashPool, models.NewCommitOpts{Hash: "selected"}), prevCommit: models.NewCommit(hashPool, models.NewCommitOpts{Hash: "selected"}),
expectedStr: "◯ │ │", expectedStr: "◯ │ │",
@ -443,10 +444,10 @@ func TestRenderPipeSet(t *testing.T) {
{ {
name: "when previous commit is selected, not a merge commit, and spawns a continuing pipe", name: "when previous commit is selected, not a merge commit, and spawns a continuing pipe",
pipes: []*Pipe{ pipes: []*Pipe{
{fromPos: 0, toPos: 0, fromHash: "a1", toHash: "a2", kind: TERMINATES, style: red}, {fromPos: 0, toPos: 0, fromHash: pool("a1"), toHash: pool("a2"), kind: TERMINATES, style: red},
{fromPos: 0, toPos: 0, fromHash: "a2", toHash: "a3", kind: STARTS, style: green}, {fromPos: 0, toPos: 0, fromHash: pool("a2"), toHash: pool("a3"), kind: STARTS, style: green},
{fromPos: 0, toPos: 1, fromHash: "a2", toHash: "b3", kind: STARTS, style: green}, {fromPos: 0, toPos: 1, fromHash: pool("a2"), toHash: pool("b3"), kind: STARTS, style: green},
{fromPos: 1, toPos: 0, fromHash: "selected", toHash: "a2", kind: TERMINATES, style: yellow}, {fromPos: 1, toPos: 0, fromHash: pool("selected"), toHash: pool("a2"), kind: TERMINATES, style: yellow},
}, },
prevCommit: models.NewCommit(hashPool, models.NewCommitOpts{Hash: "selected"}), prevCommit: models.NewCommit(hashPool, models.NewCommitOpts{Hash: "selected"}),
expectedStr: "⏣─╯", expectedStr: "⏣─╯",
@ -461,7 +462,7 @@ func TestRenderPipeSet(t *testing.T) {
for _, test := range tests { for _, test := range tests {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
actualStr := renderPipeSet(test.pipes, "selected", test.prevCommit) actualStr := renderPipeSet(test.pipes, pool("selected"), test.prevCommit)
t.Log("actual cells:") t.Log("actual cells:")
t.Log(actualStr) t.Log(actualStr)
expectedStr := "" expectedStr := ""
@ -482,6 +483,7 @@ func TestRenderPipeSet(t *testing.T) {
func TestGetNextPipes(t *testing.T) { func TestGetNextPipes(t *testing.T) {
hashPool := &utils.StringPool{} hashPool := &utils.StringPool{}
pool := func(s string) *string { return hashPool.Add(s) }
tests := []struct { tests := []struct {
prevPipes []*Pipe prevPipes []*Pipe
@ -490,43 +492,43 @@ func TestGetNextPipes(t *testing.T) {
}{ }{
{ {
prevPipes: []*Pipe{ prevPipes: []*Pipe{
{fromPos: 0, toPos: 0, fromHash: "a", toHash: "b", kind: STARTS, style: style.FgDefault}, {fromPos: 0, toPos: 0, fromHash: pool("a"), toHash: pool("b"), kind: STARTS, style: style.FgDefault},
}, },
commit: models.NewCommit(hashPool, models.NewCommitOpts{ commit: models.NewCommit(hashPool, models.NewCommitOpts{
Hash: "b", Hash: "b",
Parents: []string{"c"}, Parents: []string{"c"},
}), }),
expected: []*Pipe{ expected: []*Pipe{
{fromPos: 0, toPos: 0, fromHash: "a", toHash: "b", kind: TERMINATES, style: style.FgDefault}, {fromPos: 0, toPos: 0, fromHash: pool("a"), toHash: pool("b"), kind: TERMINATES, style: style.FgDefault},
{fromPos: 0, toPos: 0, fromHash: "b", toHash: "c", kind: STARTS, style: style.FgDefault}, {fromPos: 0, toPos: 0, fromHash: pool("b"), toHash: pool("c"), kind: STARTS, style: style.FgDefault},
}, },
}, },
{ {
prevPipes: []*Pipe{ prevPipes: []*Pipe{
{fromPos: 0, toPos: 0, fromHash: "a", toHash: "b", kind: TERMINATES, style: style.FgDefault}, {fromPos: 0, toPos: 0, fromHash: pool("a"), toHash: pool("b"), kind: TERMINATES, style: style.FgDefault},
{fromPos: 0, toPos: 0, fromHash: "b", toHash: "c", kind: STARTS, style: style.FgDefault}, {fromPos: 0, toPos: 0, fromHash: pool("b"), toHash: pool("c"), kind: STARTS, style: style.FgDefault},
{fromPos: 0, toPos: 1, fromHash: "b", toHash: "d", kind: STARTS, style: style.FgDefault}, {fromPos: 0, toPos: 1, fromHash: pool("b"), toHash: pool("d"), kind: STARTS, style: style.FgDefault},
}, },
commit: models.NewCommit(hashPool, models.NewCommitOpts{ commit: models.NewCommit(hashPool, models.NewCommitOpts{
Hash: "d", Hash: "d",
Parents: []string{"e"}, Parents: []string{"e"},
}), }),
expected: []*Pipe{ expected: []*Pipe{
{fromPos: 0, toPos: 0, fromHash: "b", toHash: "c", kind: CONTINUES, style: style.FgDefault}, {fromPos: 0, toPos: 0, fromHash: pool("b"), toHash: pool("c"), kind: CONTINUES, style: style.FgDefault},
{fromPos: 1, toPos: 1, fromHash: "b", toHash: "d", kind: TERMINATES, style: style.FgDefault}, {fromPos: 1, toPos: 1, fromHash: pool("b"), toHash: pool("d"), kind: TERMINATES, style: style.FgDefault},
{fromPos: 1, toPos: 1, fromHash: "d", toHash: "e", kind: STARTS, style: style.FgDefault}, {fromPos: 1, toPos: 1, fromHash: pool("d"), toHash: pool("e"), kind: STARTS, style: style.FgDefault},
}, },
}, },
{ {
prevPipes: []*Pipe{ prevPipes: []*Pipe{
{fromPos: 0, toPos: 0, fromHash: "a", toHash: "root", kind: TERMINATES, style: style.FgDefault}, {fromPos: 0, toPos: 0, fromHash: pool("a"), toHash: pool("root"), kind: TERMINATES, style: style.FgDefault},
}, },
commit: models.NewCommit(hashPool, models.NewCommitOpts{ commit: models.NewCommit(hashPool, models.NewCommitOpts{
Hash: "root", Hash: "root",
Parents: []string{}, Parents: []string{},
}), }),
expected: []*Pipe{ expected: []*Pipe{
{fromPos: 1, toPos: 1, fromHash: "root", toHash: models.EmptyTreeCommitHash, kind: STARTS, style: style.FgDefault}, {fromPos: 1, toPos: 1, fromHash: pool("root"), toHash: pool(models.EmptyTreeCommitHash), kind: STARTS, style: style.FgDefault},
}, },
}, },
} }
@ -538,8 +540,8 @@ func TestGetNextPipes(t *testing.T) {
getStyle := func(c *models.Commit) style.TextStyle { return style.FgDefault } getStyle := func(c *models.Commit) style.TextStyle { return style.FgDefault }
pipes := getNextPipes(test.prevPipes, test.commit, getStyle) pipes := getNextPipes(test.prevPipes, test.commit, getStyle)
// rendering cells so that it's easier to see what went wrong // rendering cells so that it's easier to see what went wrong
actualStr := renderPipeSet(pipes, "selected", nil) actualStr := renderPipeSet(pipes, pool("selected"), nil)
expectedStr := renderPipeSet(test.expected, "selected", nil) expectedStr := renderPipeSet(test.expected, pool("selected"), nil)
t.Log("expected cells:") t.Log("expected cells:")
t.Log(expectedStr) t.Log(expectedStr)
t.Log("actual cells:") t.Log("actual cells:")
@ -552,19 +554,19 @@ func BenchmarkRenderCommitGraph(b *testing.B) {
oldColorLevel := color.ForceSetColorLevel(terminfo.ColorLevelMillions) oldColorLevel := color.ForceSetColorLevel(terminfo.ColorLevelMillions)
defer color.ForceSetColorLevel(oldColorLevel) defer color.ForceSetColorLevel(oldColorLevel)
commits := generateCommits(50) hashPool := &utils.StringPool{}
commits := generateCommits(hashPool, 50)
getStyle := func(commit *models.Commit) style.TextStyle { getStyle := func(commit *models.Commit) style.TextStyle {
return authors.AuthorStyle(commit.AuthorName) return authors.AuthorStyle(commit.AuthorName)
} }
b.ResetTimer() b.ResetTimer()
for b.Loop() { for b.Loop() {
RenderCommitGraph(commits, "selected", getStyle) RenderCommitGraph(commits, hashPool.Add("selected"), getStyle)
} }
} }
func generateCommits(count int) []*models.Commit { func generateCommits(hashPool *utils.StringPool, count int) []*models.Commit {
hashPool := &utils.StringPool{}
rnd := rand.New(rand.NewSource(1234)) rnd := rand.New(rand.NewSource(1234))
pool := []*models.Commit{models.NewCommit(hashPool, models.NewCommitOpts{Hash: "a", AuthorName: "A"})} pool := []*models.Commit{models.NewCommit(hashPool, models.NewCommitOpts{Hash: "a", AuthorName: "A"})}
commits := make([]*models.Commit, 0, count) commits := make([]*models.Commit, 0, count)