How Lazygit Implements Commit Graph Visualization: A Deep Dive into the Source Code

Lazygit renders the "git log --graph" style visualization by constructing pipe sets that model commit connections, then parallelizing the UTF-8 character rendering across CPU cores to minimize UI latency.

The commit graph visualization in lazygit appears next to each commit line in the Commits view, replicating the branching and merging structure of your repository history. This feature is implemented entirely within the presentation layer, specifically across pkg/gui/presentation/graph/graph.go and pkg/gui/presentation/commits.go, separating the geometric modeling of connections from the actual character rendering.

The Two-Layer Architecture

Lazygit splits the commit graph visualization into two distinct phases: graph construction (building geometric pipe models) and graph rendering (converting those models into UTF-8 characters). Both phases live in the presentation package to keep the UI responsive while handling complex repository topologies.

Pipe and PipeKind Data Structures

At the core of the visualization is the Pipe struct, defined in pkg/gui/presentation/graph/graph.go (lines 17-32). Each pipe represents a single line segment—vertical, diagonal, or horizontal—connecting commits in the graph:

type PipeKind uint8

const (
    TERMINATES PipeKind = iota // pipe ends at this commit
    STARTS                     // pipe starts at this commit
    CONTINUES                  // pipe passes through
)

type Pipe struct {
    fromHash *string        // hash where the pipe originates
    toHash   *string        // hash where the pipe ends (parent)
    style    *style.TextStyle
    fromPos  int16          // column of the origin
    toPos    int16          // column of the destination
    kind     PipeKind
}

The PipeKind enumeration determines how a segment behaves at a specific commit: TERMINATES marks the end of a branch, STARTS indicates a new branch or merge source, and CONTINUES maintains a line through the current commit. The style field enables color-coding by author or branch, while fromPos and toPos manage horizontal positioning to prevent visual collisions.

Building Connection Maps with GetPipeSets

The GetPipeSets function (lines 48-70) transforms a slice of commits into a two-dimensional array of pipes. It iterates through the commit history, calling getNextPipes for each commit to:

  1. Filter terminated pipes from the previous row
  2. Detect existing connections between child commits and parent pipes
  3. Allocate column positions using takenSpots and traversedSpots sets to prevent overlapping lines
  4. Handle merge commits by creating additional STARTS pipes for every extra parent beyond the first
  5. Sort pipes by toPos then kind to ensure deterministic drawing order
func GetPipeSets(commits []*models.Commit, getStyle func(c *models.Commit) *style.TextStyle) [][]Pipe {
    // …initialisation…
    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 {
        pipes = getNextPipes(pipes, commit, getStyle)
        return pipes
    })
}

This function returns a slice where each element corresponds to a commit row, containing all pipes that intersect that row in the visualization.

Parallel Rendering for Performance

To maintain UI responsiveness when displaying hundreds of commits, lazygit parallelizes the rendering phase using Go's concurrency primitives.

RenderAux and Worker Goroutines

The RenderAux function (lines 72-106) splits the pipe sets into chunks based on runtime.GOMAXPROCS(0), distributing the work across available CPU cores:

func RenderAux(pipeSets [][]Pipe, commits []*models.Commit, selectedCommitHashPtr *string) []string {
    maxProcs := runtime.GOMAXPROCS(0)
    chunks := make([][]string, maxProcs)
    perProc := len(pipeSets) / maxProcs

    wg := sync.WaitGroup{}
    wg.Add(maxProcs)

    for i := range maxProcs {
        go func() {
            from := i * perProc
            to := (i + 1) * perProc
            if i == maxProcs-1 {
                to = len(pipeSets)
            }
            innerLines := []string{}
            for j, pipeSet := range pipeSets[from:to] {
                k := from + j
                var prev *models.Commit
                if k > 0 {
                    prev = commits[k-1]
                }
                line := renderPipeSet(pipeSet, selectedCommitHashPtr, prev)
                innerLines = append(innerLines, line)
            }
            chunks[i] = innerLines
            wg.Done()
        }()
    }
    wg.Wait()
    return lo.Flatten(chunks)
}

Each goroutine renders a contiguous slice of pipe sets using renderPipeSet, and the results are flattened into the final ordered slice of graph lines.

Converting Pipes to UTF-8 Characters

The renderPipeSet function (lines 75-124 and 150-176) converts geometric pipe data into displayable characters through a Cell-based approach:

  1. Calculate line width from the maximum column position (maxPos)
  2. Initialize cells as CONNECTION type with default styling
  3. Paint pipe geometry using helper methods: setUp, setDown for vertical segments; setLeft, setRight for horizontal segments; special handling for merge junctions (STARTS)
  4. Highlight selection by applying bold styling when a pipe's hash matches selectedCommitHashPtr
  5. Set terminal cells to COMMIT or MERGE types for the actual commit markers
func renderPipeSet(pipes []Pipe, selectedCommitHashPtr *string, prevCommit *models.Commit) string {
    // …calculate positions…
    cells := lo.Map(lo.Range(int(maxPos)+1), func(i int, _ int) *Cell {
        return &Cell{cellType: CONNECTION, style: &style.FgDefault}
    })
    // …render each pipe…
    // …highlight selected commit…
    // …write to string builder…
    return writer.String()
}

This approach allows lazygit to handle complex branching patterns—including octopus merges and crossed lines—while maintaining clean ASCII/UTF-8 output.

Integration with the Commits View

The commit graph visualization is orchestrated by pkg/gui/presentation/commits.go, which manages when and how the graph appears in the UI.

Caching and Memoization with loadPipesets

To avoid recomputing the entire graph topology on every refresh, lazygit implements a caching mechanism via loadPipesets (lines 46-66). The cache key combines the head hash and commit count, ensuring that checking out a different branch or adding commits invalidates the cache appropriately:

pipeSets := loadPipesets(commits[rebaseOffset:])

This function retrieves cached pipe sets if available, or triggers GetPipeSets to generate new ones, significantly improving performance when scrolling through large repositories.

Slicing and Injection for Display

When showGraph is enabled, the commits view performs four steps to integrate the visualization (lines 80-52):

  1. Retrieve pipe sets for the full commit list (accounting for rebase offset)
  2. Slice the visible window using pipeSetOffset and endIdx to render only visible commits
  3. Generate graph lines by calling graph.RenderAux with the selected commit hash for highlighting
  4. Inject into display via the getGraphLine closure passed to displayCommit
if showGraph {
    pipeSets := loadPipesets(commits[rebaseOffset:])
    graphPipeSets := pipeSets[pipeSetOffset:max(endIdx-rebaseOffset, 0)]
    graphLines := graph.RenderAux(graphPipeSets, commits[graphOffset:endIdx], selectedCommitHashPtr)
    getGraphLine = func(idx int) string {
        if idx >= graphOffset { return graphLines[idx-graphOffset] }
        return ""
    }
}

The resulting graphLine string becomes column 6 in the final commit display row, positioned immediately before the commit hash and message.

Summary

  • Lazygit models commit connections as Pipe structs with geometric positions (fromPos, toPos) and behavioral types (TERMINATES, STARTS, CONTINUES) defined in pkg/gui/presentation/graph/graph.go.
  • The GetPipeSets function builds the complete topology by iterating through commits, handling merges via collision detection with takenSpots and traversedSpots tracking sets.
  • Parallel rendering via RenderAux distributes UTF-8 character generation across all available CPU cores to maintain UI responsiveness when displaying large histories.
  • Caching by head hash and commit count in loadPipesets prevents redundant recalculation during common operations like scrolling or refreshing the commits view.
  • The Cell-based rendering approach converts geometric pipes into box-drawing characters while supporting color-coding and selection highlighting.

Frequently Asked Questions

How does lazygit handle merge commits in the graph visualization?

Lazygit detects merge commits by examining parent counts in the commit model. For each merge commit, getNextPipes creates additional STARTS pipes for every parent beyond the first, connecting them to existing continuation lines or allocating new column positions. The collision detection system ensures these merge lines don't overlap by tracking takenSpots during the pipe allocation phase in GetPipeSets.

Why does lazygit use parallel rendering for the commit graph?

The RenderAux function splits the rendering workload across runtime.GOMAXPROCS(0) goroutines because UTF-8 character generation for complex graphs involves significant geometric calculations and string building. Parallelizing this work prevents UI freezing when displaying repositories with hundreds of visible commits or complex branching topologies with many intersecting lines.

Where does lazygit cache the computed graph topology?

The graph topology is cached in the presentation layer via loadPipesets in pkg/gui/presentation/commits.go. The cache key combines the repository's current head hash with the total commit count, ensuring that operations like checking out a different branch or adding new commits automatically invalidate the cache while scrolling or filtering within the same commit list reuses the existing pipe sets.

What determines the horizontal spacing of branches in the lazygit graph?

Horizontal column positions (fromPos and toPos) are determined during the getNextPipes execution within GetPipeSets. The algorithm maintains sets of takenSpots and traversedSpots to track which columns are occupied by existing pipes, then allocates new positions for starting branches or terminating connections. This ensures that parallel branches receive distinct columns while maintaining compact spacing without overlapping lines.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →