Following the git 2.0.0 release two-and-a-half months ago we’re being treated to a new minor version of git, 2.1.0, with a host of exciting new features!

The full release notes are available here, but they can be a bit terse if you’re not deeply involved in the git community. This blog is my own commentary on some aspects of the release that got us excited at Atlassian.

Better pager defaults

The quotes in this article are lifted directly from the release notes, with my own commentary below.

Since the very beginning of Git, we gave the LESS environment a default value “FRSX” when we spawn “less” as the pager. “S” (chop long lines instead of wrapping) has been removed from this default set of options, because it is more or less a personal taste thing, as opposed to the others that have good justifications (i.e. “R” is very much justified because many kinds of output we produce are colored and “FX” is justified because output we produce is often shorter than a page).

If you haven’t already overridden your git pager defaults, this change means that paged output from git commands will wrap instead of being truncated at the width of your terminal. Here’s an example with git 2.1.0 (wrapped) on the left and 2.0.3 (truncated) on the right:

Pager styles in git 2.1.0 vs git 2.0.3

This will likely only affect your log output if you use a narrow terminal or have long unbroken lines in your commit messages. The general git wisdom is to keep commit messages at no more than 72 characters wide, but if the wrapping bothers you you can disable it by restoring the original behavior with:

1
$ git config core.pager "less -S"

Of course, the pager is used to display other output as well, such as git blame, which can have very long lines depending on author name length and coding style. The 2.1.0 release notes also point out that you can enable the -S flag for just the blame pager with:

1
$ git config pager.blame "less -S"

If you’re curious about the default less options that git still uses:

  • -F makes less exit if there’s less than a page of output,
  • -R ensures only ANSI color escape sequences are output in raw form (so your git console colors work), and
  • -X prevents the screen from being cleared when less launches (again useful for logs of less than a page in length).

Better bash completion

The completion script for bash (in contrib/) has been updated to better handle aliases that define a complex sequence of commands.

This is super cool! I’m a big fan of custom git aliases. The ability to plug git’s bash completion into my complex aliases makes them that much more powerful and convenient to use from the command line. For example, I have an alias defined that greps out JIRA-style issue keys (e.g. STASH-123) from the log:

1
issues = !sh -c 'git log --oneline $@ | egrep -o [A-Z]+-[0-9]+ | sort | uniq' -

All arguments are passed on to the git log command, so you can restrict the range of commits to retrieve issue keys for. For example, git issues -n 1 will show me the issue key associated with the commit at the tip of my branch. As of 2.1.0, git’s bash completion has been improved to allow me to complete the git issues alias as if it were the git log command.

Under git 2.0.3, typing git issues m<tab> would fall back to the default bash completion behavior of listing files starting with m in the current directory. Under git 2.1.0, it correctly completes to master, as that’s how the git log command would behave. You can also seed the bash completion by prefixing your alias with a null command starting with a ‘:’. This is useful if the git command you want to complete on is not the first command in your alias. For example the alias:

1
issues = "!f() { echo 'Printing issue keys'; git log --oneline $@ | egrep -o [A-Z]+-[0-9]+ | sort | uniq; }; f"

doesn’t work properly with completion because git fails to recognize the echo command as a completion target. But if you prefix it with ': git log ;' completion works correctly again:

1
issues = "!f() { : git log ; echo 'Printing issue keys'; git log --oneline $@ | egrep -o [A-Z]+-[0-9]+ | sort | uniq; }; f"

This is a big usability win if you enjoy scripting up complex git aliases! Remember this is in contrib/, and not part of core git, so don’t forget to update your bash profile to point to the new version of contrib/completion/git-completion.bash if you need to.

approxidate comes to git commit

“git commit ‐‐date=<date>” option learned more timestamp formats, including “‐‐date=now”.

git commit’s --date flag now falls back to git’s awesome (if slightly eccentric) approxidate parser when its stricter cousin parse_date() can’t parse a particular date string. approxidate can handle obvious things like --date=now and also allows for some slightly more esoteric formats like --date="midnight the 12th of october, anno domini 1979" or --date=teatime. Alex Peattie has an excellent blog post on git’s awesome date handling if you’d like to know more.

Better paths with grep.fullname

“git grep” learned the grep.fullname configuration variable to force “‐‐full-name” to be the default. This may cause regressions for scripted users who do not expect this new behaviour.

Let me save you the man git-grep invocation:

--full-name

When run from a subdirectory, the command usually outputs paths relative to the current directory. This option forces paths to be output relative to the project top directory.

Sweet! This is a nice default for my workflow, which often involves running git grep to find a file path to copy and paste into an XML file somewhere (this may betray the fact that I’m a Java developer). If this is useful for you too, simply run:

1
$ git config --global grep.fullname true

to enable it in your config.

The --global flag applies the configuration to my $HOME/.gitconfig so it becomes the default behavior for all git repositories on my system. It can always be overridden at the repository level if needed.

git replace got smarter

Wait! Back up a bit. What did this git replace thing do in the first place?

In a nutshell, git replace lets you rewrite certain objects in your git repository without changing their corresponding tree or commit SHAs. If this is the first time you’ve heard of git replace and you’re familiar with the git data model this may sound like sacrilege! It certainly did to me. I have another blog post in the works for when and why you might use such a feature. If you want to learn more in the meantime this article is far better than the man page, which is a little scant on use cases.

Anyway, how did git replace get smarter?

“git replace” learned the “‐‐edit” subcommand to create a replacement by editing an existing object.

The --edit option allows you to conveniently copy-and-replace a particular object, by dumping its contents to a temporary file and launching your favorite editor. To replace the commit at the tip of master, you can simply run:

1
$ git replace --edit master

Or to edit the blob that the tip commit of master thinks of as jira-components/pom.xml, you can run:

1
$ git replace --edit master:jira-components/pom.xml

Should you do this? Probably not 🙂 In most cases you should be using git rebase to rewrite objects, as it properly rewrites your commit SHAs and keeps a sane history.

“git replace” learned a “‐‐graft” option to rewrite the parents of a commit.

The --graft option is a shortcut for replacing a commit with one that is identical, except with different parents. This is a convenient way of achieving one of the slightly saner use-cases for git replace, shortening your git history. To replace the parent of the tip of my master branch, I could just run:

1
$ git replace master --graft [new parent]..

Or to cut my history off at a particular point, I can orphan a commit by omitting the new parents entirely:

1
$ git replace master --graft

Again, you probably shouldn’t be doing this without good reason. Judicious use of git rebase is generally the preferred way to rewrite your history.

Sensible tag ordering with tag.sort

“git tag” learned to pay attention to “tag.sort” configuration, to be used as the default sort order when no ‐‐sort= option is given.

This is great news if you’re using version numbers in your tag names, which I imagine 99.9% of you are. Once you’ve released a version that contains a segment that is more than one digit in length (e.g. a v10 or a v1.10) git’s default lexicographic ordering doesn’t cut it any more. Take the default ordering of tags from the Atlassian Stash git repository for example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
src/stash $ git tag -l *.*.0
..
stash-parent-2.0.0
stash-parent-2.1.0
stash-parent-2.10.0
stash-parent-2.11.0
stash-parent-2.12.0
stash-parent-2.2.0
stash-parent-2.3.0
stash-parent-2.4.0
stash-parent-2.5.0
stash-parent-2.6.0
stash-parent-2.7.0
stash-parent-2.8.0
stash-parent-2.9.0
stash-parent-3.0.0
..

No good! 2.10.0 comes chronologically after 2.3.0, so this default tag sort order is incorrect. Since git 2.0.0, we’ve been able to use the --sort flag to sort numeric versions correctly:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
src/stash $ git tag --sort="version:refname" -l *.*.0
..
stash-parent-2.0.0
stash-parent-2.1.0
stash-parent-2.2.0
stash-parent-2.3.0
stash-parent-2.4.0
stash-parent-2.5.0
stash-parent-2.6.0
stash-parent-2.7.0
stash-parent-2.8.0
stash-parent-2.9.0
stash-parent-2.10.0
stash-parent-2.11.0
stash-parent-2.12.0
stash-parent-3.0.0
..

Much better. With git 2.1.0, you can make this ordering the default by running:

1
$ git config --global tag.sort version:refname

By the way, the handy -l flag used in the git tag examples above restricts the tag names displayed to a particular pattern. -l *.*.0 is used to show only major and minor Stash releases.

Simpler verification of signed commits

A new “git verify-commit” command, to check GPG signatures in signed commits, in a way similar to “git verify-tag” is used to check signed tags, was added.

If you’re using commit signing to authenticate authorship of commits, the verify-commit command makes verifying signatures a lot easier. Instead of writing your own script to parse the output of git log --show-signature you can simply pass git verify-commit a set of commits to check the signatures for. That said, the odds are you aren’t currently using signed commits (we don’t at Atlassian) as they do introduce some key management and developer annoyance overhead. Signed tags are generally accepted as a better balance between convenience and security for most projects. If you’re curious about why a project might elect to use signed commits, Mike Gerwitz tells a great git horror story on a hypothetical scenario where they’d be very useful. So if you work in an especially sensitive industry you might consider implementing them as part of your workflow.

Performance boosts

Git 2.1.0 also came with some nice performance improvements.

An experimental format to use two files (the base file and incremental changes relative to it) to represent the index has been introduced; this may reduce I/O cost of rewriting a large index when only small part of the working tree changes.

Translation: if you tend to have have big commits which change lots of files, running git add may have just gotten faster. git add is already lightning quick for any incremental case that I tried locally, so I didn’t find a big difference in performance between versions when testing. Interestingly the initial add seems to have sped up a bit for large file sets. During one of my quick and dirty performance tests, I tried staging all of the changes in the JIRA codebase between JIRA 5 and JIRA 6.

1
2
3
$ git checkout jira-v6.0
$ git reset jira-v5.0
$ time git add --all

Under git 2.0.3 I averaged 2.44 seconds. git 2.1.0 on the other hand averaged 2.18 seconds – a time saving of over 10%! Note that this wasn’t exactly done under laboratory conditions, and we’re saving about a quarter of a second when adding 14,500+ files to the index at once, so you may not see a huge impact during normal git usage. You can read more about the new split index format in the index-format documentation.

The “core.preloadindex” configuration variable is enabled by default, allowing modern platforms to take advantage of their multiple cores.

Nice! I hadn’t enabled this feature before, but after upgrading to 2.1.0 the performance difference is noticeable. As another quick and dirty test I tried timing runs of git status against the staged changes between JIRA 5 and JIRA 6 I used above. Under git 2.0.3 I averaged 4.94 seconds with 14,500+ files staged and git 2.1.0 averaged 3.99 – an impressive saving of ~19%. This is especially good if you use a custom shell prompt that checks whether your working copy has uncommitted changes every time it returns. Bash certainly felt a bit snappier for me for very large indexes.

“git blame” has been optimized greatly by reorganising the data structure that is used to keep track of the work to be done.

git blame is now faster at figuring out who broke something contributed a particular line of code :). I’m very happy about this particular improvement, as it means git-guilt (a little tool I wrote for investigating how blame changes between commits) can also expect a nice performance increase, as it heavily relies on blame output to function.

As another quick and dirty test, I checked the time it took to calculate the guilt transfer in the git source repository between 2.0.0 and 2.1.0. This causes git-guilt to fork 886 git blame commands on files of varying sizes that changed between git 2.0.0 and git 2.1.0.

1
$ git guilt v2.0.0 v2.1.0

git 2.0.3 averaged 72.1 seconds and git 2.1.0 averaged 66.7 seconds, an improvement of 7.5%! You can see the actual git-guilt transfer if you’re curious (Karsten Blees just edged out Junio C Hamano by 66 LOC).

These performance tests are all a bit on the ad-hoc side, but we’re in the process of upgrading Bitbucket to git 2.1.0. We’ll be monitoring features both pre- and post-upgrade to see how the new version effects the performance of certain git operations, particularly blame and diff. I’ll report back in a few weeks to let you know how things go.

But wait, there’s more!

There’s some other great stuff in git 2.1.0 that I haven’t covered in this blog post so go check out the full release notes if you’re interested. Huge kudos are due to the git team for another quality and feature-packed release. If you’re interested in reading more tips and tidbits from the world of git please feel free to follow me (@kannonboy) and the Atlassian Dev Tools (@atldevtools) on Twitter.

About Tim Pettersen

I'm a veteran Atlassian developer with almost a decade of service across the JIRA and Bitbucket teams. I speak and blog about developer workflows, Git, CI/CD, Java, and Atlassian's developer tools. Talk to me about plugin architecture, Node.js, Java, DVCS, or anything cool that you're hacking on!

View all posts by Tim Pettersen »