05 Nov 2020
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.
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.

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.
19 May 2020
I often find myself wanting lldb to evaluate an Objective-C expression when the current language is Swift.
I’ve drastically improved my debugging experience by adding a couple of command aliases to make this a quick thing to do.
In my ~/.lldbinit
I’ve added the following:
command alias set-lang-objc settings set target.language objective-c
command alias set-lang-swift settings set target.language Swift
Why?
Here’s a couple concrete examples of how changing language can make things simpler
Calling private selectors from Swift
Calling non public selectors is a pain in Swift.
A few selectors I commonly want to use are:
+[UIViewController _printHierarchy]
-[UIView recursiveDescription]
-[UIView _parentDescription]
-[NSObject _ivarDescription]
In order to call these in Swift I have to use value(forKey:)
- this is more characters to type and includes speech marks, which my fingers are generally slightly slower and more error prone at locating.
po UIViewController.value(forKey: "_printHierarchy")
po someView.value(forKey: "recursiveDescription")
Swapping the language to Objective-C removes a lot of the boilerplate typing:
set-lang-objc
po [UIViewController _printHierarchy]
po [someView recursiveDescription]
Calling methods in Swift when you only have a pointer address
Often I don’t have a variable in my current frame as I’m just grabbing memory addresses from other debug print commands or the view debugger (awesome use case).
Once you have a memory address in Swift you have to do the following to call a method:
po unsafeBitCast(0x7fd37e520f30, to: UIView.self).value(forKey: "recursiveDescription")
Changing the language to Objective-C this becomes considerably simpler:
set-lang-objc
po [0x7fd37e520f30 recursiveDescription]
One more thing…
In addition to the two aliases above I’ve had the following alias (that came from StackOverflow at some point) in my tool bag for a long time.
It’s really helpful for single shot commands where I want to evaluate Objective-C without changing the language for subsequent commands.
Back in my ~/.lldbinit
I have:
command alias eco expression -l objective-c -o --
Which allows me to run a single command in Objective-C whilst the language is currently set to Swift
eco [0x7fd37e520f30 recursiveDescription]
17 Apr 2020
I was pairing on a task and my colleague watched me repeatedly call the same git commands over and over in the terminal.
I’d gotten into such a rhythm of calling the same sequence of commands that I hadn’t thought to codify it into one step.
My colleague called me on it so now I’m writing a mini post in the hope it will force me to learn the lesson and share a helpful command.
The process I was repeating was for tidying up my local git repo when work had been merged on the remote.
I was following a two step process of pruning remote branches and then removing local branches that have been merged.
The second part is more complex so let’s break that down first.
Removing local branches
We need to start by getting a list of branches that are merged:
➜ git branch --merged
bug/fix-text-encoding
feature/add-readme
master
* development
The output above shows that there are some branches that we probably don’t want to delete.
master
is our default branch so we want to keep that around and the branch marked with *
is our current branch, which we also want to keep.
We can protect these two branches by filtering our list:
➜ git branch --merged | egrep --invert-match "(\*|master)"
The grep here is inverted so it will only let lines through that do not match our regular expression.
In this case our regular expression matches the literal master
or any line starting with an asterisk.
The final thing to do with this filtered list is to delete these branches.
For this we pipe the output into git branch --delete --force
:
➜ git branch --merged | egrep --invert-match "(\*|master)" | xargs git branch --delete --force
Removing references to remote branches
This is much simpler and can be achieved with:
Tying the two things together we get a final command of:
➜ git fetch --prune && git branch --merged | egrep --invert-match "(\*|master)" | xargs git branch --delete --force
Giving it a name
The above is quite a long command and although you can use your command line history to find it, we can do better.
I chose the name git delete-merged
for my command, which can be achieved by adding an alias to my global gitconfig
file:
➜ git config --global alias.delete-merged "\!sh -c 'git fetch --prune && git branch --merged | egrep --invert-match \"(\*|master)\" | xargs git branch --delete --force'"
With this in place I can now just call: