Supercharge Git on Massive Repos with Git Commit-Graph: Smooth Log, Rebase, and Branch Checks

Git tutorial - IT technology blog
Git tutorial - IT technology blog

1. The Agony of a 100K-Commit Repository

About six months ago, our team decided to merge nearly 30 microservices into a single monorepo. Everything started smoothly enough. But after just eight months with 45 developers continuously pushing code, the repo rapidly surpassed 140,000 commits with over 300 branches active in parallel.

At that point, routine operations turned into an absolute nightmare:

  • Running git log --graph --oneline -n 20 took five painful seconds with laptop fans screaming, all just to render a handful of branch lines.
  • Running git branch --contains <commit_id> to verify whether code had landed on staging? Waiting 12 to 15 seconds became the daily norm.
  • The most infuriating part was rebasing. Git had to traverse deeply nested ancestry relationships across branches. Time and again, both dev machines and CI runners locked up entirely as CPU usage pegged at 100%.

Waiting longer for a CLI tool to respond than it takes to open a web browser constantly derailed everyone’s flow state.

2. Why Does Git Struggle as the Graph Blows Up?

Git structures data as a directed acyclic graph (DAG). Each commit points backward to one or more parent commits.

In small projects, this model runs blazingly fast. But once a repo scales to hundreds of thousands of nodes, two critical bottlenecks emerge:

  • Heavy Disk I/O: When you execute git log or git merge-base, Git must unpack individual commit objects from packfiles on disk. What’s worse, Git typically only needs two tiny pieces of data: the parent commit hashes and the timestamp. Decompressing tens of thousands of objects just to read those details wastes an enormous amount of resources.
  • Costly Graph Traversal: To determine whether commit A is an ancestor of commit B, Git must traverse backward from B through thousands of history layers. Without a precomputed index, BFS/DFS traversal algorithms are forced to blind-walk the entire commit tree, exhausting both disk I/O and CPU cycles.

3. Quick Fixes and Their Limitations

Before discovering the real fix, our team experimented with several standard workarounds, but each hit its own dead end:

Approach 1: Running git gc

git gc --prune=now --aggressive

This command packs loose objects into tighter packfiles. While the size of the .git directory shrank, Git still had to dissect every commit header to traverse the ancestry graph. Deep traversal performance barely improved at all.

Approach 2: Using Shallow Clones

git clone --depth 50 https://github.com/org/repo.git

This works reasonably well for CI runners because it only fetches the 50 most recent commits. On developer workstations, however, shallow clones introduce endless headaches: you can’t run git blame on older code, long-distance rebases fail, and unexpected errors frequently crop up during push operations.

Approach 3: Splitting the Repository Back Apart

Breaking the monorepo back down into smaller repositories sounded architecturally neat on paper. In practice, the refactoring effort would have dragged on for months, with high risks of disrupting entire CI/CD pipelines and cross-team workflows.

4. The Real Game-Changer: Enabling Git Commit-Graph

Starting with Git 2.18 (and significantly polished in 2.24+), Git introduced Commit-Graph. This is a single binary file stored at .git/objects/info/commit-graph.

This file pre-indexes the graph’s entire backbone: commit hashes, parent-child relationships, commit dates, and generation numbers. As a result, Git no longer needs to decompress individual objects. Instead, it queries a compact, lightning-fast binary lookup table.

Step 1: Enable the Configuration Flags

First, enable reading and writing the commit-graph on your machine:

git config --global core.commitGraph true
git config --global gc.writeCommitGraph true

Step 2: Generate the Commit-Graph File

Open your terminal inside the repository and run:

git commit-graph write --reachable --changed-paths

The --reachable flag captures all commits referenced by branches and tags. More importantly, the --changed-paths flag (available in Git 2.28+) bakes in a Bloom filter of modified paths. This speeds up file-filtered history commands like git log -- path/to/file.go by 4x to 8x.

Step 3: Benchmark the Difference

Try running the branch check command that previously made you pull your hair out:

time git branch --contains HEAD~5000

On our real-world 140,000-commit repository, execution time dropped dramatically from 14.2 seconds down to just 0.16 seconds. Running git log --graph rendered instantly without a stutter.

Step 4: Automate Updates with git maintenance

Manually re-running graph writes every day isn’t practical. Since Git 2.30, you can offload this task entirely to background automation:

# Register the repo for periodic maintenance
git maintenance register

# Start the background service
git maintenance start

Git will set up a cron job (or systemd timer on Linux, Task Scheduler on Windows). Every hour, it automatically generates incremental split commit-graph layers. Everything runs silently in the background, keeping your repo blazing fast without requiring any manual intervention.

Share: