Highlights from Git 2.22

A look at some of the new features in the latest Git release.

null
|
| 10 minutes

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:

git rebase --rebase-merges example

[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:

git branch merge-base 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 of git branch with a combination of grep and awk, but both seem like rather unfulfilling options.

    In Git 2.22, you can now use the much more natural git 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 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 master into your copy of dir/. It will copy any contents that are present in master into your working tree, but won’t delete tracked files that master doesn’t have. The result is a combination of the two: what you had before and what was in master.

    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 --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 accept git diff --no-function-context. This is a result of using Git’s own parse-options API, which is used across many sub-commands to ensure that command-line options are parsed consistently.

    The git diff command was written prior to the parse-options API, so it had its own hand-crafted parser. In Git 2.22, git diff now uses the parse-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 the diff-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 the src/ 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 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.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

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!