
Highlights from Git 2.54
The open-source Git project just released Git 2.54 with features and bug fixes from over 137 contributors, 66 of them new. We last caught up with you on the latest in Git back when 2.52 was released.
To celebrate this most recent release, here is GitHubâs look at some of the most interesting features and changes introduced since last time.
đĄ Since the last Git release we wrote about was Git 2.52, this blog post covers the highlights from both the 2.53 and 2.54 releases.
Rewrite history with git history
The Git project has a long history of providing tools to rewrite your repositoryâs history. git rebase âi is the most well-known, and itâs remarkably flexible: you can reorder, squash, edit, and drop commits. But that flexibility comes with complexity: an interactive rebase operates on a range of commits, updates your working tree and index as it goes, and can leave you in a conflicted state that you need to resolve before proceeding.
For simpler cases, all of that machinery can feel like overkill. If all you want to do is fix a typo in a commit message three commits back, or split one commit into two, an interactive rebase works, but requires you to set up a to-do list, mark the right commit for editing, and then drive the rebase to completion.
Git 2.54 introduces a new experimental command that is designed for exactly these simpler cases: [git history](https://git-scm.com/docs/git-history/2.54.0). The history command currently supports two operations: reword and split.
git history reword <commit> opens your editor with the specified commitâs message and rewrites it in place, updating any branches that descend from that commit. Unlike git rebase, it doesnât touch your working tree or index, and it can even operate in a bare repository.
git history split <commit> lets you interactively split a commit into two by selecting which hunks should be carved out into a new parent commit. The interface will look familiar if youâve ever used add in interactive mode via git add âp:
$ git history split HEAD
diff --git a/bar b/bar
new file mode 100644
index 0000000..50810a5
--- /dev/null
+++ b/bar
@@ -0,0 +1 @@
+bar
(1/1) Stage addition [y,n,q,a,d,p,?]? y
After selecting hunks, Git creates a new commit with those changes as the parent of the original commit (which retains whatever hunks you didnât select) and rewrites any descendent branches to point at the updated history.
There are a couple of intentional limitations worth noting. The history command does not support histories that contain merge commits, and it will refuse to perform any operation that would result in a merge conflict. By design, git history is meant for targeted, non-interactive rewrites, not the kind of open-ended history rewriting typically relegated to git rebase âi.
The history command is built on top of git replayâs core machinery, which was itself extracted into a library as part of this work. That foundation means that git history benefits from replayâs ability to operate without touching the working tree, making it a natural fit for scripting and automation in addition to interactive use.
This command is still marked as experimental, so its interface may evolve. Give it a try with git history reword and git history split, available in Git 2.54.
[source, source, source, source, source]
Config-based hooks
If youâve ever wanted to share a Git hook across multiple repositories, youâve probably had to reach for a third-party hook manager, or manually symlink scripts into each repositoryâs $GIT_DIR/hooks directory. Thatâs because, historically, Git hooks could only be defined as executable scripts living in one place: the hooks subdirectory of your .git directory (or whatever core.hooksPath points to).
That meant that if you wanted to run a linter before every commit across all of your repositories, you had to copy the script into each repository, which can be tedious and error-prone. Alternatively, you could set core.hooksPath to point to a shared directory, but that causes all of your repositories to share the exact same set of hooks, with no way to mix and match.
Git 2.54 introduces a new way to define hooks: in your configuration files. Instead of placing a script at .git/hooks/pre-commit, you can now write:
[hook "linter"]
event = pre-commit
command = ~/bin/linter --cpp20
The hook.<name>.command key specifies the command to run, and hook.<name>.event specifies which hook event should trigger it. Since this is just configuration, it can live in your per-user ~/.gitconfig, a system-wide /etc/gitconfig, or in a repositoryâs local config. That makes it straightforward to define a set of hooks centrally and have them apply everywhere.
Even better, you can now run multiple hooks for the same event. If you want both a linter and a secrets scanner to run before every commit, you can configure them independently:
[hook "linter"]
event = pre-commit
command = ~/bin/linter --cpp20
[hook "no-leaks"]
event = pre-commit
command = ~/bin/leak-detector
Git will run them in the order it encounters their configuration. The traditional hook script in $GIT_DIR/hooks still works, and runs last, so existing hooks are unaffected. You can see which hooks are configured (and where they come from) with git hook list:
$ git hook list pre-commit
global linter ~/bin/linter --cpp20
local no-leaks ~/bin/leak-detector
Individual hooks can be disabled without removing their configuration by setting hook.<name>.enabled = false, which is particularly handy when a hook is defined in a system-level config but you need to opt a specific repository out.
Along the way, Gitâs internal handling of hooks has been modernized. Many built-in hooks that were previously invoked through ad-hoc code paths (like pre-push, post-rewrite, and the various receive-pack hooks) have been migrated to use the new hook API, meaning they all benefit from the new configuration-based hook machinery.
Geometric repacking during maintenance by default
Returning readers of this series may recall our coverage of the new geometric strategy within git maintenance, which was introduced in Git 2.52. That strategy works by inspecting the contents of your repository to determine if some number of packfiles can be combined to form a geometric progression by object count. If they can, Git performs a geometric repack, condensing the contents of your repository without needing to perform a full garbage collection.
In 2.52, the geometric strategy was available as an opt-in choice via the maintenance.strategy configuration. In 2.54, it becomes the default strategy for manual maintenance. That means when you run git maintenance run without specifying a strategy, Git will now use the geometric approach instead of the traditional gc task.
In practice, this means that your repositories will be maintained more efficiently out of the box. The geometric strategy avoids the expensive all-into-one repacks that gc performs, instead combining packs incrementally when possible and falling back to a full gc only when it would consolidate the entire repository into a single pack. Along the way, it keeps your commit-graph, reflogs, and other auxiliary data structures up to date.
If you were already using maintenance.strategy = geometric in your configuration, nothing changes. If you hadnât set a strategy (or were relying on the old gc default), youâll start seeing the benefits of geometric repacking automatically. The gc strategy is still available if you prefer it and can be selected with maintenance.strategy = gc.
[source]
The tip of the icebergâŠ
Now that weâve covered some of the larger changes in more detail, letâs take a closer look at a selection of some other new features and updates in this release.
-
TheÂ
git add âp command, Gitâs tool for interactively staging individual hunks, received a handful of usability improvements in this release. When navigating between hunks with theÂJ andÂK keys, Git now shows whether youâve previously accepted or skipped each hunk, so you donât have to remember your earlier decisions.Separately, a newÂ
--no-auto-advance flag changes howÂgit add âp handles the transition between files. Normally, once youâve made a decision on every hunk in a file, the session automatically moves on to the next one. WithÂ--no-auto-advance, the session stays put after youâve decided on the last hunk, letting you useÂ< andÂ> to move between files at your own pace. This can be useful when you want to review your decisions holistically before committing to them. -
git replay, the experimental command for replaying commits onto a new base without touching the working tree, continues to mature. This release brings several improvements:Âreplay now performs atomic reference updates by default (instead of printingÂupdate-ref commands toÂstdout), has learned a newÂ--revert mode that reverses the changes from a range of commits, can now drop commits that become empty during replay, and supports replaying all the way down to the root commit. -
Gitâs HTTP transport now handles HTTP 429 âToo Many Requestsâ responses. Previously, a 429 from the server would be treated as a fatal error. Git can now retry the request, honoring the serverâsÂ
Retry-After header when present, or fall back to a configurable delay via the newÂhttp.retryAfter setting. The newÂhttp.maxRetries andÂhttp.maxRetryTime configuration options provide control over how many times to retry and how long to wait, respectively.[source]
-
git log âL, which traces the history of a range of lines within a file, has historically used its own custom output path that bypassed much of Gitâs standard diff machinery. As a result, it was incompatible with several useful options, including the-Sand-Gâpickaxeâ options for searching by content changes.This release reworks
git log âLto route its output through the standard diff pipeline, making it compatible with patch formatting options and pickaxe searches for the first time.Say you want to trace the history of
strbuf_addstr()instrbuf.c, but only see commits wherelenwas added or removed within that function:$ git log -L :strbuf_addstr:strbuf.c -S len --oneline -1 a70f8f19ad2 strbuf: introduce strbuf_addstrings() to repeatedly add a string diff --git a/strbuf.c b/strbuf.c --- a/strbuf.c +++ b/strbuf.c @@ -316,0 +316,9 @@ +void strbuf_addstrings(struct strbuf *sb, const char *s, size_t n) +{ + size_t len = strlen(s); + + strbuf_grow(sb, st_mult(len, n)); + for (size_t i = 0; i < n; i++) + strbuf_add(sb, s, len); +}Prior to this release, options like
-S, (and-G,--word-diff, along with--color-moved) were silently ignored when used with-L. Now they work together naturally:-Lscopes the output to the function you care about, and-Sfilters down to just the commits that touched the symbol youâre searching for within it.[source]
-
Incremental multi-pack indexes, which we first covered in our discussion of Git 2.47 and followed up on in Git 2.50, received further work in this release. The MIDX machinery now supports compaction, which merges smaller MIDX layers together (along with their associated reachability bitmaps) to keep the number of layers in the chain manageable. This is an important step toward making incremental MIDXs practical for long-lived repositories that accumulate many layers over time.
[source]
-
git statuslearned a newstatus.compareBranchesconfiguration option. By default,git statusshows how your current branch compares to its configured upstream (e.g., âYour branch is ahead of âorigin/mainâ by 3 commitsâ). Withstatus.compareBranches, you can ask it to also compare against your push remote, or both:[status] compareBranches = @{upstream} @{push}This is useful if your push destination differs from your upstream, as is common in triangular workflows where you fetch from one remote and push to another (like a fork).
[source]
-
Say you have a series of commits, and want to add a trailer to each one of them. You could do this manually, or automate it with something like:Â
git rebase -x âgit commit --amend --no-edit --trailer=âReviewed-by: A U Thor <a href="mailto:<author@example.com>ââ, but thatâs kind of a mouthful.In Git 2.54,Â
git rebase learned a newÂ--trailer option, which appends a trailer to every rebased commit via theÂinterpret-trailers machinery. Instead of the monstrosity above, we can now writeÂgit rebase --trailer "Reviewed-by: A <a href="mailto:<a@example.com>" and achieve the same effect.[source]
-
When signing commits, a signature remains valid even when it was signed with a GPG key that has since expired. Previously, Git displayed these signatures with a scary red color, which could be misleading and lead you to interpret the signature itself as invalid. Git now correctly treats a valid signature made with a since-expired key as a good signature.
[source]
-
git blame learned a newÂ--diff-algorithm option, allowing you to select which diff algorithm (e.g.,Âhistogram,Âpatience, orÂminimal) is used when computing blame. This can sometimes produce meaningfully different (and more useful) blame output, depending on the nature of the changes in your repositoryâs history.[source]
-
Under the hood, a significant amount of work went into restructuring Gitâs object database (ODB) internals. The ODB source API has been refactored to use a pluggable backend design, with individual functions likeÂ
read_object(),Âwrite_object(), andÂfor_each_object() now dispatched through function pointers on a per-source basis. While none of this is user-visible today, it lays the groundwork for future features like alternative storage backends or more flexible object database configurations. -
git backfill, the experimental command for downloading missing blobs in a partial clone, learned to accept revision and pathspec arguments. Previously,Âbackfill would always download blobs reachable fromÂHEAD across the entire tree. You can now scope it to a particular range of history (e.g.,Âgit backfill main~100..main) or a subset of paths (e.g.,Âgit backfill -- '*.c'), including pathspecs with wildcards.This makes backfill much more practical for large partial clones where you only need historical blobs for a specific area of the repository.
[source]
-
Gitâs alias configuration has historically been limited to ASCII alphanumeric characters and hyphens. That meant alias names like âhĂ€mtaâ (Swedish for âfetchâ) or âç¶æ â (Japanese for âstatusâ) were off-limits. Git 2.54 lifts that restriction with a new subsection-based syntax:
[alias "hÀmta"] command = fetchThe traditional
[alias] co = checkoutsyntax continues to work for ASCII names. The new subsection form supports any characters (except newlines andNULbytes), is matched case-sensitively as raw bytes, and uses a command key for the alias definition. Shell completion has been updated to handle these aliases as well. -
The histogram diff algorithm received a fix for a subtle output quality issue. After any diff algorithm runs, Git performs a âcompactionâ phase that shifts and merges change groups to produce cleaner output. In some cases, this shifting could move a change group across the anchor lines that the histogram algorithm had chosen, producing a diff that was technically correct but visually redundant. Git now detects when this happens and re-diffs the affected region, resulting in tighter output that better matches what you would expect.
[source]
âŠthe rest of the iceberg
Thatâs just a sample of changes from the latest release. For more, check out the release notes for 2.53 and 2.54, or any previous version in the Git repository.
Written by
Taylor Blau is a Principal Software Engineer at GitHub where he works on Git.
Source: GitHub Blog


