Just a couple of months after version 2.8, Git 2.9 has shipped with a huge collection of usability improvements and new command options. Here’s the new features that we’re particularly excited about on the Bitbucket team.
git rebase can now exec without going interactive
Many developers use a rebasing workflow to keep a clean commit history. At Atlassian, we usually perform an explicit merge to get our feature branches on to master, to keep a record of where each feature was developed. However, we do avoid “spurious” merge commits by rebasing when pulling changes to our current branch from the upstream server (git pull −−rebase) and occasionally to bring our feature branches up to date with master (git rebase master). If you’re into rebasing, you’re probably aware that each time you rebase, you’re essentially rewriting history by applying each of your new commits on top of the specified base. Depending on the nature of the changes from the upstream branch, you may encounter test failures or even compilation problems for certain commits in your newly created history. If these changes cause merge conflicts, the rebase process will pause and allow you to resolve them. But changes that merge cleanly may still break compilation or tests, leaving broken commits littering your history.
However, you can instruct Git to run your project’s test suite for each rewritten commit. Prior to Git 2.9 you could do this with a combination of git rebase −−interactive and the exec command. For example:
git rebase master −−interactive −−exec=”npm test”
would generate an interactive rebase plan which invokes npm test after rewriting each commit, ensuring that your tests still pass:
exec npm test
pick ed93626 ACE-1294: removed pull request service from test
exec npm test
pick b02eb9a ACE-1294: moved fromHash, toHash and diffType to batch
exec npm test
pick e68f710 ACE-1294: added testing data to batch email file
exec npm test
…
# Rebase f32fa9d..0ddde5f onto f32fa9d (20 command(s))
In the event that a test fails, rebase will pause to let you fix the tests (and apply your changes to that commit):
1 failing
1) Host request “after all” hook:
Uncaught Error: connect ECONNRESET 127.0.0.1:3001
…
npm ERR! Test failed.
Execution failed: npm test
You can fix the problem, and then run
git rebase −−continue
This is handy, but needing to do an interactive rebase is a bit clunky. As of Git 2.9, you can perform a non-interactive rebase exec, with:
git rebase master -x “npm test”
Just replace npm test with make, rake, mvn clean install, or whatever you use to build and test your project.
git diff and git log now detect renamed files by default
Git doesn’t explicitly store the fact that files have been renamed. For example, if I renamed index.js to app.js and then ran a simple git diff, I’d get back what looks like a file deletion and addition:
new file mode 100644
index 0000000..144ec7f
−−− /dev/null
+++ b/app.js
@@ -0,0 +1 @@
+module.exports = require(‘./lib/index’);
diff –git a/index.js b/index.js
deleted file mode 100644
index 144ec7f..0000000
−−− a/index.js
+++ /dev/null
@@ -1 +0,0 @@
-module.exports = require(‘./lib/index’);
A move is technically just a move and a delete, but this isn’t the most human-friendly way to show it. Instead, you can use the -M flag to instruct Git to attempt to detect renamed files on the fly when computing a diff. For the above example, git diff -M gives us:
similarity index 100%
rename from index.js
rename to app.js
What does -M stand for? renaMes? Who cares! As of Git 2.9, the git diff and git log commands will both detect renames by default, unless you explicitly pass the −−no-renames flag.
git clone learned −−shallow-submodules
If you’re using submodules, you’re in luck – for once! 🙂 Git 2.9 introduces the −−shallow-submodules flag that allows you to grab a full clone of your repository, and then recursively shallow clone any referenced submodules to a depth of one commit. This is useful if you don’t need the full history of your project’s dependencies. For example, if you have a large monorepo with each project stored as a submodule, you may want to clone with shallow submodules initially, and then selectively deepen the few projects you want to work with. Another scenario would be configuring a CI or CD job that needs to perform a merge: Git needs the primary repository’s history in order to perform its recursive merge algorithm, and you’ll likely also need the latest commit from each of your submodules in order to actually perform the build. However you probably don’t need the full history for every submodule, so retrieving just the latest commit will save you both time and bandwidth.
Overriding .git/hooks with core.hooksPath
Git’s comprehensive hook system is a powerful way to tap into the lifecycle of various Git commands. If you haven’t played with hooks yet, take a quick look at the .git/hooks directory in any Git repository. Git automatically populates it with a set of sample hook scripts:
applypatch-msg.sample | pre-applypatch.sample | pre-rebase.sample |
commit-msg.sample | pre-commit.sample | prepare-commit-msg.sample |
post-update.sample | pre-push.sample | update.sample |
Git hooks are useful for all sorts of things, for example:
- a pre-commit hook can verify that your tests are passing before allowing a commit to be created
- a prepare-commit-msg hook can automatically prepend your commit message with a JIRA issue key based on your branch name
- a post-checkout hook can verify that the commit you just checked out doesn’t have a broken build
However, up until now, hooks have had a big administrative drawback: you have to copy or symlink them into the .git/hooks directory for every repository you want to apply them to. Git 2.9 introduces a new core.hooksPath config property to override the location of your hooks directory. So:
git config core.hooksPath /etc/git/hooks
will configure your current repository to check for hooks in /etc/git/hooks, or:
git config −−global core.hooksPath /etc/git/hooks
will let you centrally manage hooks for all of your local repositories.
git commit learnt to be permanently verbose
Do you ever invoke git commit and then stare blankly at vim trying to remember all the changes you just made? The verbose flag is for you!
Instead of:
# Please enter the commit message for your changes. Lines starting
# with ‘#’ will be ignored, and an empty message aborts the commit.
# On branch master
# Your branch is up-to-date with ‘origin/master’.
#
# Changes to be committed:
# new file: package.json
#
you can invoke git commit −−verbose to view an inline diff of your changes. Don’t worry, it won’t be included in your commit message:
# Please enter the commit message for your changes. Lines starting
# with ‘#’ will be ignored, and an empty message aborts the commit.
# On branch master
# Your branch is up-to-date with ‘origin/master’.
#
# Changes to be committed:
# new file: package.json
#
# −−−−−−−−−−−−−−−−−−−−−−−− >8 −−−−−−−−−−−−−−−−−−−−−−−−
# Do not touch the line above.
# Everything below will be removed.
diff –git a/package.json b/package.json
index 28bcfc7..825e958 100644
−−− a/package.json
+++ b/package.json
@@ -31,7 +31,7 @@
“@atlassian/gulp-atlassian-amd-name”: “0.1.1”,
“@atlassian/gulp-atlassian-expose-soy”: “^1.0.1”,
“@atlassian/gulp-atlassian-soy”: “^2.0.6”,
– “left-pad”: “1.0.3”,
+ “left-pad”: “1.1.0”,
“babel-core”: “^6.9.0”,
“babel-plugin-react-require”: “^1.0.2”,
“babel-plugin-transform-es2015-modules-amd”: “^6.8.0”,
“babel-plugin-transform-es2015-modules-amd-lazy”: “^0.1.0”,
~
The −−verbose flag isn’t new, but as of Git 2.9 you can enable it permanently with git config −−global commit.verbose
git merge will no longer merge unrelated histories
It’s possible to have multiple branches with completely separate root commits — and therefore separate histories — within the same repository. There are a few good reasons for this, for example: in Bitbucket you can store your project’s documentation on its own branch and serve it as a static website using Aerobatic; or you may have rewritten your master branch to use Git LFS in order to reduce the size of your repository. Regardless, if you or one of your colleagues accidentally ran git merge on two unrelated branches in your repository, it would have some bad consequences for your team. In the former case, your documentation would be quietly merged into your code. In the latter, you’d probably just end up with a horrific set of merge conflicts.
Regardless, merging unrelated histories in Git 2.9+ is a non-issue because git merge won’t let you. Unless you explicitly pass the −−allow-unrelated-histories flag. So, uh, don’t do that!
There’s plenty more!
These are just six items from a list of over a hundred features, fixes, and performance improvements shipped by the Git wizards in version 2.9. Check out the release notes for the full scoop, and then download the latest version or upgrade using your favorite package manager.
Thanks for reading! If you want to chat Git, Bitbucket, or JIRA arcanery please hit me up on Twitter: I’m @kannonboy.