Three new Campus Experts are joining the fall 2022 batch of the MLH Fellowship to work with open source maintainers and get real-world experience.
The open source Git project just released Git 2.22 with features and bug fixes brought to you from over 74 contributors, 18 of them new. Here’s our look at some of the most exciting features and changes introduced since Git 2.21.
You might have used
git rebase before to alter the history of your repository. If you haven’t, here’s a quick primer:
git rebase replays a series of commits on a new initial commit. For example, you might have used
git rebase to make sure that your feature branch was based on the latest changes from upstream. Say that you have a repository structure that looks like:
o (my-feature) / o --- o --- o (master)
Let’s say that while you were working on
my-feature, the branch you started from (
master) changed, so now your repository looks like:
o --- o --- o (my-feature) / o --- o --- o --- o --- o (master)
How do you make sure that your branch
my-feature merges into
master cleanly? You could just merge it, but the end result might be hard for others to understand if you have to resolve conflicts. If you haven’t yet shared the branch with reviewers, they might prefer to see your commits as if they had been written directly on top of the current
So, you might rebase it. Remember: rebasing is an operation that takes a series of commits and applies them on top of a new base. So, after rebasing
my-feature on the latest from
master, you’ll instead get:
o --- o --- o (my-feature) / o --- o --- o --- o --- o (master)
git rebase did was take the first “new” commit in (my-feature), applied it on top of the new tip of master, and then so on through all of the remaining commits on
my-feature in order, until there were none left.
Now, what if your example is more complicated? Let’s say that
my-feature has its own branching structure that you want to preserve while replaying the commits. To further complicate things, let’s also assume that you want to use some of
rebase‘s interactive features, like dropping, reordering, and renaming commits.
If you didn’t mind giving up those interactive features, you could have used
--preserve-merges. You can use
git rebase -i --preserve-merges and edit the history interactively, but some of your repository’s structure might not remain intact.
Git has a new option
--rebase-merges, since 2.18, but in 2.22 the old option is officially deprecated in favor of
--rebase-merges allows you to preserve the structure of your changes, while also giving you the full power of interactivity.
Here’s an example. Let’s say that you have a branching structure based on
master, but upstream (say,
origin/master) has changed since you created your branch. You want to replay your commits on the latest from upstream, preserve the branching structure, and make a few modifications to a commit message along the way (we’ll simulate this by fixing a typo).
In Git 2.22, this is what that might look like:
Given some set of two or more branches, how can you tell what history they have in common? As it turns out, Git has a precise way to answer this question. Git calls this a merge base: the most recent common ancestor among a set of commits.
When might you want to compute a merge base in practice? The obvious answer is: when you’re merging! Git computes this common ancestor as the base for a three-way merge of the content (hence the name “merge base”). But you might also want to use this common ancestor as a cutoff point for listing commits. Running
git log A...B will show all of the commits in
B down to their common ancestor; in other words, what happened since the two diverged (you can also use
--left-right to see which commit is on which side).
This “triple-dot” notation is associated with merge bases in other contexts, too. Running
git diff A...B will show the differences between
B and the merge base of
B. That’s another way of showing what has happened on
What’s another instance that you might want to use a merge-base? Say that you’re working on a feature branch, and you decide that part of the way through, you’d like to start over on a different branch. Let’s also say that you’d like to start at the same place on
master that your existing feature branch was cut from.
How do you create such a thing? Well, you could manually inspect the
git log, but this might be cumbersome if your history is particularly large. You could invoke:
$ git merge-base master my-feature
to compute the SHA-1 of the merge base between
my-feature, copy the SHA-1 you got back, and then paste it into:
$ git branch my-other-feature <sha-1>
Now in Git 2.22,
git branch and
git checkout -b have both learned the triple-dot merge base syntax. To specify that you’d like to create a branch from the merge base of two other branches (say,
B), you can now run:
$ git branch my-other-feature A...B # or... $ git checkout -b my-other-feature A...B
Here’s an example:
- Have you ever needed to ask for the name of the branch you have checked out? You could use
git symbolic-ref(if you were aware of its existence), or hacked up the output of
git branchwith a combination of
awk, but both seem like rather unfulfilling options.
In Git 2.22, you can now use the much more natural
git branch --show-currentto get the name of the branch you currently have checked out. (If you want to use this output in a machine-readable setting,
git symbolic-refis still preferred, though.) [source]
- Say you’re on a feature branch, and you want to get back the contents of the
dir/directory as it was on
master. To do this, you might run
git checkout master -- dir. But what if one of the files,
dir/file.txt, wasn’t present at all on
master? What does it mean to “go back” to that state?
Git treats that checkout command as a request to “overlay” the contents of
masterinto your copy of
dir/. It will copy any contents that are present in
masterinto your working tree, but won’t delete tracked files that
masterdoesn’t have. The result is a combination of the two: what you had before and what was in
However, it’s just as reasonable to interpret this as a request to convert your
dir/into the exact set of contents from
master, both adding and deleting as appropriate. In Git 2.22, there’s now a way to express that:
git checkout --no-overlay -- dir. The default behavior remains unchanged (i.e., the same as passing
- You might have noticed that wherever Git accepts an option like
git diff --function-context(to show the function context nearest each hunk), it also will accept
git diff --no-function-context. This is a result of using Git’s own
parse-optionsAPI, which is used across many sub-commands to ensure that command-line options are parsed consistently.
git diffcommand was written prior to the
parse-optionsAPI, so it had its own hand-crafted parser. In Git 2.22,
git diffnow uses the
parse-optionsAPI, meaning you should expect more consistent command-line options parsing in more parts of Git. [source]
- You might have heard the term “trailers” to describe those extra bits of information at the end of a commit, like “Signed-off-by”, or “Co-authored-by”. You might also know that
--formatoption has allowed you to list those trailers as part of a custom format.
Now you can further specify what you’d like to display, filtering trailers by key, value, and more. Say you want to get a list of individuals most often listed as reviewers on a project. Now, you can do this with:
$ git log --pretty="%(trailers:key=Reviewed-by,valueonly)" | grep '.' | sort | uniq -c | sort -rn | head -5
- Git now ships with a new tracing mechanism, Trace2, which supports a much more flexible and structured output format. Trace2 allows you to set a destination to receive long-running performance and telemetry data. It’s completely off by default, and organizations that wish to use it may opt-in as they choose.
As an aside: it’s worth noting that you define where your data goes. The Git project does not—nor does it have plans to ever—collect your data. [source]
Learn about all of the new, flexible logging mechanisms
- After finding a ‘good’ or ‘bad’ commit,
git bisecttries to show you a pretty version of the commit. But ever since it was introduced in 2005,
bisecthas used the
diff-treeplumbing to show you the commit, which is a long way from “pretty”. By default, it shows you only the top-level of the tree. So you might find out that the commit in question changed the
src/directory. Not helpful. It also shows the machine-readable
--rawdiff format, giving you only the before and after hashes, with no clue as to what actually changed. And for merge commits, it shows nothing at all!
In Git 2.22,
bisectwill show a full
--statdiff, summarizing the changes to each file by line count (for merges, this counts the differences that were brought in by the merge). It stops short of showing the full content-level diff, but you can view that yourself with
git show. [source]
- In our last post, we talked about Git’s new directory rename detection mechanism. Internally, Git compares the contents of the source and destination trees to see whether or not it thinks that a directory has been renamed. Sometimes, Git will mark a directory as renamed when in fact it wasn’t.
In Git 2.22, those heuristics have been toned down so that Git only marks a directory as having been renamed when it is more sure. When it isn’t as sure, Git will leave these paths marked as conflicting, letting you review the change and mark it appropriately. [source]
- Many projects use a Git tag to mark the location of a release in the repository’s history. For example, in the Git repository the
v2.22.0tag points to this commit.
Something you might not have known is that Git tags can point to any object: blobs, trees, commits, and even other tags! While commits are the most common, it’s usually a mistake to create a tag pointing to another tag.
One way you can make this mistake is by running:
$ git tag -f -m "updated message" <my-tag> <my-tag>
Which you may have written if you meant “update
<my-tag>with this message and leave the thing it points to unchanged”. But, that invocation creates a new tag which points at the old tag, when you most likely meant to point it at the thing that the old tag points at.To help prevent you from making this mistake, Git now warns you when you create a tag pointing to another tag. [source]
- In an earlier blog post, we wrote about reachability bitmaps, and how they can improve graph traversals. In Git 2.22, these bitmaps are now generated by default in bare repositories, which should provide a considerable speed-up for repositories serving fetches. [source]