Selectively select changes to commit with git (or Imma edit your hunk)
Wednesday, 16 November 2011
Sometimes during software development you start working on something and then find out you need to fix something else first.
Has that happened to you too? Well, it has to me.
As a good Git user, you stash your current changes fix the new issue commit the changes and then pop the stashed changes to keep plugging away.
But, sometimes you get caught in the thrill of the moment (more like the omg-I-have-to-fix-that-bug-now of the moment) and forget to stash.
Has that happened to you too? Well, it has to me.
What happens next is that you end up with changes for two different and probably unrelated things.
The problem, explained
Let’s say you’re writing a story about your site and your first version of the file is done and you’re ready to commit your initial file.
This is a lovely fly.
Once upon a time there was a website called <name>.
So you commit it:
> git add test.txt
> git commit -m "Initial file"
[master (root-commit) 113eda8] Initial file
1 files changed, 3 insertions(+), 0 deletions(-)
create mode 100644 test.txt
Then you go on to add more text to your file…
> git diff
[...]
@@ -1,3 +1,9 @@
+My great, great website story
+=============================
+
This is a lovely fly.
-Once upon a time there was a website called <name>.
+Once upon a time there was a website called joaquin.windmuller.ca
+
+It was a nice site.
+
So as you can see, I added a title to my story and a couple of lines (the ones that start with +, lines without a marker at the beginning are reference lines and if I had deleted lines they would begin with**-**).
And then BOOM, all hell breaks loose, you notice you wrote “fly” instead of “file”. OMG. So you immediately fix it.
> git diff
[...]
@@ -1,3 +1,9 @@
-This is a lovely fly.
+My great, great website story
+=============================
+
+This is a lovely file.
+
+Once upon a time there was a website called joaquin.windmuller.ca
+
+It was a nice site.
-Once upon a time there was a website called <name>.
And this is where you get in trouble. Because you didn’t stash the changes first, your diff is showing changes that “fix” two different issues.
You could undo the changes to “fly”, commit and then re-do the changes. In this case it may make sense because it’s a small change, but your case could be more complicated and not as easy to “undo”.
Enter git add -i
What git add -i does is allow you to interactively (hence the i) select what parts you want to add to the staging area.
git add -i
staged unstaged path
1: unchanged +8/-2 test.txt
*** Commands ***
1: [s]tatus 2: [u]pdate 3: [r]evert 4: [a]dd untracked
5: [p]atch 6: [d]iff 7: [q]uit 8: [h]elp
What now> 5
staged unstaged path
1: unchanged +8/-2 [t]est.txt
Patch update>> 1
staged unstaged path
* 1: unchanged +8/-2 [t]est.txt
Patch update>>
- First select the option 5 “[p]atch”, this option will allow you to select the parts of the diff you want.
- Then it will ask you for the file number you want to patch (in this case 1).
- Patch allows you to select multiple files, but I find it easier to work one at a time. So after selecting the file, just hit enter again.
After hitting enter, git will show you the changes on the file you selected, hunk by hunk.
In our example:
diff --git a/test.txt b/test.txt
index fd4ed01..8229b30 100644
--- a/test.txt
+++ b/test.txt
@@ -1,3 +1,9 @@
-This is a lovely fly.
+My great, great website story
+=============================
+
+This is a lovely file.
+
+Once upon a time there was a website called joaquin.windmuller.ca
+
+It was a nice site.
-Once upon a time there was a website called <name>.
Stage this hunk [y,n,q,a,d,/,s,e,?]?
A what? hunk?
A hunk is each section of a diff that shows some context on where the differences happen. Lines with**+** are new lines,- indicates deleted lines and lines with a space**’ '** are left untouched. But the important part is the first line of the hunk (the header), in this example:
@@ -1,3 +1,9 @@
It represents the range that the diff referes to. It can be read as:
@@ from-file-range to-file-range @@
from-file-range is represented as:
-<start line>,<number of lines>
to-file-range is represented as:
+<start line>,<number of lines>
The hunk header of our example (@@ -1,3 +1,9 @@) basically means:
the content of the file originally starting in line 1, followed by 3 lines. Well it has changed and now it starts on line 1 and spans the next 9 lines.
It doesn’t necessarily mean that you added 6 lines because you could have deleted others. That’s where the content of the diff comes into place with the + and -.
Jump into editing
So you can edit the diff and the hunk ranges to tell git which parts of the diff to actually apply.
Let’s select only the change from “fly” to “file” and ignore the new title and contet:
First hit e and then enter, to edit.
A file editor will open and you should end up with a file like this:
# Manual hunk edit mode -- see bottom for a quick guide
@@ -1,3 +1,9 @@
-This is a lovely fly.
+My great, great website story
+=============================
+
+This is a lovely file.
+
+Once upon a time there was a website called joaquin.windmuller.ca
+
+It was a nice site.
-Once upon a time there was a website called <name>.
# ---
# To remove '-' lines, make them ' ' lines (context).
# To remove '+' lines, delete them.
# Lines starting with # will be removed.
#
# If the patch applies cleanly, the edited hunk will immediately be
# marked for staging. If it does not apply cleanly, you will be given
# an opportunity to edit again. If all lines of the hunk are removed,
# then the edit is aborted and the hunk is left unchanged.
Here you can delete the lines you don’t want to include and then modify the number of lines on the “to-file-range”, like this:
# Manual hunk edit mode -- see bottom for a quick guide
@@ -1,3 +1,3 @@
-This is a lovely fly.
+This is a lovely file.
# ---
# To remove '-' lines, make them ' ' lines (context).
# To remove '+' lines, delete them.
# Lines starting with # will be removed.
#
# If the patch applies cleanly, the edited hunk will immediately be
# marked for staging. If it does not apply cleanly, you will be given
# an opportunity to edit again. If all lines of the hunk are removed,
# then the edit is aborted and the hunk is left unchanged.
So basically you are telling git that the change should be only in one line (actual diff content) and the number of lines on the file didn’t change (3).
If everything works ok, git will show the commands prompt again. You can use option 6 “[d]iff” to check the changes:
*** Commands ***
1: [s]tatus 2: [u]pdate 3: [r]evert 4: [a]dd untracked
5: [p]atch 6: [d]iff 7: [q]uit 8: [h]elp
What now> 6
staged unstaged path
1: +1/-1 +7/-1 [t]est.txt
Review diff>> 1
diff --git a/test.txt b/test.txt
index fd4ed01..18304be 100644
--- a/test.txt
+++ b/test.txt
@@ -1,3 +1,3 @@
-This is a lovely fly.
+This is a lovely file.
Once upon a time there was a website called <name>.
Now you are ready to commit those changes. Exit the interactive mode by using option 7 (or ‘q’)
You can check the changes ready to commit by usinggit diff --cached
And to see that the rest of the modifications are still available withgit diff
The secrets of editing hunks
Editing the hunks can be confusing at first, the instructions that git gives you help but are not enough to get started.
# —||
# To remove ‘-’ lines, make them ’ ’ lines (context).
# To remove ‘+’ lines, delete them.
# Lines starting with # will be removed.
#
# If the patch applies cleanly, the edited hunk will immediately be
# marked for staging. If it does not apply cleanly, you will be given
# an opportunity to edit again. If all lines of the hunk are removed,
# then the edit is aborted and the hunk is left unchanged.
The secret sauce is…counting lines:
- If you remove a line that starts with + then subtract one to the new line count (last digit of the hunk’s header).
- If you remove a line that starts with - then add one to the new line count (last digit of the hunk’s header).
- Don’t remove the other lines (reference lines).
This should allow you to quickly modify the hunks to select the parts you want.
Most of what I showed on this article I learned by reading these two questions on Stack Overflow: