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?
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
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|
|$ git add contact.html|
|$ git st|
|$ git ci -m "Add required attribute to form fields"|
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|
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|
|@@ -15,6 +15,7 @@ window.xhrForms =|
|$this = $(@)|
|+ # 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|
Then, it presents the next hunk. This time, we’ll choose yes.
|@@ -40,7 +41,7 @@ window.xhrForms =|
|$msg = $(".form-notice", $form)|
|$msg = $("<div/>").addClass("form-notice").prependTo($form)|
|- $msg.removeClass "form-success"|
|+ $msg.removeClass "form-success form-error"|
|Stage this hunk [y,n,q,a,d,/,K,g,e,?]? y|
If we check the status again, we’ll see that our file is partially staged.
|$ git st|
Finally, we can can commit the code changes we just staged.
|$ git commit -m "Remove form-error class too"|
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.