Faster bitmaps when coverage is thin

Reachability bitmaps have been part of Git’s performance toolkit for years. They let Git answer “what objects are reachable from this commit?” questions quickly—essential for serving fetches and clones. Each bitmap position maps to a specific object, and the bit’s value records whether that object is reachable. Queries then become bitmap operations: to find objects unique to one branch versus another, Git builds a bitmap for each side and computes their AND NOT.

The trouble starts when bitmap coverage is incomplete. If a branch or tag has moved since its last bitmap was recorded, Git must fall back to a slower path. Previously, Git would walk the commit graph from each tip, marking objects in a “have” bitmap until it hit an existing bitmap or ran out of history. That walk is fast when bitmaps are dense, but when coverage is sparse—many tips without any nearby bitmap—the walk can touch a large portion of the object graph, negating the benefit of bitmap use.

Git 2.42 introduces an alternative traversal algorithm aimed squarely at that sparse-coverage case. Instead of building a bitmap from each unwanted tip individually, it first identifies the query boundary: the first commit reachable from both the wanted and unwanted sides of the query. Walking back from that single boundary commit until hitting an existing bitmap (or history’s start) produces a “have” bitmap. The “want” side is then computed by walking from the wanted tip, stopping when reaching an object already marked in the “have” set. A final AND NOT yields the result.

Figure 1: Bitmap-based traversal computing the set of objects unique to `main` in Git 2.41.0.

The advantage is most visible when the unwanted half of the query is large or when bitmaps sit close to the boundary. In real-world repositories, the new approach shows between a 2- and 15-fold speedup over the old traversal. You can enable it in Git 2.42 by setting:

$ git repack -ad --write-bitmap-index
$ git config pack.useBitmapBoundaryTraversal true

Then use git rev-list with --use-bitmap-index to take advantage of the faster path.

Skipping hidden refs instead of inspecting them

Server operators often hide certain references from clients during a push or fetch. GitHub, for example, maintains refs/pull/NNN/head and refs/pull/NNN/merge but does not advertise those to pushers. Git already supported this via transfer.hideRefs, but the implementation was blunt: every reference was enumerated and inspected, then discarded if it matched a hidden pattern. With thousands of hidden refs, that becomes a lot of prefix comparisons that end in rejection.

Git 2.42 replaces that per-reference check with a smarter lookup. For each excluded region in the packed-refs file, Git locates its start and end points. That lets it build a jump list so whole ranges of hidden references can be skipped in one step, rather than processed and rejected individually. The same machinery also powers a new --exclude option for git for-each-ref, letting you filter references by pattern efficiently. In extreme cases, the reference advertisement during a push sees a 20-fold reduction in CPU cost.

Figure 3: Running `for-each-ref` while excluding the `refs/pull/` hierarchy.

The release also adds matching --include and --exclude flags to git pack-refs. That command maintains the packed-refs file, and until now, any loose reference would end up packed. If a particular reference changes or is deleted frequently, repeatedly rewriting it inside packed-refs is wasteful. The new flags let you keep those churn-prone references out of the packed file altogether, which can simplify repository maintenance in setups where certain refs are in constant flux.

Figure 4: The same `for-each-ref` invocation as above, this time using a jump list as in Git 2.42.

Protecting Unreachable Objects From Pruning

Git's cruft packs, introduced in the previous release, track the age of unreachable objects so they can gradually age out and eventually be pruned. By default, Git retains an unreachable object if it is reachable from another unreachable object that was modified after the pruning cutoff. But what if you want to keep unreachable objects indefinitely, even if they haven't been touched recently?

In earlier versions, the only way was to point a reference at those objects—a practical solution for a small set, but unworkable when you're dealing with many objects. Git 2.42 adds a new gc.recentObjectsHook configuration option that lets you register external programs to run before any pruning garbage collection. Each program can print a line-delimited list of object IDs, and every object listed is exempt from pruning regardless of age. The mechanism works even if you haven't switched to cruft packs, as it also applies to loose unreachable objects that haven't aged out yet.

This opens the door to maintaining a large set of precious unreachable objects through an external system like a SQLite database. You can try it with:

$ git config gc.recentObjectsHook /path/to/your/program
$ git gc --prune=<approxidate>

Sparse Index Support for diff-tree

The sparse index lets you check out only a narrow cone of your repository. Many commands have learned to work with it, but any command without support would force a full index expansion—an expensive operation. This release brings full sparse index support to diff-tree, so you can run it without expanding your index. The work was contributed by Shuqi Liang as part of Google Summer of Code.

Richer for-each-ref Formatting

The --format option of git for-each-ref has picked up new ways to display data about commits at reference tips. You can now request GPG signature information directly, or break it into components like grade, signer, key, and fingerprint:

$ git for-each-ref --format='%(refname) %(signature:key)' \
    --sort=v:refname 'refs/remotes/origin/release-*' | tac
refs/remotes/origin/release-3.1 4AEE18F83AFDEB23
refs/remotes/origin/release-3.0 4AEE18F83AFDEB23
refs/remotes/origin/release-2.13 4AEE18F83AFDEB23
[...]

This contribution came from Kousik Sanagavarapu, also a Google Summer of Code student working on Git.

Expanded rev-list --stdin Modifiers

git rev-list accepts complex modifiers such as --branches, --tags, and --remotes on the command line. Its --stdin mode, which reads a line-delimited sequence of commits from standard input (with ^ prefixes to exclude reachable objects), previously only handled raw object IDs. In Git 2.42, --stdin now understands the same modifiers you can pass on the command line, making it considerably more useful in scripts.

Safer Tag Message Editing

Imagine you're writing a tag message for a tag named foo while a background fetch brings in a tag called foo/bar. Git rejects this because it can't store both a loose tag file at $GIT_DIR/refs/tags/foo and a directory at the same path. In previous versions, the error would arrive after your in-progress message at $GIT_DIR/TAG_EDITMSG had already been deleted. Git 2.42 delays that deletion until the tag is successfully written, so you can recover your message if the tag creation fails.

Dereferencing Nested Tags in git tag --points-at

A subtle bug has been fixed in git tag listing. When using --points-at to show only tags pointing at a particular object, tags that pointed at that object through one or more intermediate tags were missed. Git 2.42 now dereferences tags through multiple layers before checking whether they point to the given object.

cat-file Gains -Z Mode

Git 2.38 added a -z flag to git cat-file --batch for NUL-delimited input, useful for queries that themselves contain newlines. But output remained newline-delimited, which becomes unparseable when a query contains a newline and the object is missing—the resulting "missing" message is mixed with the query's line breaks. The new -Z flag changes both input and output to NUL-delimited format, keeping results unambiguous.

Further Reading

This covers a sample of what landed in the release. For the full list, consult the release notes for 2.42 or any previous version in the Git repository.