How To Craft Your Changes Into Small Atomic Commits Using Git
Software Engineering Team Lead and Director of Cloudsure
Small, atomic commits makes it easier for code reviews, browsing the history and reverting changes. Life happens and commits can touch more lines and files than I want but the changes committed should be distinct. "Don't mix your apples with your toaster."
TL;DR: Use
git add -i
orgit add -p
. Stage the files you know are related. Break other mangled changes in files into smaller chunks (known as hunks) using patch mode. Stage. Commit. Do it again. Be careful.
In this post I am going to show you how to craft your changes into small atomic commits. This means that you are going to break up your changes into separate pieces of work and commit them.
Let's get things straight
It was brought to my attention in Reddit by WetDynamics that "atomic commits sound nice in theory but in practice you end up with 100 commits of "extracted foo into a method" or "refactored bar to make it more readable". Does it really make your git history easier to grok than a single commit focused on a feature?" So I wrote about it.
Stop!
Know what you are doing. If you make a mistake by omitting a file or leaving out a chunk of a file you will have a broken commit.
Doing this is much easier when you keep your changes as small and simple as possible. Although you can scale up, the more complex your changes are, the more your brain has to work and the larger the chances are of you making a mistake.
Approaches
There are multiple ways to breakdown changes into smaller commits:
The not so cool way
- Commit the files that are related. Make a copy of the file with multiple changes, revert the other changes in the file, add the new changes to the file and commit it. Put your changes back for the next commit. No more stone age processes. 😄
The Git way
- You can use the interactive mode approach to add the files you want and choose parts of the files you are interested in committing.
- You can use the patch approach directly.
Getting started
git diff layout.scss
will give you the diff for that file. You
will notice some seriously stylish changes that I have made.
Quickly check the status of the file in git status
and see that
it is already staged. This file must not be committed in its
entirety so the file must not be staged.
➜ ahoy git:master ✗ 9cc08bf ➜ git status
On branch master
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
renamed: data/ship.json -> data/ships.json
modified: layout.scss
modified: src/pirate.clj
modified: components/parrot.jsx
Crafting atomic changes
This is the meat of what I want to share with you. I am going to show you how to stage files and parts of files to craft your commits. 😎
The interactive mode way
Get started with git add -i
or git add --interactive
See a
status screen which is like git status
on steroids.
-
To the left you have staged changes. If the file hasn't been staged yet it will display unchanged.
-
To your right you have unstaged changes. If the file is fully staged then it will display nothing.
➜ ahoy git:master ✗ 9cc08bf ➜ git add -i
staged unstaged path
1: +0/-12 nothing data/ship.json
2: +16/-0 nothing data/ships.json
3: +8/-2 nothing layout.scss
4: +4/-4 nothing src/pirate.clj
5: unchanged +2/-2 components/parrot.jsx
You are presented with a bunch of options that will aid you on your journey. Let's take a look at them:
What now>
is the base prompt patiently awaiting your commands.
Type in any of the numbers or letters below:
-
1
ors
to see the steroid status screen again -
2
oru
lets you stage files. When you select it, it will show files that have not been staged. This is equivalent togit add <file>
- If there are none you will stay in the
What Now>
prompt. If there are files to be staged then you will enter theUpdate>>
prompt. - Enter the numbers of the files or the highlighted letters and press
ENTER
. The files are shown again. This time you will notice asterisks next to the files you have selected. They are the one's that will be staged. - Rinse and repeat this process until you are happy with the files you want to stage.
- Press
ENTER
to stage the files. - If you want to quit this mode, just press
ENTER
- If there are none you will stay in the
-
3
orr
will do the opposite of update. It will unstage (or revert) your changed file. This is equivalent togit rm --cached <file>
The process is exactly the same as above except you will be in theRevert>>
prompt. -
4
ora
will add any untracked files. Again, the process is the same but you will be in theAdd untracked>>
prompt. This initiates agit add <file>
-
5
orp
will allow us to selectively choose parts of a file (known as hunks) and stage just that part. You will be in thePatch update>>
prompt. Same story as before. I'll get to this in a bit. -
6
ord
will diff any staged file. Again it is the same above but now you will be in theReview diff>>
prompt and you will be seeing diffs for staged files as you would ingit diff --cached <file>
Patch update mode
You are presented with hunks. The layout.scss
file below only has one hunk.
Uh, not ideal because I want to break it up into atomic commits.
Patch update>>
diff --git a/layout.scss b/layout.scss
index fcc3ea4..8bdf8b6 100644
--- a/layout.scss
+++ b/layout.scss
@@ -1,9 +1,15 @@
+h1 {
+ font-size: 3em;
+}
.parrot {
background-color: blue;
+ border: solid 1px green;
}
.ship {
- background-color: green;
+ background-color: brown;
+ font-size: 2.5em;
}
.pirate {
- background-color: red;
+ background-color: black;
+ border: solid 1px green
}
Stage this hunk [y,n,q,a,d,s,e,?]?
Ahoy matey, there be options:
y - stage this hunk
n - do not stage this hunk
q - quit; do not stage this hunk or any of the remaining ones
a - stage this hunk and all later hunks in the file
d - do not stage this hunk or any of the later hunks in the file
s - split the current hunk into smaller hunks <-----Booya!
e - manually edit the current hunk
? - print help
Press s
to split that hunk into baby hunks. Now there are more options
Stage this hunk [y,n,q,a,d,K,g,/,e,?]?
...
g - select a hunk to go to
/ - search for a hunk matching the given regex
K - leave this hunk undecided, see previous hunk
...
Git was able to split that file into 4 hunks which you can choose to stage or not. Stage the ones that are relevant to your commit and leave the rest for now. You won't lose those changes.
Hunk #1
@@ -1,2 +1,5 @@
+h1 {
+ font-size: 3em;
+}
.parrot {
background-color: blue;
Hunk #2
@@ -1,4 +4,5 @@
.parrot {
background-color: blue;
+ border: solid 1px green;
}
.ship {
Hunk #3
@@ -3,5 +7,6 @@
}
.ship {
- background-color: green;
+ background-color: brown;
+ font-size: 2.5em;
}
.pirate {
Hunk #4
@@ -6,4 +11,5 @@
}
.pirate {
- background-color: red;
+ background-color: black;
+ border: solid 1px green
}
When a hunk cannot be split you will see the following in which can you can edit the hunk.
Sorry, cannot split this hunk
@@ -9,4 +9,8 @@
"pirate": "Sailor Tom the Plank",
"parrot": "Major Idea"
}
+ {
+ "ship": "Mother Octopus",
+ "pirate": "Pi",
+ "parrot": "Log"
]
This @@ -1,9 +1,11 @@ line specifies the boundaries of the hunk to change. The -1,9 means the original portion of code started at line 1 in the code below and went for 9 more lines. The +1,11 means that we want to replace the original portion with the following code starting at line 1 and spanning the next 11 lines. ~ http://blog.jimbaca.com
To wrap it up, select the related files and create the related hunks. Stage them. Quit and commit. Do it again for the next change.
The Git patch way
You can also use git patch directly. git add -p
or
git add --patch
will take you into patch mode.
- reset files
git reset --patch
- checkout out parts of files
git checkout --patch
- stash parts of files
git stash save --patch