Git Interactive Rebasing

For many, rebasing is used to bring a feature branch up to date with master. Another great usage of rebasing is to interactively rewrite history within a branch. It's quite simple and can avoid having multiple commits that bring no value to a feature branch other than to "fix typo".

Let's dive in to an example. Let's assume we have a new empty repository and we're adding a readme file.

[~/src/echobind/git-rebasing] touch README.md
[~/src/echobind/git-rebasing] git add .                               
[~/src/echobind/git-rebasing] git commit . -m "Adding empty README.md"
[master (root-commit) 1af7152] Adding empty README.md
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 README.md

Now at this point, we might actually add content.

[~/src/echobind/git-rebasing] echo "Test content" >> README.md 
[~/src/echobind/git-rebasing] git commit . -m "Updated content"
[master ffed32a] Updated content
 1 file changed, 1 insertion(+)

[~/src/echobind/git-rebasing] git log
commit ffed32a5da6f0335a4541b543a1219a43047a597  
Author: Robert Beene <robert@echobind.com>  
Date:   Mon Apr 14 12:15:13 2014 -0400

    Updated content

commit 1af71525bcfcf08fdbaa0869b876969718548f7d  
Author: Robert Beene <robert@echobind.com>  
Date:   Mon Apr 14 12:12:43 2014 -0400

    Adding empty README.md

We now have two commits that represent the creation of a file and the addition of content. One can argue they should remain separate but assume that they shouldn't exist separately.

This is where interactive rebasing comes into play.

git rebase -i HEAD~2

pick bd5e0c8 Adding empty README.md  
pick 136cb88 Updated content

# Rebase 1af7152..136cb88 onto 1af7152
#
# Commands:
#  p, pick = use commit
#  r, reword = use commit, but edit the commit message
#  e, edit = use commit, but stop for amending
#  s, squash = use commit, but meld into previous commit
#  f, fixup = like "squash", but discard this commit's log message
#  x, exec = run command (the rest of the line) using shell
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
# Note that empty commits are commented out

Let's break down the command.

git rebase -i HEAD~2  

The -i parameter indicates this will be an interactive rebase. HEAD~2 tells git to interactively rebase the last 2 commits. If you need to go further, increment the number accordingly.

Above you'll see the two commits and various options for working with them. The two we'll concentrate on are fixup and squash.

By changing pick to the letter s, you will pull the changes from that specific commit into the one immediately above it. For our purposes, we might do this for the updated content commit.

If we do so and write this file, we'll be presented with another file.

# This is a combination of 2 commits.
# The first commit's message is:

Adding empty README.md

# This is the 2nd commit message:

Updated content

# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
# rebase in progress; onto 1af7152
# You are currently editing a commit while rebasing branch 'master' on '1af7152'.
#
# Changes to be committed:
#┬╗modified:   README.md
#

Here we can modify either commit message. In doing so they will become the singular message for the resulting commit. For a case like this, we might delete "Updated content" and simply modify the original message to be more descriptive.

When you've completed, you'll notice a different status.

[detached HEAD dd1530c] Adding README.md with content
 1 file changed, 3 insertions(+)
Successfully rebased and updated refs/heads/master.  
[~/src/echobind/git-rebasing] g
On branch master  
Your branch and 'origin/master' have diverged,  
and have 1 and 2 different commits each, respectively.  
  (use "git pull" to merge the remote branch into yours)

nothing to commit, working directory clean  
[~/src/echobind/git-rebasing]        

This is the dangerous part of rebasing. You are in fact rewriting history and you'll need to force push this to your origin. This will overwrite what was previously there. Note: if you have not yet pushed to origin, you won't encounter this issue.

Now before you run off and type the following command, you need to ensure you are doing simple pushes. This will ensure that you will push only to your current branch. Without it, you may end up pushing out to all of the origin branches you have checked out locally.

git config --global push.default simple  

Once this is set, you can freely force push.

git push -f  
[~/src/echobind/git-rebasing] git push -f
Counting objects: 1, done.  
Writing objects: 100% (1/1), 210 bytes | 0 bytes/s, done.  
Total 1 (delta 0), reused 0 (delta 0)  
To git@github.com:rbeene/git-rebasing.git  
 + 136cb88...dd1530c master -> master (forced update)

If you ever have the need to simple squash the commit and do not need to change the original commit message, you can simple use the fixup option. This will squash the commits and drop the 2nd commit message entirely. You will still want to force push afterwards.

WARNINGS:

  1. Be very careful if you are working in a branch with another developer. If you are, either be careful to advise your colleague to pull immmediately after pushing OR avoid this entirely. Rewriting history can be dangerous when other developers are in the same branch. Generally, I'm not in the same branch so this is less of an issue for my workflow.

  2. Be absolutely postive you've set the config to do simple pushes. If you haven't or don't want to, you can specify the branch by writing the following.

git push -f origin BRANCH_NAME  

This will avoid the potential problem entirely but unnecessary if you make the config changes.