Pretty much every minute or so.

This answer seems to surprise many, so I thought I'd flex the old writing muscles and justify this seemingly-bonkers process.

Commits are Save Points

The easiest way of visualising a change (herein: a commit) is like a save point in a video game. You're creating a snapshot of how things are at a point in time, writing this down, and giving yourself the ability to go back to that point in time.

In older version control systems, creating a commit was quite formal as the change was synced immediately to the central server. This put a big onus, socially-speaking, on making them meaningful and 'complete'; they were expected to work.

I found this restrictive when working locally. The tooling constrained how I worked - I ended up writing commits that were quite large, corresponding to a feature or a significant chunk of a large feature. The push towards 'all or nothing' meant I wasn't giving myself room to experiment and I often lost track of smaller changes within a wider change.

Recently I've been converting some JavaScript code to replace JQuery with native-DOM functionality. Within a particular file are lots of individual chunks of JQuery that I'm tackling one by one. Sometimes these are easy, sometimes it takes a bit of experimentation to get right. When doing the latter fiddly changes it's common to undo the change and try a different technique. Effectively, I want to go back to the save point!

But I don't want to have to undo the changes to the entire file to get to that save point - because I'd be throwing mostly working code away. So here's what I do.

'Public' Commits vs 'Work In Progress' Commits

Git's distributed nature means that changes you apply locally aren't shared with the rest of the team until you push. I can commit what I want, when I want, how I want. While commits are local, they're mine to play with and do as I please. I can use the tooling to empower how I work, rather than constrain it.

In the example above, I'd commit every time I got an individual JQuery replacement working. I'd also commit when I need to go to a meeting, or when I leave my desk for a tea/toilet break. Or at the end of the day. Very often these commit messages will be terse and very contextual. "brb going for a wee" is a common occurrence in my commit message! I can then have a look through my git log to get an accurate understanding of where I'm at with a task. I don't need to worry about going into a huge amount of detail unless I think it would be helpful - the last commit of the day commonly has a bullet-list of thoughts and TODOs.

git log --oneline -n 5 is a compact way of showing the last five commits in a terminal. I have this aliased to git slg because I use it so much.

Now, I can guarantee that when I need to leave my desk, the commit I make won't compile. That's fine. The reason I do it anyway is to help defend against the "Doorway Effect". Ever walk into a room only to forget why you're there? Or even worse, what you were doing before? Well, by creating a small save point just before stepping through the doorway, I can easily jump back into that old frame of reference.

To undo a commit in git AND put those changes back into your local staging area, you can run git reset --soft HEAD^. I run this when I get back to my desk to reinstate that frame of reference.

(To backup these piecemeal changes somewhere remote, you can push them to a branch on origin, or if you want to keep that prim and proper, onto a private fork. Most source control services like GitHub, BitBucket and GitLab allow for private forks of repositories. They're useful dumping grounds and a godsend if your laptop decides to die.)

When I'm ready to share the work I've done, I now need to go through and turn these 'work in progress' commits into more meaningful, well-structured and properly documented commits. I call these 'public' commits because I'm going to push them to the remote repository. They'll be reviewed and become part of the project history.

With the type of work in progress commits I write, I know I need to go back and tidy these up. This gives me the opportunity to perform deliberate practice in writing good commit messages. Indeed, it forces me to!

To take these commits and arrange them into commits for publication, I'll use the rebase command. This also presents me with an opportunity for deliberate practice with rebasing, so that when I need to use it for realsies it's scary and undaunting.

  • First, I'll make sure my Git's understanding of the origin repository is up to date with reality: git fetch origin.
  • (Let's say I branched from the develop branch). I'll then make sure my changes are applied on top of what's in the base branch: git rebase origin/master. This will replay every commit one-by-one, asking for fixups if there are any conflicts.
  • I'll check everything works, then perform an interactive rebase on all of my unmerged (to develop) commits, so that I can tidy them up: git rebase -i origin/develop

Interactive rebase is a mechanism for you to instruct git what to do with each individual commit you have. A text editor will open with a list of commits and explain the options available. You can, amongst other things:

  • keep a commit as is (pick, or p for short)
  • keep a commit as is, but reword/r its commit message
  • squash/s two commits into one (combining both their commit messages too)
  • fixup/f a commit - this squashes it onto the previous commit, but discards its message

I'll do some thinking as to what the commits are, and then us a combination of reword and fixup to build out the commits that I want. Once you save and exit the text editor, the instructions are executed one-by-one on the commits, in the order specified.

In the text editor that's opened up when performing the interactive rebase, you can also re-arrange the commits. This is really useful if you added a "quick fix" commit to something and want to combine it into the original commit where that should have been made, but that commit is several commits back in the history. Sneaky!

Making the Rebase Experience Nicer

Note, to make this a bit easier to work with, I have the following set in my ~/.giconfig file:

[rebase]
  abbreviateCommands = true

This turns:

pick f7f3f6d Change my name a bit
pick 310154e Update README formatting and add blame
pick a5f4a0d Add cat-file

Into:

p f7f3f6d Change my name a bit
p 310154e Update README formatting and add blame
p a5f4a0d Add cat-file

Being Paranoid With Changes

One of the interactive rebase options, on top of the ones above, is to "exec"/"x" a command. You can manually add one of these after each commit in the interactive rebase to, amongst other things, build and test your code. But doing that is time-consuming and boring.

So let's say that running the command npm test builds and executes our unit test. We can automatically run this command after every commit in our interactive rebase without having to manually add it, by adding it to the git rebase command:

git rebase -i -x "npm test" origin/develop

Now, if a npm test fails, the rebase stops at this point. I can investigate, make any changes and git rebase --continue when I'm happy.

How Do You Do It?

This workflow has suited me well for several years. I'm not saying the way I use git is right or wrong. It's just what I do. I'd love to hear about how you use git, or any improvements to my workflow. Please do get in touch!