The journey towards smaller pull requests
I've seen many thought leaders argue for smaller pull requests and changes. But how do you do that, without losing flow or impeding progress. I delve into my observations and tricks.
I was reading through Tidy First and read about splitting behavioral changes and structural changes. I.e. "Does a PR change how the system works" compared to simply the structure of the code, i.e. small tidying. It reminded me of Mark Seeman’s great post on 10 tips for better Pull Requests, which argues:
“Just as the Single Responsibility Principle states that a class should have only one responsibility, so should a Pull Request address only a single concern.
He talks about batch size and presents the following tradeoff space:
I can recommend reading the full post, which touches on some of the same points as Mark Seeman’s Agile Pull Requests post where he among other things argues to minimize the price of review:
This correlates with my general focus on increasing my Continuous Deployment practices... But it left me with "how?". For me, review is not the only ‘price’ for small batch sizes. Even though the steps are not that time-consuming, creating a new branch, making a pull request, giving it a reasonable title, etc, breaks flow and aren’t instantly gratifying.
I initially wanted to message Kent Beck asking his perspective, and started to write out something along the lines of:
Hi Kent. Thank you for writing a great book! You’ve inspired me to make smaller pull requests, and however i sti understand the principle of creating changes in somewhat atomic commits, potentially cherry-picking into a clean branch if I do a minor tidy, or better yet; change branch and create my tidy (or change) there, creating a pull request. But I'm a not good at delayed gratification. There is a lot of steps I have to do, and even though I know that there is value in it, those step are expensive in the moment and break flow. How do I lower that price?
I wanted to end the sentence with a variety of ideas/options, and in that process, I had a rubber-ducking realization, which I’ve made a habit of since.
Automating the boring stuff
Is the title of a book, but also possibly the mentality of an engineer. Steps that you do quite often can be optimized. Mentally, I went through my normal procedure
Working on some behavioral change
Realize there is some code smell, potential for tidying, or some needed derived functionality
stash (or commit) my current WIP changes on the initial feature.
Check out the main branch, and pull it
Create a new branch
Make the potential change.
Test it
Create a pull request.
Sometimes the suggested change is very closely tied to whatever feature I am working on. Maybe I realize that in the current edge case, a given function is slow, so I want to optimize it. In that case, I’d make the change in a standalone commit in the same branch, again test it - and then cherry-pick the changes into a new clean branch, and again create a pull request.
Creating Pull requests with ease
One big hurdle was the steps involved in creating a pull request; opening a browser, finding the repo, click, click, click, add a title, click, click, click, share it...
Then I realized... There is such a thing as command line interfaces!
The Github CLI has gh pr create
, and the Azure DevOps CLI has az repos create
. More generically Gitlab supports push options, enabling you to write git push -o merge_request.create
without installing any CLI.
This made my engineering fingers tingle.
I, however, wanted something simple that gave me a PR and returned the URL to the PR so all I had to do was share it in the relevant channel. It’d also be neat if it was usable by my coworkers too. We generally use MAKE
in our projects, so that seemed like a great destination for a helper. Some struggling later I had:
❯ make PR
Creating pull request...
PR: Tidy Payment.Evaluate (#2788)
URL: https://{...}/pullrequest/2788
I didn’t have to do anything regarding the title; if my branch only has one commit, it is automatically sourced from there. This suddenly made it much faster. This also opened the door for additional functionality, like:
❯ make draftPR
And so on. flags etc could even enable triggering auto-completion or similar if applicable.
Getting Git Great
Git also can create custom Git aliases - and here one can go berserk. I’ve heard arguments against creating aliases from different people; generally arguing that people then forget or (never learn) the core git operations. I’d argue that if you make them yourself (and as a result of prior pains) it’s just an optimization of your workflow. By not giving you the code to implement the following, I can at least argue I’m innocent in that regard. I have about 100 different aliases. Some are mainly jokes or gimmicks, like:
❯ git whorank
765 Casper Weiss Bang
498 Casper Bang
...
Which shows how many commits each person has made in a given repo. It doesn’t do anything and isn’t representative of anything.
Others I use almost all the time, like:
❯ git com
Switched to branch 'master'
Your branch is up to date with 'origin/master'.
This is great because it works both in repos utilizing main as well as master, for their trunk branch - further proving that changing the main from one to the other, isn’t that big of a deal. (I haven’t supported trunk yet, so please don’t..!)
In the context of this specific problem, however, I realized that the following became tiring:
❯ git com
Switched to branch 'master'
Your branch is up to date with 'origin/master'.
❯ git pl
Already up to date.
❯ git cob tidy-that-thing
Switched to a new branch 'tidy-that-thing'
So.. an alias’ of aliases later:
❯ git cleanBranch tidy-that-thing
Switched to branch 'master'
Your branch is up to date with 'origin/master'.
Already up to date.
Switched to a new branch 'tidy-that-thing'
Which might save me a few milliseconds, sure, but I am a short-sighted lazy annoying human; I’ll do whatever is in my power to not do the best thing, if it seems cumbersome.
Micro sidebar
I just realized that you can update a given branch without checking it out, like this:
git fetch origin master:master
And create a branch with another source like this:
git checkout -b tidy-that-thing master
That made me change the git cleanBranch
implementation a bit, however, that’s the great thing about abstractions; you can optimize behind the scenes. A benefit of this approach is that my alias git back
goes back to the prior branch, rather than the trunk.
Also yes - most aliases have to be puns.
Putting it all together
These are all great, but what is the flow then?
Working on some feature
Realize I have to tidy something.
Commit or stash what I have.
git cleanBranch tidy-that-thing
Tidy code
Ensure it works
Commit, and observe commit ID
make PR
Send link to coworkers
git back
As mentioned, there are cases where the change is inherently required to continue work in the current branch. In that case, I normally use stacked pull requests - A concept I’ll write about soon. But it’s that simple.
Simple is not easy
Atomic commits and pull requests are inherently quite simple. But I am not saying it’s easy. Doing the above still requires you to have good commit-hygiene, while ensuring that your changes are, to some extent, not coupled... That is not necessarily easy to do. It requires practice and experience, and no blog post can give either.
Furthermore, no matter how good an idea or practice is; you still need to do it. My monkey brain sometimes impacts my ability to do what is good for me. But by reducing steps, I at least cannot use its time-consuming as an excuse.