git rebase --exec

In this post we’ll look at the --exec option on git rebase. We’ll run through a couple of concrete examples and explain why you might consider them useful.


In a nutshell the --exec flag allows you to run a command against a range of commits starting at the point you specify and finishing on the latest commit. Some worked examples will hopefully make this a little clearer.


Unit testing all commits

Having a clean history that compiles on each commit is really helpful when debugging especially for times when you have to reach for git bisect.

To validate that all commits are clean before pushing to the remote it would be useful to run the unit tests on every commit. That might go a little like this:

Given the following history

* cadc5160 Integrate onboarding screen into main app
* 47e01676 Implement onboarding screen in sample app
* f5ff3165 Add onboarding-screen feature flag
*   e156c2b3 Merge pull request #1 from feature/update-terms-of-use
|\ 

We can grab the sha of the commit before the new work (in this case e156c2b3) and then run

git rebase e156c2b3 --exec "swift test"

The result will be that git will check out each commit in turn and run swift test. If the tests fail on any of the commits then git stops the rebase and waits for us to resolve the issues.

If you perform the above before pushing you can guarantee that all the commits are in good order. You can also use the same mechanism in your CI pipelines if you want to enforce that each commit should compile and pass tests.


Running a code formatter/linter

Teams often run some kind of code formatter or linter to catch common issues and avoid the same conversations occurring over and over in code reviews. Running these tools is great but ideally we’d want to ensure they are run for every commit to avoid code churn. To illustrate the issue with code churn look at this watered down example:

Below we have 3 commits on a project to the same area in one file. The first commit is implementing a bug fix, the second is adding new work and the last commit is a result of running a formatting tool.

git churn

The result of this history is that when there is an issue and we run git annotate on the file to find out the motivation for the changes we end with all of our changes being attributed to the very unhelpful “Run formatter” commit message. If we wrote meaningful messages on the original commits then that effort is now more difficult to find and causes us to reach for more involved git commands like git log --follow.

This is another great use case for git rebase --exec as we can run the formatter/linter on each commit before we push to the remote. If there are any commits that fail the formatter/linter then git will pause the rebase, wait for us to fix the issues and then continue once we have resolved them.


More

The --exec argument can run any command and decides to continue the rebase or stop depending on the exit status of the command. This means you can write any custom command or program that you’d want to run against a range of commits to verify things and as long as you use an appropriate exit status then git will automatically continue or stop.