Highlights from Git 2.54
- git history 支持 reword 和 split 操作,无需操作工作区,适合轻量级历史修改。
- 新命令不支持含合并提交的历史,且自动规避冲突,定位为非交互式精准重写工具。
- 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`. 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,?]? yAfter 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 --cpp20The `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-detectorGit 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-detectorIndividual 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`.
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 handlesHTTP 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.
- `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 `-S` and `-G` “pickaxe” options for searching by content changes.
This release reworks `git log –L` to 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()` in `strbuf.c`, but only see commits where `len` was 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: `-L` scopes the output to the function you care about, and `-S` filters down to just the commits that touched the symbol you’re searching for within it.
- Incremental multi-pack indexes, which we first covered inour discussion of Git 2.47and followed up on inGit 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.
- `git status` learned a new `status.compareBranches` configuration option. By default, `git status` shows how your current branch compares to its configured upstream (e.g., “Your branch is ahead of ‘origin/main’ by 3 commits”). With `status.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).
- 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.
- When signing commits, asignatureremains 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.
- `git blame`learned a new`--diff-algorithm`option, allowing you to select whichdiff 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.
- 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 apartial clone, learned to accept revision andpathspecarguments. 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.
- 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 = checkout` syntax continues to work for ASCII names. The new subsection form supports any characters (except newlines and `NUL` bytes), 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.
…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.