Rewriting your git history to share concise, readable changes

By Steve Brewer

If you want humans to work on your code in future, it needs to explain itself. When developers find your code and have no clue what it's doing, it’s rarely because they’re inexperienced or incompetent, it's usually a lack of readable code or documentation explaining what you were thinking.

Don't expect other developers to spend hours investigating your work to be able to contribute to it. It's your responsibility to make your code play nicely with humans so it can evolve, instead of stagnating because nobody will touch it.

But how?

For as long as you're working on a branch and until you merge it into master, you can edit, reorder and drop commits as you please.

In this article I'll share my process for trimming branches down into a concise story, making it easier for others to read, understand and review.

I learned this approach working with Seb Jacobs and Joel Chippindale at FutureLearn, where I had the luxurious abundance of well-documented, readable code.

Rails expert Sarah Mei also backs this kind of approach to commits, and she’s a developer who knows how to communicate effectively.

Inspect any line of code with git ‘blame’

Many developers start off writing code and committing before they get to the stage where they’re investigating existing code, but I’ll begin with this because it’s the ‘why’ behind this whole approach.

When you’re working on new code and you come across something like this:

1 <% @users.each do |user| %>
2     <% @user = user %>
3     <%= user.name %>
4 <% end %>

It’s not obvious why each user in the loop is being declared as an instance variable, and I can’t find where @user is being used. Maybe it’s been deleted, maybe I just haven’t spotted it, maybe it’s part of a microservice? Who knows.

The first place I check is git blame app/views/users/index.html.erb and look at line 2, which should give me the hash of the last commit that modified that line, which in this case is probably the one that created it.

git1

Let’s git show 77d281f and oh dear…

git2

It was me. And I didn’t give any clues about what was going through my mind that day (it was today 🙄).

If this were more complicated, I might then spend another few hours trying to figure out why this was written like this, only to find the reason it’s there in the first place is long gone and that line can be deleted, avoiding all this confusion.

So how do we get to a place where our commits reveal the intent behind our code to make it easier for people to work on it?

Make frequent, small commits to condense and re-order later

Committing frequently, even with messages like ‘wip’ allows you to move around the changes you’re making within your branch in an interactive rebase.

Here’s one commit you might add:

wip initialise user controller

class UsersController < ApplicationController
  def index
    # TODO: Variables 
  end
end

You then add the index view, do some styling, then later create the User model.

It doesn’t matter why we don’t write code in the order it makes sense to in the beginning, but when sharing this code with others, it makes their lives easier to put it into a narrative order of how they’d expect to read it.

An interactive rebase allows you to re-order, delete and merge commits into each other.

It’s important to note after you’ve done a rebase locally, you have to force push your branch to GitHub to overwrite what’s there, which means if others are working on the branch you should co-ordinate this with them so they don’t lose changes they’ve made. When they try to pull the branch git will try and merge all the different commits together - it’s a mess.

I'd recommend just pushing commits normally and saving the interactive rebase either for the end, or co-ordinate one when both developers are ready.

When you do rebase, if they have no new changes on their branch, then after you force push they should should git reset —hard origin/branch-name.

If they have changes, you should pull them in before you rebase.

So, git rebase -i gives me the Sublime window below, where I can replace ‘pick’ with the action I want to do to that commit, or I can move the lines around to change order:

pick ac42d2a Add users index
pick 17352fb wip initialise user controller
pick 80da918 wip Add User model
pick a42badc wip Add variables to user controller

# Rebase a42badc onto 3494cb0 (4 commands)
#
# Commands:
# p, pick = use commit
# r, reword = use commit, but edit the commit message
# e, edit = use commit, but stop for amending
# s, squash = use commit, but meld into previous commit
# f, fixup = like "squash", but discard this commit's log message
# x, exec = run command (the rest of the line) using shell
# d, drop = remove commit
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
# Note that empty commits are commented out

For a better narrative order for this branch, I might move the users index view to the bottom to come last as it depends on the model and controller existing.

I might fixup the variables commit into the controller commit (merge them together), put the model commit first because the controller needs it and then reword all the commits now that I’m done:

pick 80da918 wip Add User model
r 17352fb wip initialise user controller
f a42badc wip Add variables to user controller
pick ac42d2a Add users index

I can do that one change at a time to help me deal with any merge conflicts, but once I’m done my branch becomes:

774d7e9 Create User model
c419ed9 Create users controller
7effc68 Create users index

Not only is it easier for others to follow, we’ve got rid of any irrelevant changes, like the ‘to do’ comment we introduced in wip initialise user controller. That was deleted in wip Add variables to user controller and fixing it up into the commit that introduced it gets rid of it completely.

Write full and juicy commit messages

Now the branch is done, it’s time to make sure we’ve documented what we did here.

I’m going back in!

r 70e713e Create User model
r 57814e1 Create users controller
r 57d019c Create users index

Let’s put some more details in the model commit about why we’re doing this:

Create User model

https://trello.com/my-story-ticket

So our clients can login to our app, we need to create a table for them in the
database that will store only the minimum data we need to allow us to give them access to
the service.

# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# Date:      Wed Jun 13 10:36:06 2018 +0300
#
# interactive rebase in progress; onto 03df2d5
# Last command done (1 command done):
#    reword 70e713e Create User model
# Next commands to do (2 remaining commands):
#    reword 57814e1 Create users controller
#    reword 57d019c Create users index
# You are currently editing a commit while rebasing branch 'master' on '03df2d5'.
#
#
# Initial commit
#
# Changes to be committed:
#   new file:   app/models/user.rb
#

I don't need to explain anything that's very obvious from the code that can be seen below the message, just my decisions that will be invisible to fresh eyes, eyes that could be of any experience level and reading this anytime in the future.

I don't recommend waffling, be concise, but there's no word limit, so if you think something might useful to know about in future, talk about it. Waffle is still better than no information, and you never know what might be unclear to future you.

If you made a compromise, say so and why. If you thought about an edge case but decided it wasn't a priority now, say so. If you did some weird hack, say so. Your weird hacks can live long after the problem they solve if you don't explain what that problem is so someone can delete it when it’s redundant.

Formatting commit messages

GitHub doesn’t support markdown in commit messages at the time of writing this, but I still write it in markdown anyway because most developers are familiar with it’s syntax. I use backticks even though they’re displayed as backticks, and I use stars for bullet points.

You can even do ASCII diagrams and emojis for expressive visuals that communicate more vividly.

To make the messages easier to read, the cmd + opt + q Mac shortcut makes your paragraphs ~70 character wide.

Coping with merge conflicts

As the developer working on a branch, you have the most knowledge in the world about what you've done, so you're most qualified to fix merge conflicts. Yes they’re hard, but they’re even harder for everyone who’s not you.

My approach is always to attempt to solve them within a rebase and hope for the best, then read through your commits carefully after to make sure the code is how you expect. This is easier when your commits are small.

A good narrative order of well-named commits also make it easier to solve merge conflicts as it’s easier to understand where you are in time within the context of your branch.

Good test coverage also helps, as failing tests will tell you something broke in the merge.