Atomic Commits with Git

You know atomic commits are the way to go, but you struggle making them happen with Git. Nathan shares a simple, step-by-step way to create atomic commits in the command line.

One of the reasons I fell in love with Git several years ago was because of how simple and intuitive the command line tool was. Using command-line tools is almost always a productivity boost, and using Git on the command line is no exception. However, I found myself firing up SourceTree (a Git GUI application) on a daily basis. Why?

Atomic commits.

What are Atomic Commits?

One of the best practices in version control, no matter what tool you use, is to keep your commits atomic.

What does that mean?

There are a few definitions for atomic in the dictionary, but this one is the most relevant:

atomic: of or forming a single irreducible unit or component in a larger system

When making code changes, you want to make commits that are generally smaller and that encompass only one irreducible feature, fix, or improvement. I recently created a simple contact form for a small site, and here is a truncated example of what my commits looked like.

  • Create HTML for form

  • Style form

  • Add HTML5 validation

  • Fix unrelated JS bug

  • Add ajax submit to form with mock server results from PHP

  • Add JS validation to form

  • Send form results via email

  • Log form results to database

  • Style form validation and success/error messages

Each of these commits adds either a bare-minimum feature, a fix, or a small improvement to the feature or code. The semantic commit message style we use at Sparkbox requires our commits to be atomic.

If you are publishing a change log from your commit history, as Rob Tarr recommends, you may want to rewrite your local history before you push, to combine smaller feature commits into a single one like “Create working contact form.”

Why Atomic Commits?

Code Reviews are Easier

When you keep your commits small like this, it is easier for someone to review your code and see each incremental change.

Easier to Roll Back

Let’s say you make a feature change and mix in an unrelated bug fix. Later, someone decides that feature change isn’t all that great, and they want to roll it back. If your feature change commit included only that feature, rolling back would be simply a matter of reversing the commit, but if it were mixed with a bug fix, rolling back would be more difficult (especially if someone other than you has to do it).

Committing Parts of a Changed File

One of the problems I run into trying to keep my commits clean and atomic is that I inevitably make changes to my code that are unrelated to my current task. I see a method from yesterday that I forgot to document, and instead of writing it down as a todo and making the changes later, it’s easier to just make the change now.

When I get ready to commit my changes for the current task, I want to make sure I don’t mix the new documentation into that commit. If the documentation change is in another file, that’s easy. With Git, you stage only the files you want to commit first.

$ git st
M contact.html
M contact.php
$ git add contact.html
$ git st
A contact.html
M contact.php
$ git ci -m "Add required attribute to form fields"
view raw atomic-commits1 hosted with ❤ by GitHub

But what if you have changes in the same file that you need to commit separately? I used to fire up SourceTree, which would allow me to selectively stage “hunks” of code. However, I recently went cold-turkey and uninstalled SourceTree to force myself to only use the command line.

From the command line, you can selectively stage “hunks” of code that are in the same file using git add --patch or git add -p. When you use the patch argument for staging, Git will display each hunk of changes in a file and ask you whether you want to add it or not. Simply choose yes or no (or some of the other advanced options), and then commit your staged hunks when you are done.

Here is a CoffeeScript file with a documentation change and an unrelated naming change. With git status, we see just one file has changed.

$ git status
M coffee/forms.coffee
view raw atomic-commits2 hosted with ❤ by GitHub

To stage hunks of code in a file instead of the whole file, use the patch parameter with your git add command. It will show you the first changed hunk in the file and ask you if you want to stage it or not. We’ll choose “?” so we can see the full list of options, and then we’ll choose “n” because we DON’T want to stage this hunk.

$ git add -p
diff --git a/coffee/forms.coffee b/coffee/forms.coffee
index a9f4d6d..40a8ed8 100644
--- a/coffee/forms.coffee
+++ b/coffee/forms.coffee
@@ -15,6 +15,7 @@ window.xhrForms =
$this = $(@)
$this.mask $this.data("mask")
+ # Submit handler for our ajax forms
handleSubmit: (form) ->
$form = $(form)
url = $form.attr "action"
Stage this hunk [y,n,a,d,/,j,J,g,e,?]? ?
y - stage this hunk
n - do not stage this hunk
a - stage this and all the remaining hunks in the file
d - do not stage this hunk nor any of the remaining hunks in the file
g - select a hunk to go to
/ - search for a hunk matching the given regex
j - leave this hunk undecided, see next undecided hunk
J - leave this hunk undecided, see next hunk
k - leave this hunk undecided, see previous undecided hunk
K - leave this hunk undecided, see previous hunk
s - split the current hunk into smaller hunks
e - manually edit the current hunk
? - print help
Stage this hunk [y,n,q,a,d,/,j,J,g,e,?]? n
view raw atomic-commits3 hosted with ❤ by GitHub

Then, it presents the next hunk. This time, we’ll choose yes.

@@ -40,7 +41,7 @@ window.xhrForms =
$msg = $(".form-notice", $form)
if !$msg.length
$msg = $("<div/>").addClass("form-notice").prependTo($form)
- $msg.removeClass "form-success"
+ $msg.removeClass "form-success form-error"
if data.status
$msg.addClass "form-success"
else
Stage this hunk [y,n,q,a,d,/,K,g,e,?]? y
view raw atomic-commits4 hosted with ❤ by GitHub

If we check the status again, we’ll see that our file is partially staged.

$ git st
MM coffee/forms.coffee
view raw atomic-commits5 hosted with ❤ by GitHub

Finally, we can can commit the code changes we just staged.

$ git commit -m "Remove form-error class too"
view raw atomic-commits6 hosted with ❤ by GitHub

No Excuses

Sometimes interactive staging can be a bit more complicated, but in general, atomic commits with Git are pretty easy. There is no excuse to continue committing your bug fixes with your features. By keeping your commits atomic, you will add some sanity to your workflow, and your fellow developers will thank you.