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

> Explore how lazygit visualizes commit graphs by building pipe sets and parallelizing UTF-8 rendering for minimal UI latency. Understand the source code behind this feature.

- Repository: [Jesse Duffield/lazygit](https://github.com/jesseduffield/lazygit)
- Tags: deep-dive
- Published: 2026-03-02

---

**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`](https://github.com/jesseduffield/lazygit/blob/main/pkg/gui/presentation/graph/graph.go) and [`pkg/gui/presentation/commits.go`](https://github.com/jesseduffield/lazygit/blob/main/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`](https://github.com/jesseduffield/lazygit/blob/main/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:

```go
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

```go
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:

```go
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

```go
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`](https://github.com/jesseduffield/lazygit/blob/main/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:

```go
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`

```go
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`](https://github.com/jesseduffield/lazygit/blob/main/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`](https://github.com/jesseduffield/lazygit/blob/main/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.