Understanding Git Rebase

Git can be confusing…

Git is the best. It’s jam-packed with features for managing a codebase, and when it’s used correctly, it can be a huge productivity-booster. However, because it’s so full of features, and due to its command-line nature, it can be really confusing to use correctly.

One of the most confusing concepts in git (yet at the same time, one of the most beneficial tools) is rebasing. Using the rebase command, you can maintain an overall cleaner commit history, making it easier to roll back to previous versions of your code if needs be. Rebasing can also be used on a single branch to “squash” commits into a single commit (called an interactive rebase), thereby getting rid of unneeded, small little commits in the overall git log. This results in full, finished features only being a single commit, rather than a conglomeration of many small, inconsequential commits that clutter up the overall commit history.

Rebasing is a difficult concept to understand, so here’s an (hopefully) easy guide to getting started with Rebasing. It includes a tutorial, that I highly recommend that you try out yourself.

Disclaimer

This document assumes that you know the basic of using Git. If you don’t, see my previous blog post on Git basics.

Rebasing vs. Merging

In the world of git, there are two camps: those that merge everything, and those that rebase as often as possible. Both have to do with managing the commit history between two branches.

Many know what merging is, and how to do it:

git merge dev

Merging effectively takes the two branches and meshes them together, keeping the original commit history between the two, plus adding in another commit for the actual merging.

Rebasing is very similar to merging, with one major difference: Essentially, rebasing means that instead of merging all of the two branches’ commits together in chronological order, you “detach” the feature branch from the point in which is was forked, fast forward the original branch to its most up-to-date state, then place the feature branch (1 commit at a time) at the new HEAD of the original branch. This results in the branch (and resulting commit history) behaving as if you had forked the original branch in its up-to-date state, rather than when you actually started the feature.

Rebasing is in fact used in conjunction with merging, though they’re used at different times, and for different reasons. Rebasing is for keeping your branches up-to-date with other branches, while merging is used primarily for adding in finished features to primary branches.

That explanation might not have made too much sense, it’s a little confusing at first. The concept is best explained through an example.

Example Project

Let’s imagine a scenario that we’re a developer in a team working on a project. Using a normal git workflow, we’ll be able to see when it’s appropriate to rebase, how to rebase, and when it’s appropriate to perform an interactive rebase, along with how to do it.

Start by creating a directory, init-ing a blank git repository in it, and adding a file to the project.

mkdir project; cd project
git init
touch file1.txt
git add .
git commit -m "Initial commit"

This represents our team’s project prior to starting a new feature. So, if you run a git log, you should see something like the following:

commit 7b0ef6c7b76c02333d022d7fa971c8c584d35b94
Author: John Turner <john***************om>
Date: Thu Aug 11 17:28:49 2016 -0600

Initial commit

Start New Feature in New Branch

Now, let’s start a feature branch:

git checkout -b feature

Let’s say that this feature needs to have 2 new files added to it in order to be considered finished. In the new branch, add a new file to repository, stage the changes to git, and commit them.

touch file2.txt
git add .
git commit -m "Added file 2 in feature branch"

Hotfix/Feature Added to Master

Now, suppose that while you were working on this feature in a separate branch, someone from your team finished a different feature, or added a hotfix, which was merged into the master branch, thereby moving the commit history forward to include your teammate’s work. Let’s simulate that by switching back to master, and adding a different file to the project:

git checkout master
touch file3-master.txt
git add .
git commit -m "Added file 3-master to master branch (Teammate-added feature)"

In order to avoid merge conflicts, it’s a best practice to keep your feature branches up-to-date with the branches from which they were forked. So, at this point you have two options:

  1. Merge master into your feature branch, thereby making the commit history something like this (from oldest to most recent):
    • Initial Commit
    • Added file 2 in feature branch
    • Added file3-master to master branch (Teammate-added feature)
    • Merged Branch master into feature
  2. Rebase feature branch off of master, which would make the commit history something like this:
    • Initial Commit
    • Added file3-master to master branch (Teammate-added feature)
    • Added file 2 in feature branch

Since your feature is not finished yet, and your teammate’s feature is finished and merged into master, it makes more sense to have option two’s commit history showing finished features in chronological order. Plus, rebasing off of master makes is much easier to perform an interactive rebase, resulting in a much cleaner commit history (we’ll get to interactive rebases in a bit).

So, let’s rebase our feature branch off of master:

git checkout feature
git rebase master

Once this is done, your feature branch should be up-to-date with the most recent version of master, AND, your commit history is nice and clean, chronologically-speaking (with reference to finished features).

Finish Feature – Interactive Rebase

Let’s finish our feature branch by adding the final file, and prepare it to be merged into master:

touch file4.txt
 git add .
 git commit -m "Finished feature"

Now that the feature is finished, it’s ready to be merged into the master branch. If we look at the git log of this current feature, it reads something like this:

  • Initial commit
  • Added file 3-master to master branch
  • Added file 2 in feature branch
  • Finished Feature

If we were to merge the feature branch into master as is (as well as all feature branches with all of the small little commits that make up finished features), our commit log would quickly fill up with small commits that might represent no more than a broken, half-finished step in a feature. These commits provide no real value when looking through the commit history of the production branch, and can cause unneeded delays when you’re in a bind and need to quickly roll back to a previous, stable version of your source code.

In order to keep your production branch clean from these inconsequential commits, we can perform an interactive rebase, essentially squashing all of our work into a single, clean commit. The one downside to performing an interactive rebase is that it is more involved than running a single command. Let’s do it in our example project to understand how it works.

Prep for Interactive Rebase – Figure out Total Number of Commits

To begin an interactive rebase, you’ll need to first know how many commits you’d like to squash down into a single commit. To do this, run git log, and count the number of commits that make up your feature.

For us, in this project, we want to squash 2 commits into 1 final, full-featured commit:

commit 211d04f853f63b017a1a62166d4cbe9a8922226f
Author: John Turner <john*****************om>
Date: Sun Aug 14 21:27:27 2016 -0600

Finished feature

commit ea9b4e05997629a284eede8e0f4230b520a7b377
Author: John Turner <john*****************om>
Date: Thu Aug 11 17:29:12 2016 -0600

Added file 2 in feature branch

commit d1522ee4ecfa6e55b757815a9d99aa11b9dcd1a8
Author: John Turner <john*****************om>
Date: Thu Aug 11 17:29:46 2016 -0600

Added file 3-master to master branch (Teammate-added feature)

commit 7b0ef6c7b76c02333d022d7fa971c8c584d35b94
Author: John Turner <john*****************om>
Date: Thu Aug 11 17:28:49 2016 -0600

Initial commit

Start the Interactive Rebase

Once you know how many commits you’d like to squash, start the interactive rebase by structuring the command:

git rebase -i HEAD~2

Note: The -i is the necessary flag to perform the interactive rebase. HEAD means that you’re starting from your current position in the current branch, and the ~2 means that you’d like to grab two total commits for the interactive rebase.

Choose Commits to Pick vs. Squash

At this point you will be presented with a file in whatever command-line editor you have set as a default (it should be vim if you haven’t specifically set a default editor). The file should look something like this:

pick ea9b4e0 Added file 2 in feature branch
pick 211d04f Finished feature

# Rebase d1522ee..211d04f onto d1522ee (2 command(s))
#
# 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
# d, drop = remove commit
#
# 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

In this file, you’ll choose which commit(s) you’d like to squash, and which commit you’d like to “pick“. Pick-ing a commit essentially means that the squashed commits will be rolled up into the commit you choose. There are a bunch of different articles and opinions out there that say that you should either pick the final commit or the first commit. In my experience, it doesn’t really matter. I always just choose the first commit, since it just makes sense to me that way. The main thing here is that you pick one commit and squash all the rest.

You’ll notice in the file that all of the commits listed have pick next to them. If you were to save this file and exit it, you would end up with the exact same commit history as when you started. So, to squash the commits into a single one, change out the second pick for an s, resulting in a file that looks like this:

pick ea9b4e0 Added file 2 in feature branch
s 211d04f Finished feature

# Rebase d1522ee..211d04f onto d1522ee (2 command(s))
#
# 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
# d, drop = remove commit
#
# 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

Save this file and exit.

Choose Commit Message

After exiting this file, you’ll be presented with a new file–one for choosing/writing your new commit’s message. If you’ve been following this example project, it should look something like this:

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

Added file 2 in feature branch

# This is the 2nd commit message:

Finished feature

# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# Date: Thu Aug 11 17:29:12 2016 -0600
#
# interactive rebase in progress; onto d1522ee
# Last commands done (2 commands done):
# pick ea9b4e0 Added file 2 in feature branch
# s 211d04f Finished feature
# No commands remaining.
# You are currently editing a commit while rebasing branch 'feature' on 'd1522ee'.
#
# Changes to be committed:
# new file: .DS_Store
# new file: file2.txt
# new file: file4.txt
#

You can see that both of the original commit messages are included in the file, and they are the only things that aren’t commented out. You’ll want to remove one (or both) of the messages, and leave a single commit message to be used for the final commit. In this example project, I’m going to delete both of the messages, and write a new message. The file then looks something like this:

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

Added two files which resulted in...

# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# Date: Thu Aug 11 17:29:12 2016 -0600
#
# interactive rebase in progress; onto d1522ee
# Last commands done (2 commands done):
# pick ea9b4e0 Added file 2 in feature branch
# s 211d04f Finished feature
# No commands remaining.
# You are currently editing a commit while rebasing branch 'feature' on 'd1522ee'.
#
# Changes to be committed:
# new file: .DS_Store
# new file: file2.txt
# new file: file4.txt

Save and close that file. And that should be the end of the interactive rebase! The terminal output should read something like this:

[detached HEAD dd0ec2c] Added two files which resulted in...
 Date: Thu Aug 11 17:29:12 2016 -0600
 3 files changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 .DS_Store
 create mode 100644 file2.txt
 create mode 100644 file4.txt
Successfully rebased and updated refs/heads/feature.

Project End

Now that our feature branch is finished, and is represented by a single, finished commit, let’s merge it into master. Generally you would do this through a pull request in GitHub or Bitbucket, but for this simple example we’ll just merge it in through the command line:

git checkout master
git merge feature

This results in terminal output like the following:

Updating d1522ee..dd0ec2c
Fast-forward
 .DS_Store | Bin 0 -> 6148 bytes
 file2.txt | 0
 file4.txt | 0
 3 files changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 .DS_Store
 create mode 100644 file2.txt
 create mode 100644 file4.txt

And the final master git log looks like this:

commit dd0ec2c6dcf660772dc02a9d83b2ee81a7ca0fc1
Author: John Turner <john**********************>
Date: Thu Aug 11 17:29:12 2016 -0600

Added two files which resulted in...

commit d1522ee4ecfa6e55b757815a9d99aa11b9dcd1a8
Author: John Turner <john**********************>
Date: Thu Aug 11 17:29:46 2016 -0600

Added file 3-master to master branch (Teammate-added feature)

commit 7b0ef6c7b76c02333d022d7fa971c8c584d35b94
Author: John Turner <john**********************>
Date: Thu Aug 11 17:28:49 2016 -0600

Initial commit

So fresh, so clean…

Wrapping Up

And that’s the meat and potatoes of rebasing! Once you’ve done it a few times it starts to become second nature in your git workflow.

As a final word of caution, rebasing is especially useful if adopted by everyone on your development team. If one or two people choose not to adopt rebasing, but rather continue to merge master branches back in on feature branches, this makes it exceptionally difficult to perform interactive rebases. And, when those same people don’t perform interactive rebases on their features, the commit history still becomes cluttered and difficult to work with.

So, the moral of the story: Make sure that everyone adopts this cleaner method of managing your git history!.

Miscellaneous Tidbits

Here are a few things to keep in mind when incorporating the rebase command into your workflow.

Git Pull Default Behavior

git pull, by default, performs a merge behind-the-scenes. You can change this to perform a rebase instead, thereby keeping your commit history that much cleaner. To do so, run the following (in git >= 1.7.9):

git config --global pull.rebase true

Further Reading

2018-03-12T02:23:45+00:00 September 6th, 2016|
css.php