Neat new features in Git 2.7

January 5th 2016 Tim Pettersen in Git

A quick two months after 2.6, Git 2.7 has been released with a plethora of features, fixes and performance improvements. Here's some of the new stuff we found interesting on the Bitbucket team.

Rounding out git worktree

The awesome git worktree command, introduced in Git 2.5, lets you check out and work on multiple repository branches in separate directories, simultaneously. For example, if you need to make a quick hotfix and don't want to mess with your working copy you can check out a new branch in a new directory with:

$ git worktree add -b hotfix/BB-1234 ../hotfix/BB-1234
Preparing ../hotfix/BB-1234 (identifier BB-1234)
HEAD is now at 886e0ba Merged in bedwards/BB-13430-api-merge-pr (pull request #7822)

Git 2.7 adds the git worktree list subcommand to display your repository's worktrees (and their associated branches):

$ git worktree list
/Users/kannonboy/src/bitbucket/bitbucket            37732bd [master]
/Users/kannonboy/src/bitbucket/staging              d5924bc [staging]
/Users/kannonboy/src/bitbucket/hotfix/BB-1234       37732bd [hotfix/BB-1234]

The git bisect command's support for worktrees has also been improved. The refs that bisect uses to track "good" and "bad" commits have been moved from .git/refs/bisect to .git/refs/worktrees/$worktree_name/refs/bisect so you can run bisects concurrently across multiple worktrees.

For completeness, as of Git 2.7, you can also now clone a worktree on disk. This creates a new, independent Git repository (not another worktree).

Fun-fact: Git worktrees aren't just for branches! When researching the new Git functionality for this post I compared the Git v2.6.0 and v2.7.0 tags in parallel by checking them out in separate worktrees and building them:

$ git worktree add ../git-v2.6.0 v2.6.0
Preparing ../git-v2.6.0 (identifier git-v2.6.0)
HEAD is now at be08dee Git 2.6

$ git worktree add ../git-v2.7.0 v2.7.0
Preparing ../git-v2.7.0 (identifier git-v2.7.0)
HEAD is now at 7548842 Git 2.7

$ git worktree list
/Users/kannonboy/src/git         7548842 [master]
/Users/kannonboy/src/git-v2.6.0  be08dee (detached HEAD)
/Users/kannonboy/src/git-v2.7.0  7548842 (detached HEAD)

$ cd ../git-v2.7.0 && make

A couple of git stash improvements

If you're a fan of git rebase, you might be familiar with the --autostash option. It automatically stashes any local changes made to your working copy before rebasing, and reapplies them after the rebase is completed.

$ git rebase master --autostash
Created autostash: 54f212a
HEAD is now at 8303dca It's a kludge, but put the tuple from the database in the cache.
First, rewinding head to replay your work on top of it...
Applied autostash.

This is handy as it allows you to rebase from a dirty worktree. There's also a handy config flag named rebase.autostash to make this behaviour the default, which you can enable globally with:

$ git config --global rebase.autostash true

rebase.autostash has actually been available since Git 1.8.4, but 2.7 introduces the ability to cancel this flag with the --no-autostash option. I think this is mainly for completeness, as using it only seems to give you the dirty worktree warning:

$ git rebase master --no-autostash
Cannot rebase: You have unstaged changes.
Please commit or stash them.

I might be missing something though, let me know if you have a proper use case!

Speaking of config flags, Git 2.7 also introduces stash.showPatch. The default behaviour of git stash show is to display a summary of your stashed files.

$ git stash show
package.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)

Passing the -p flag puts git stash show into "patch mode", which displays the full diff:

diff --git a/package.json b/package.json
index c876b26..e21eeb3 100644
--- a/package.json
+++ b/package.json
@@ -48,7 +48,7 @@
     "mkdirp": "^0.5.0",
     "byline": "^4.2.1",
     "express": "~3.3.4",
-    "git-guilt": "^0.1.0",
+    "git-guilt": "^0.1.1",
     "jsonfile": "^2.0.0",
     "jugglingdb-sqlite3": "0.0.5",
     "jugglingdb-postgres": "~0.1.0",

stash.showPatch makes this behaviour the default. You can enable it with:

$ git config --global stash.showPatch true

If you enable stash.showPatch but then decide you want to view just the file summary, you can get the old behaviour back by passing the --stat option instead.

$ git stash show --stat
package.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)

As an aside: --no-patch is a valid option, but doesn't negate stash.showPatch as you'd expect.

git filter-branch gained speed (and a progress bar)

git filter-branch is a versatile tool for rewriting git history. Since every commit object has a reference to its parents (and transitively to all of its ancestors), rewriting a particular commit means rewriting all of its successors as well. This means that even trivial history rewriting operations can take some time if you pick an older commit.

Git 2.7 introduces a nifty new progress bar that estimates how much time a filter-branch command will take to complete:

git filter-branch progress bar

As a bonus, filter-branch commands that don't modify the index or tree, now skip reading the index entirely, leading to dramatically better performance. The commit-filter command in the GIF above simply rewrites the Git author associated with each commit name from my real name ("Tim Pettersen"), to my handle ("Kannonboy"), and doesn't touch the tree. It took only ~38s to rewrite the first 1000 commits of Bitbucket Server under Git 2.7.0, versus ~64s under Git 2.6.0: an impressive ~40% improvement. The tests introduced with the performance improvements show even more dramatic savings of ~60%.

Improved negation in .gitignore

.gitignore files let you exclude certain files that reside within your worktree being staged in your repository. You can negate these patterns by prepending a ! to "unignore" a particular file. For example:

# .gitignore
*.json
!cat.json

Will ignore all json files except cat.json.

However, in Git 2.6, you couldn't apply a negation to a file residing in a directory that had been ignored.

# .gitignore
/animals
!/animals/cat.json # <-- this was still ignored (pre Git 2.7)

As of Git 2.7, the second example above also works. You can now apply a ! to "unignore" files in directories that would otherwise be ignored.

But wait, there's more!

These are just a small sample of the Git goodness that landed in 2.7. For the full scoop, check out Junio C Hamano's release notes, or peruse the Git repository yourself with:

$ git log v2.6.0..v2.7.0

If you've enjoyed this post or have questions about Git or Bitbucket, drop me a line on Twitter: I'm @kannonboy.