High Performance Git

Section V ยท Diagnosis and Recovery

Chapter 20

Configuration Playbook

Pencil sketch of two people fixing a small boat together.

After a few tuning passes, a large repository tends to accumulate the same config lines:

The names are easy. The harder part is remembering which ones are local accelerators, which ones are workflow policy changes, and which ones only make sense if some other background system is already running.


Read the Current State Before You Tune It

Most configuration mistakes are not bad values. They are values whose origin nobody remembers.

Start by reading the current posture with origin information attached:

git config --show-origin --get-regexp '^(core\.fsmonitor|core\.untrackedCache|core\.splitIndex|feature\.manyFiles|index\.sparse|maintenance\.strategy|fetch\.writeCommitGraph|status\.aheadBehind|protocol\.version|fetch\.negotiationAlgorithm|remote\..*\.(promisor|partialclonefilter)|extensions\.worktreeConfig)$' || true

Git config has layers:

If a setting is coming from a global profile, a template directory, or a worktree-local file, that changes how you reason about the next command.

core.fsmonitor Cuts Tracked-Path Refresh Cost

Fsmonitor
Integration that lets Git learn which paths changed from a filesystem monitor instead of probing every tracked path itself.

core.fsmonitor=true tells Git to ask a filesystem monitor which tracked paths changed instead of probing the whole tracked set with ordinary stat calls.

The basic lifecycle is simple:

git config --get core.fsmonitor
git config core.fsmonitor true
git config --unset core.fsmonitor

Use it when repeated status, add, and branch switches are paying for tracked-path refresh work across a large checkout.

Leave it off when:

That last point matters. Git 2.35.1 and older misread boolean core.fsmonitor values as hook pathnames. If an IDE, wrapper, or older system Git still touches the checkout, the "fast path" can turn into incorrect or confusing behavior quickly.

core.untrackedCache Avoids Re-Scanning Quiet Directories

Untracked Cache
Index extension that caches directory mtimes to avoid re-scanning unchanged directories when looking for untracked files.

core.untrackedCache=true tells Git to cache directory mtimes so it can avoid repeating full untracked scans in directories that did not change.

git config --get core.untrackedCache
git config core.untrackedCache true
git config core.untrackedCache false

Use it when untracked discovery is the visible cost, especially in repositories with many build outputs, generated trees, or broad working directories.

The tradeoff is narrower than fsmonitor's. Check that mtime behaves properly on the system before enabling it. If directory mtimes are unreliable because of the filesystem or surrounding tools, the cache is not worth the ambiguity.

core.splitIndex Shrinks Repeated Index Rewrites

Split Index
Mode that stores a stable shared index plus a smaller mutable overlay to reduce repeated full-index rewrites.

core.splitIndex=true makes Git keep a stable shared index plus a smaller mutable overlay. It helps when the index is large and Git keeps rewriting it for relatively small changes.

git config --get core.splitIndex
git config core.splitIndex true
git config core.splitIndex false

Use it when:

Do not expect it to solve slow untracked discovery or slow history walks. Split index is about index write volume, not the rest of Git's cost model.

feature.manyFiles Is a Bundle, Not a Neutral Toggle

feature.manyFiles=true is one of the few high-level convenience knobs in Git that is worth discussing directly because it enables several lower-level settings together.

git config --get feature.manyFiles
git config feature.manyFiles true
git config --unset feature.manyFiles

It implies:

That is why it helps. This is a bundle aimed at repositories with large working trees, not one isolated optimization.

That also makes it less than neutral. index.skipHash makes Git clients older than 2.13.0 refuse to parse the index, and clients older than 2.40.0 report an error during git fsck.

Use it when every Git touching the checkout is recent enough and the local working tree is simply huge.

Leave it off when the toolchain is mixed or the repository gets touched by older IDE integrations, CI images, or vendor-packaged Git builds you do not fully control.

index.sparse Makes the Index Match a Narrow Working Tree

index.sparse=true lets Git write sparse-directory entries into the index so the index shape better matches a sparse working tree.

git config --get index.sparse
git sparse-checkout set --sparse-index src docs
git sparse-checkout reapply --no-sparse-index

index.sparse has no effect unless core.sparseCheckout and core.sparseCheckoutCone are both enabled. In practice, that means you usually turn it on through git sparse-checkout set --sparse-index instead of toggling the raw config by hand.

Use it when sparse-checkout is already narrowing the working tree and local index cost is still visible.

Leave it off when external tools still choke on sparse-directory entries. Current docs are blunt here: older Git versions may fail to interact with the repository until the sparse index is disabled again.

maintenance.strategy Is About Schedule, Not Magic

maintenance.strategy does not make a repository fast by itself. It picks a recommended background schedule for git maintenance run --schedule=<frequency>.

git config --get maintenance.strategy
git maintenance start
git maintenance unregister --force

Two strategy strings matter here:

incremental is the one that matters in large repositories. It schedules:

Use it when the repository is long-lived enough that background upkeep should be routine instead of heroic. Do not treat it like a foreground speed flag. It only matters if maintenance is actually running. git maintenance start is the honest version because it both sets the config posture and wires the scheduler into the host.

fetch.writeCommitGraph Decides Where Graph Refresh Cost Lives

fetch.writeCommitGraph=true tells Git to write a commit-graph after fetches that download a pack. That helps later graph-heavy commands, but it also puts more work on the foreground fetch path.

git config --get fetch.writeCommitGraph
git config fetch.writeCommitGraph false
git config --unset fetch.writeCommitGraph

Use true when fetches are infrequent enough that paying graph maintenance in the foreground is acceptable.

Use false when:

This is a cost-placement decision. Commit-graph data helps. The question is whether fetch should pay for updating it right now.

status.aheadBehind Is Often Worth Turning Off in Large Repositories

status.aheadBehind=false removes the ahead/behind calculation from ordinary git status output.

git config --get status.aheadBehind
git config status.aheadBehind false
git config --unset status.aheadBehind

Use it when branch divergence calculations are expensive enough to make everyday status feel heavier than it should.

Leave it on when the repository is small enough that the count is cheap and the information is genuinely useful in the default status output.

It improves perceived speed by deleting optional foreground work rather than by accelerating the underlying mechanism.

protocol.version and fetch.negotiationAlgorithm Shape Fetch Behavior

protocol.version and fetch.negotiationAlgorithm sit on the transport side rather than the local working-tree side.

git config --get protocol.version
git config --get fetch.negotiationAlgorithm
git config protocol.version 2
git config fetch.negotiationAlgorithm skipping

protocol.version already defaults to 2 when unset. Setting it explicitly is mostly about making the policy visible in environments where you do not trust defaults to stay aligned.

fetch.negotiationAlgorithm=skipping is sharper. It reduces negotiation round trips by skipping commits more aggressively, but it can also produce a larger-than-necessary packfile. That is a real trade:

Use it when negotiation is the visible cost. Leave it alone when the network path is fine or when transfer size matters more than negotiation latency.

remote.<name>.promisor and remote.<name>.partialclonefilter Change Fetch Policy

remote.<name>.promisor=true and remote.<name>.partialclonefilter=blob:none change what object absence means in the repository.

git config --get-regexp '^remote\.origin\.(promisor|partialclonefilter)$'
git config remote.origin.promisor true
git config remote.origin.partialclonefilter blob:none
git fetch --refetch origin

These are not generic speed toggles. They are partial-clone policy.

Use them when:

Changing remote.<name>.partialclonefilter only affects fetches for new commits. The --refetch form helps align later fetch policy, but it does not magically turn an existing full clone into a dehydrated repository. That distinction matters enough to keep repeating.

extensions.worktreeConfig Keeps Local Tuning Local

extensions.worktreeConfig=true lets Git read and write a worktree-specific config file so local settings stay attached to one worktree instead of leaking into the whole repository.

git config --get extensions.worktreeConfig
git config extensions.worktreeConfig true
git config --worktree core.sparseCheckout true
git config --worktree index.sparse true

Use it when the repository has several linked worktrees with different local roles:

By itself, it does not make anything faster. What it does is keep the rest of the speed knobs from bleeding across unrelated worktrees.

Use Bundles, Not Confetti

The best configuration posture is usually a small coherent bundle, not twelve unrelated lines copied from three blog posts.

A large local checkout bundle usually looks like:

git config core.fsmonitor true
git config core.splitIndex true
git config core.untrackedCache true

# Only when every Git touching the checkout is recent enough:
git config feature.manyFiles true

A background-maintenance bundle usually looks like:

git maintenance start
git config fetch.writeCommitGraph false
git config status.aheadBehind false

A blobless transfer bundle usually looks like:

git config protocol.version 2
git config fetch.negotiationAlgorithm skipping
git config remote.origin.promisor true
git config remote.origin.partialclonefilter blob:none

Those bundles are useful because they answer one practical question each:

The settings matter less as isolated trivia than as clean answers to concrete repository costs.