Highlights from Git 2.22
A look at some of the new features in the latest Git release.
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.
Rebase merges, interactively
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 master.
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)
All that 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
. Using --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:
[source]
Create branches from merge bases
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 A
and 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 A
and B
. That’s another way of showing what has happened on B
.
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 master
and 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, A
and 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:
[source]
Tidbits
- 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 ofgit branch
with a combination ofgrep
andawk
, but both seem like rather unfulfilling options.
In Git 2.22, you can now use the much more naturalgit branch --show-current
to 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-ref
is 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 onmaster
. To do this, you might rungit checkout master -- dir
. But what if one of the files,dir/file.txt
, wasn’t present at all onmaster
? What does it mean to “go back” to that state?
Git treats that checkout command as a request to “overlay” the contents ofmaster
into your copy ofdir/
. It will copy any contents that are present inmaster
into your working tree, but won’t delete tracked files thatmaster
doesn’t have. The result is a combination of the two: what you had before and what was inmaster
.
However, it’s just as reasonable to interpret this as a request to convert yourdir/
into the exact set of contents frommaster
, 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--overlay
). [source] - 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 acceptgit diff --no-function-context
. This is a result of using Git’s ownparse-options
API, which is used across many sub-commands to ensure that command-line options are parsed consistently.
Thegit diff
command was written prior to theparse-options
API, so it had its own hand-crafted parser. In Git 2.22,git diff
now uses theparse-options
API, 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
git log
‘s--format
option 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
[source]
- 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 bisect
tries to show you a pretty version of the commit. But ever since it was introduced in 2005,bisect
has used thediff-tree
plumbing 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 thesrc/
directory. Not helpful. It also shows the machine-readable--raw
diff 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,bisect
will show a full--stat
diff, 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 withgit 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.0
tag 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]
Until next time
That’s just a sampling of changes from the latest version. Read the full release notes for 2.22, or find the release notes for previous versions in the Git repository.
Tags:
Written by
Related posts
What the EU’s new software legislation means for developers
The EU Cyber Resilience Act will introduce new cybersecurity requirements for software released in the EU. Learn what it means for your open source projects and what GitHub is doing to ensure the law will be a net win for open source maintainers.
Game Off 2024 theme announcement
GitHub’s annual month-long game jam, where creativity knows no limits! Throughout November, dive into your favorite game engines, libraries, and programming languages to bring your wildest game ideas to life. Whether you’re a seasoned dev or just getting started, it’s all about having fun and making something awesome!
Highlights from Git 2.47
Git 2.47 is here, with features like incremental multi-pack indexes and more. Check out our coverage of some of the highlights here.