Geometric Repacking Arrives

Git 2.33 continues the project’s push toward scalable repository maintenance with a new option for git repack. Historically, the command offered two extremes: repack loose objects into a fresh pack, or consolidate all existing packs into a single monolithic one. While fewer packs generally improve performance—many operations scale with pack count—the all-in-one approach has long been required on busy repositories because reachability bitmaps, a key server-side optimization, can only cover objects in one pack.

The new “geometric” strategy introduces a middle ground. Instead of forcing everything into one pack, Git identifies a minimal set of packs to combine so the remaining packs form a geometric progression by object count. If the smallest pack holds N objects, the next must hold at least 2N, then 4N, and so on. This keeps the pack landscape tidy while clustering recently added objects together.

The algorithm walks pack sizes from largest to smallest, checking where the progression breaks. When it finds a violation—say, two adjacent packs of equal size—it marks the smaller packs for consolidation, then expands the candidate set until the progression holds again. A practical example:

Here the break occurs between the second and third packs because both contain a single object. Reaching a valid progression requires rolling the first two packs together, but that still leaves a two-object pack beside a one-object pack:

Only after including the first four packs does the ratio satisfy the geometric rule relative to the fifth pack:

You can reproduce this on a local repository with a script that inspects pack object counts before and after a geometric repack:

$ packsizes() {
    find .git/objects/pack -type f -name '*.pack' |
    while read pack; do
      printf "%7d %s\n" \
        "$(git show-index < ${pack%.pack}.idx | wc -l)" "$pack"
    done | sort -rn
  }
$ packsizes # before
$ git repack --geometric=2 -d
$ packsizes # after

This feature pairs with a new on-disk reverse index format for multi-pack indexes, which lets Git map bit positions back to objects across multiple packs. Together they lay the groundwork for multi-pack bitmaps, which would remove the single-pack constraint entirely. Those follow-up patches are still in review, but the building blocks are now in place.

merge-ort: A Faster Merge Backend

Git’s default merge strategy, merge-recursive, has been around for most of the project’s history. It replaced the older resolve strategy by handling criss-cross histories through recursive merging and by detecting file-level renames across branches. But its architecture, originally a Python script later translated to C, still leans on the index and working tree as its primary data structures. That design has produced a string of subtle bugs in unusual corner cases and makes certain large merges painfully slow, especially those involving renames.

The new merge-ort strategy is a clean-room rewrite that keeps the same concepts—recursion and rename detection—but ditches the legacy constraints. It works without touching the index or working tree during the merge, which improves correctness and also benefits partial clones by avoiding unnecessary object downloads. In benchmarks that are particularly adversarial to the old algorithm, merge-ort shows over a 500x speedup on a single complex merge, and over a 9000x improvement across a rebase series where it can reuse cached intermediate results. Typical merges aren’t that dramatic, but merge-ort is consistently faster while merge-recursive has wide performance variance.

The cleaner codebase also fixes known bugs and opens the door to features like showing the diff between a default merge result and the final committed state, which would reveal how a human resolved conflicts. You can try it now with git merge -s ort or by setting pull.twohead to ort. It’s expected to become the default eventually, though the full benefits may await further integration work—for instance, having rebase pass cached data between individual merges. For a deeper dive, the author has posted a series of articles explaining the rationale and optimizations behind the rewrite.

Scripting and plumbing improvements

Calling git rev-list with --pretty is a common way to drive Git’s history traversal machinery from scripts. The flag can format commits with their author, date, hash, and message parts, but its output is historically awkward to parse: rev-list writes a commit <hash> line before each formatted entry, a quirk preserved for backwards compatibility. Git 2.33 adds --no-commit-header to opt out of that design, which makes shell pipelines much cleaner when you only want the formatted fields.

Another long-standing gap in rev-list is that --filter=blob:limit= only filters blobs; non-blob objects still appear in the output. If you wanted to enumerate every blob under 200 bytes introduced since version 2.32, piping the result through cat-file would show commits and trees mixed with the blobs. Git 2.32 adds --filter=object:type=<type>, which combined with the existing limit filter (multiple --filter options are unioned) lets you restrict the result to just one object type:

$ git rev-list --objects --no-object-names \
    --filter=object:type=blob \
    --filter=blob:limit=200 \
    --filter-provided-objects v2.32.0..v2.33.0 \
  | git cat-file --batch-check='%(objecttype)' | sort -u
blob

The --filter-provided-objects flag ensures the filters also apply to the traversal tips, which are exempt by default.

Decoration loading and log format additions

Git loads reference decorations for git log when output is a terminal or --decorate appears on the command line. That work was wasted whenever the chosen --format could not display decoration information. Git 2.33 now detects whether loaded decorations can ever appear in the output and skips loading entirely when impossible. When decorations are shown, the loading process only does as much work as needed—if a decorated object never appears among the displayed commits, no extra references are resolved.

For those who do use custom formats, Git 2.32 adds %ah and %ch, which print the author and committer dates in the human-readable format first seen in Git 2.21. The %(describe) placeholder also arrives, letting a format string include the output of git describe for each commit—bare or with options like %(describe:match=<foo>,exclude=<bar>) to control the describe --match and --exclude behavior.

Commit message and workflow tools

The fixup commit workflow, where git commit --fixup creates a placeholder that git rebase --autosquash later positions and squashes, traditionally replaced a patch’s contents. Git 2.32 extends it with --fixup=amend:<commit> to swap both the log message and the patch contents, and --fixup=reword:<commit> to replace only the message while leaving the diff untouched.

The same release teaches git commit a --trailer flag, inserting structured Signed-off-by or Reviewed-by-style lines automatically in a parseable position. Combined with --fixup, it can retrofit missing trailers onto an earlier commit:

$ git commit --no-edit \
    --fixup=reword:foo \
    --trailer='Signed-off-by=Mona Lisa Octocat <[email protected]>'
$ EDITOR=true git rebase -i --autosquash foo^

Faster checkouts and smaller index files

Two related strands in Git 2.32 target mechanical operations on large repositories. The first parallelizes working-copy updates. In the past, git checkout created, modified, and removed files one by one, an approach that was fine when disks were the bottleneck—though even then parallel index refreshes with lstat() threads helped by giving the I/O scheduler more work segments to order optimally. Now the updates are split into groups, each delegated to a worker process. Two new options control the behavior:

  • checkout.workers decides how many workers to use; a value of 0 means one worker per logical core.
  • checkout.thresholdForParallelism sets the minimum number of updates that trigger the parallel code path over the sequential one.

The second strand is the sparse index. Even in a sparse checkout, Git historically tracked every file in the repository in its index, so operations that query or rewrite the index were slow regardless of how few files appeared on disk. Git 2.32 limits the index to files inside the sparse checkout plus boundary directories when cone mode is active. So far git checkout, git commit, and git status have been converted; other index-touching commands are being updated incrementally. Enable the behavior with index.sparse, knowing that the feature is still maturing: Git may convert a sparse index to a full one mid-operation, which can be slower than the original call. Future releases will reduce that fallback to zero as the remaining commands are migrated.

Small fixes and security details

Git 2.33 adds a SECURITY.md document, explaining how to report vulnerabilities and prominently listing the security mailing list [email protected]. The document also covers how embargoed security releases are coordinated.

Reachability bitmaps power fetch and clone performance. Several fixes and optimizations for that machinery landed across both releases. One bug marked tag objects uninteresting without marking the objects they point to; because haves and wants are ANDed, correctness held, but Git could waste CPU cycles replacing a cheap bitmap query with a full object walk when the tagged content fell outside the bitmap. That’s fixed so uninteresting status propagates down to the tagged object.

Another improvement affects on-the-fly bitmaps built for server-side answers that need to combine a fresh traversal with an existing bitmap. The code already skips re-traversing commits that a previous bitmap covers, but did not do the same for trees. Root trees are rarely shared, so that walk remained useful; descending into shared sub-trees was the lost opportunity. Git 2.33 applies the skip optimization to shared sub-trees, with measurable speedups for server-side work.

Finally, a cosmetic fix in pack generation for bitmap-backed fetches: the “Enumerating objects” progress meter briefly flashed the number of pack-reused objects, then reset it to zero before counting the objects Git packs itself. The counter is now accurate from the start, so you can stare as long as you like.

Smaller features, bigger focus

Beyond the headline items, the 2.32 and 2.33 releases carry a steady stream of smaller refinements that tighten everyday workflows. The --fixup option for git commit gains a companion --fixup=reword:, letting you stage a replacement message for an existing commit without touching its content. Instead of running a separate interactive rebase, you can mark a commit for rewording directly from the command line.

Merges also get a long-overdue convenience: a conflict during git merge now exits with a detailed hint about unmerged paths, pointing you to git status for the full picture. The error output is clearer about which files need manual resolution, cutting down on the “what just failed?” moment.

For those who script around Git, git rev-list now understands the --disk-usage flag, reporting the total size of all objects reachable from a given set of refs. Combined with the existing traversal options, this makes it straightforward to measure exactly what a branch or tag carries in storage terms, no extra plumbing required.

The documentation and error-message polish continues as well. Several commands that previously used vague phrasing now spell out the corrective action, such as git switch suggesting a -c flag when you try to check out a branch that doesn’t exist. It is a small UX shift, but one that removes guesswork at the terminal.

The rest of the release

Of course, these highlights only scratch the surface. Both releases contain dozens of additional fixes, performance tweaks and internal cleanups. To dig into the full list, read the official release notes for 2.32 and 2.33, or browse the notes for any prior version in the Git source tree.