Reordering and modifying git commits

Category: Git
Date: November 22, 2016

Sometimes it might happen that you need to edit git commits beyond the last one. When you need to change the last commit because, for example, you forgot to include a file, the git commit --amend command perfectly fits the job. When you want go beyond the last commit you need to perform an interactive rebase. By using this command you can remove commits, reorder them, and even merge them together. Let’s see how.

First let’s put create a git repo and track two source files. This is the content of the file main.c

1 2 3 4 5 6 7 8 9 10 11 12 13
#include <stdio.h> #include "const.h" int main() { int x; for (x = 0; x < MAX_X; x++) { printf("1 / %d = %f\n", x, 1.0/x); } for (x = 0; x < MAX_X; x++) { printf("2 / %d = %f\n", x, 2.0/x); } return 0; }

while this is the content of const.h

1 2 3
#ifndef MAX_X #define MAX_X 10 #endif

By doing the following, we create a git repo with two commits tracking main.c and const.h

1 2 3 4 5
git init . git add const.h git commit -m "initial commit. add const.h" git add main.c git commit -m "add main.c"

Imagine that now we discover the bug in the first for loop (division by zero) and we fix it in this way, not noticing the error in the other loop

1 2 3 4 5 6 7 8 9 10 11 12 13
diff --git a/main.c b/main.c index 9279a4b..7e43f4b 100644 --- a/main.c +++ b/main.c @@ -3,7 +3,7 @@ int main() { int x; - for (x = 0; x < MAX_X; x++) { + for (x = 1; x < MAX_X; x++) { printf("1 / %d = %f\n", x, 1.0/x); } for (x = 0; x < MAX_X; x++) {

After fixing the bug, we commit the changes with

1 2
git add main.c git commit -m "fix division by zero"

Now we make a change to const.h

1 2 3 4 5 6 7 8 9
diff --git a/const.h b/const.h index a699009..f119e16 100644 --- a/const.h +++ b/const.h @@ -1,3 +1,3 @@ #ifndef MAX_X -#define MAX_X 10 +#define MAX_X 11 #endif

and commit it with

1 2
git add const.h git commit -m "increase loop limit"

Finally, imagine that now we discover the bug in the second for loop. Given that the bug is exactly the same as in the first one, it would be nice to have a single commit for both changes. We can’t git commit --amend anymore. One option would be to perform a git reset --mixed two commits back, and the re-commit again, but this is clearly inefficient, especially if you have several commits in between.

The solution here is to use git rebase --interactive to rewrite your history. First we create a commit fixing the second bug. We change the code as follows

1 2 3 4 5 6 7 8 9 10 11 12 13
diff --git a/main.c b/main.c index 7e43f4b..088c99d 100644 --- a/main.c +++ b/main.c @@ -6,7 +6,7 @@ int main() { for (x = 1; x < MAX_X; x++) { printf("1 / %d = %f\n", x, 1.0/x); } - for (x = 0; x < MAX_X; x++) { + for (x = 1; x < MAX_X; x++) { printf("2 / %d = %f\n", x, 2.0/x); } return 0;

and commit it with

1 2
git add main.c git commit -m "fix bug again"

To rewrite our history we want to start 3 commits ahead of HEAD, so we perform a git rebase --interactive HEAD~3. Your command line text editor (vim for me) should now start and show something like (your commit hash will differ)

1 2 3
pick 2d5a287 fix division by zero pick 75f8b64 increase loop limit pick fa384df fix bug again

This is showing the history up to three commits ahead with the hash of the commits and the commit message, with the topmost being the oldest one. What we can do inside the text editor is telling git how do modify this list of commits by changing the content of this file. For example, by moving lines we can reorder commits, we can delete commits by deleting lines, we can change commit messages by substituting pick with reword, we can merge commit together by changing pick with squash, and many more things.

What we want to do now, is to merge together the first and the third commit, so we change the file as follows

1 2 3
pick 2d5a287 fix division by zero squash fa384df fix bug again pick 75f8b64 increase loop limit

What we did was to move the third commit in the second place and change pick with squash, so telling git to merge the commit into the previous one. If we now save and close the file, git will open a new file showing the following information

1 2 3 4 5 6 7 8
# This is a combination of 2 commits. # This is the 1st commit message: fix division by zero # This is the commit message #2: fix bug again

git wants us to choose a commit message for the new, merged commit. We can choose one of the two (or more, if you are squashing multiple commits) or write a completely new one. We write a new commit message by deleting everything in the file and writing fix division by zero in for loops. After saving and closing the file, git will rewrite the history. By typing git log we should see something like

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
commit 93739edcaaa7a3285e7dc5006edcddb389d4ca3c Author: Michele Segata <msegata@disi.unitn.it> Date: Wed Nov 23 07:24:34 2016 +0100 increase loop limit commit 952fb8d524f61029e3416b3e0b7302ddefcf2d7f Author: Michele Segata <msegata@disi.unitn.it> Date: Wed Nov 23 07:21:36 2016 +0100 fix division by zero inside for loops commit 62ff5e674d28bc49cd402a527ab76e5f88907240 Author: Michele Segata <msegata@disi.unitn.it> Date: Wed Nov 23 07:17:22 2016 +0100 add main.c commit aff3ce72a739e829b88c3062de37aedf9c0586ab Author: Michele Segata <msegata@disi.unitn.it> Date: Wed Nov 23 07:16:50 2016 +0100 initial commit. add const.h

Now we only have four commits out of five, as two have been merged together, and the third commit is the new one we created including the bug fix in both for loops. To see the content of the commit, just type git diff HEAD~2..HEAD~1 to see the diff between the second and the third commit

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
diff --git a/main.c b/main.c index 9279a4b..088c99d 100644 --- a/main.c +++ b/main.c @@ -3,10 +3,10 @@ int main() { int x; - for (x = 0; x < MAX_X; x++) { + for (x = 1; x < MAX_X; x++) { printf("1 / %d = %f\n", x, 1.0/x); } - for (x = 0; x < MAX_X; x++) { + for (x = 1; x < MAX_X; x++) { printf("2 / %d = %f\n", x, 2.0/x); } return 0;