Imagine you have a programming problem to solve. The problem is a little bit on the border of your competence, and you are not entirely sure what the solution will be when your done. But, on the other hand, it is not the first time in the history of the world this particular kind of problem has been solved. Without a doubt, there is at least one possible solution to the problem.
You start to describe what you want to achieve by writing an automatic unit test. Quite soon you bump into an obstacle in the form the API you need to use. The API is not a good fit with the current design of the code and makes writing the test unnecessarily tedious. You see a way to fix this, you change a little code and test cases in a related part of the system to make the API usage easier to test. But then, you find another part of the code that depends on the code you just changed. So you update that other piece of code accordingly, but only after fixing a tiny little piece of code to make the change smooth. A while later you have completed all the different small changes that followed from your first design adjustment. You do the last bit of the test case and it is time to do some programming on the original task you set out with. Thanks to your test case and the alterations you have made, the solution is rather straightforward and not too much code is needed. Well done!
This kind of sequence is common before it is time to introduce the new code into the master branch by the way of a pull-request (PR) that co-workers review.
When your co-workers look at your PR, they notice that it is rather big, and spans tens of files and is a hodge-podge of code library version updates, renamed variables, removed code, lifted methods, extracted methods, etc. In the worst case it is all one single commit, maybe more than one, but not small, nor obviously correct. Simply, a too large PR.
The prescription
Let’s put aside, for a moment, whether or not this PR should be reviewed at all, or be returned to the author, and instead contemplate why tasks often, but not always, turns out this way?
There is no shortage of hints, good advice and guidelines for how to make good PR:s, good commits, good commit messages, good refactorings and so on. There are also tools, some good, some less good, to help the programmer reconstruct a sequence of commits for a PR, in contrary to actual order of the code changes being done. Git, for example, offers tools that allows us to save pending code changes out our way (git stash), and tools that allow us to select and single out changed parts into own commits (git add –patch). So, there is really no reason for PRs to end up this way! Or?
I notice that I sometimes manage to create better and sometimes worse pull-requests, in regards to its readability. I also notice that even co-workers that almost always succeed in submitting pull-requests as a string of logical, clear and almost trivial commits, can make good and sometimes better PRs.
On occasions, I find myself with a big mess of code changes and my attempts to split the changed rows into commits (git add –patch) does not go well. My desire to start all over again, now that I know the steps, is miniscule as I have already spent enough time on this particular problem. Besides, the solution and the resulting code is not bad! It might very well be really good. It’s just that the pull-request will be messy and harder to review, which increases the risk of bugs slipping through my co-workers’ tough but fair reviews.
And I know that the reason I end up in this situation is the fact that, to me, programming often is a non-linear activity, where the main task, solving the original problem, is interleaved by the need to change code, solve sub-problems, improving conditions to solve sub-problems, etc.
And, I know that when I structure my work as a stack, I can push what I have done thus far onto the stack (git stash), so that whenever I start something that I would want to present as an individual and separate change, it is both possible and trivial. This new interleaving activity may in turn need to go onto the stack as yet a new change is needed.
This pattern can be repeated multiple times and by committing a small commit at the finishing of each sub-task with a concise message, a sequence of individual commits will come out. A sequence of commits that each introduces a small and almost trivial change. Running your tests each time ensures that the commit would become a successful build, too. Then, to continue, I simply pop the top task from my stack (git stash pop) and carry on to solve it, then produce a small commit, and so forth.
Git helps me keep track of my code changes when I work like this. But, I, myself, must prepare so that when a sub-task is completed and committed, I can quickly return to the previous task and focus my mind on my previous line of thought and continue without any delays. We know that task switching can be expensive, so I try to make the life of my future me easier by writing a little instruction whenever I stash, e.g. like `git stash save CONTINUE-with-adding-read-only-checks`, as the git default message when stashing is much less helpful.
The reality
So, even though I know a way that is better, why do I not always succeed with this well-arranged pull-requests?
Maybe I do not value well-ordered PRs enough? In my personal experience, the difference between reviewing easily readable and logically arranged commits in a pull-request compared to one similarly sized commit is distinct. Also, the value of small commits is clear when I need to locate which commit that introduced a regression (git bisect). And of course, the value to my future self when months away from now I need to review the history of a certain piece of code to better understand why it looks like it does.
And, even though I know about and master the tools I need to succeed; unit-tests, git stash, git add –patch, etc?
What if it to succeed takes that me as a programmer focuses my consciousness on two things at the same time? Namely, 1) the problem solving that is my original and main task, and 2) conscious reflection on my ongoing work process.
I need to be aware of what I am doing in two different ways. The first, and most obvious, way is that I need to be aware of: the problem I am trying to solve and the tools and solutions alternatives I have available. The second is that I need to diagnose what step I am currently taking. Whether it is small and isolated enough to be obvious to someone that will review my change?
I must therefore, dedicate a part of my consciousness to decide if what I currently do is a direct part of solving the original problem, or if it is a refactoring/preparation, and then immediately decide if I should stash my workspace, and make a commit of the small change.
In situations when this is challenging to me, either because I am a novice in the programming language, the technology, the algorithm or the problem domain, most of my focus will be on the first part, the solving of the original problem. Very little, if any, of my mind will concern itself with reflecting on the type of change I am doing at each moment, and therefore I will neglect to stash, and commit the small steps that I do. The result, of course, is not a nice string of small and obviously correct commits.
The paradox
Is this an essential paradox? That, when I do, to me simple problem solving and programming, my pull-requests will turn out neat and nicely ordered, but when I work with complex problems with no solution known in advance, and really could use that extra bit of reviewing of my code, I will make messier pull-requests that will make reviewing more difficult?
I don’t think it has to be a paradox, but I notice that it often turns out that way.
What if we could practice noticing these logical programming changes, such the behaviour become reflexes? As reflexes, noticing micro task changes would not burden our conscious mind. Then, in theory, we could remain completely focused on our original problem solving at the same time as we, by reflex, make our tracks in the source code history, small and obviously correct.
Compare this idea to learning to ride a bicycle. To the complete beginner, biking requires full attention of your mind and body. Even making your feet turn the pedals requires a conscious thought. To us that have been biking since early childhood, the actual biking require so little of our consciousness that we can look around, keep a conversation with our friend, read a map, answer calls on the mobile, or even do tricks, like letting go of the handle-bars.
The bike-ride
Maybe it is so, that the behaviour that makes our work turn out like a series of logical commits can be practiced, much in the same way as biking? In that case, the desired behaviour would eventually turn so fully internalized that we would do it without spending a single thought on it?
Then we could practice this behaviour, so that we could indeed focus even more of our mind’s attention on making good, clear and elegant solutions, and, at the same time make our pull-requests easy to review to our co-workers and our future selves.
Do you recognize yourself in this essay? Do you have any advice or experience on how to get maximal attention on finding good and elegant solutions and still leave neat and tidy traces in the code repository? Join and share on #softwareexcellence.