Rebasing??? What the heck???
We've all been there. You've just submitted a pull request that adds an absolutely amazing feature to your favorite open source project and the maintainer asks you to rebase your PR to master. CRAP! Now you're going to have to type in a bunch of arcane git commands and deal with merging code. And who can remember all of the steps involved? Well, this post will help you do just that.
First, we have to remember that a commit can be thought of as a snapshot of your repository at a certain point in time. It contains everything in your code at the time of the commit. Git doesn't work off of "diffs." This ends up making our life much easier and makes the process of rebasing much simpler both in its implementation, but (thankfully) in how we can mentally model what is happening during a rebase. A new branch in Git is simply a spot where we decided to split off from whatever line of commits we had been working from and start creating a separate set of commits, so when we decide to rebase, we are simply picking a new commit to start our branch from. Once I started thinking in this fashion, dealing with rebases became a much more simple mental exercise.
Let's figure this out!
I am going to work through the a simple scenario to explain the steps involved. You can play along at home if you would like. This scenario is probably the simplest possible rebasing example that still includes the need to merge changes during the rebase. We need to create some commits to work from, so let's do that first.
To start, open your favorite shell program and create a directory. For our purposes, I am going to call the folder learnrebase
. Change to that directory and initialize a git repo by typing git init
~
$ mkdir learnrebase
~
$ cd learnrebase/
~/learnrebase
$ git init
Initialized empty Git repository in c:/Users/Ashley/learnrebase/.git/
Now, let's create a new file called file1.txt and put the text "Created in master branch" in it. We will add the file to our staging area and commit this change.
~/learnrebase (master)
$ echo "Created in master branch" > file1.txt
~/learnrebase (master)
$ git add file1.txt
~/learnrebase (master)
$ git commit -m "First commit (from master)"
[master (root-commit) a856a73] First commit (from master)
1 file changed, 1 insertion(+)
create mode 100644 file1.txt
Next, we create a branch.
~/learnrebase (master)
$ git checkout -b NewFeature
Switched to a new branch 'NewFeature'
Now change the text in file1.txt
to be the following:
Edited in NewFeature branch
Added in NewFeature branch
Commit these changes.
~/learnrebase (NewFeature)
$ git add file1.txt
~/learnrebase (NewFeature)
$ git commit -m "Second commit (from NewFeature branch)"
[NewFeature c18ec05] Second commit (from NewFeature branch)
1 file changed, 2 insertions(+), 1 deletion(-)
Now, we will go back to the master branch and change our file. This simulates someone else committing changes to our parent branch (such as what could happen if a pull request is pulled in after we created our branch). This will create a situation where we need to merge the changes during our rebase. First, checkout master.
~/learnrebase (NewFeature)
$ git checkout master
Switched to branch 'master'
Now change the text in file1.txt
to be the following:
Updated in master branch
Added in master branch
Commit this change.
~/learnrebase (master)
$ git add file1.txt
~/learnrebase (master)
$ git commit -m "Third commit (from master)"
[master a7aee78] Third commit (from master)
1 file changed, 3 insertions(+), 1 deletion(-)
If we now look at the graph of our repo, it will look like this
Now we will rebase the NewFeature
branch so that it will branch off from "Third commit (from master)" instead of "First commit (from master)." This will require working with git's rebasing functionality. To continue, checkout the NewFeature branch.
~/learnrebase (master)
$ git checkout NewFeature
Switched to branch 'NewFeature'
Now, we will kick off the rebasing process. Git will tell us that Auto-merging failed.
~/learnrebase (NewFeature)
$ git rebase master
First, rewinding head to replay your work on top of it...
Applying: Second commit (from NewFeature branch)
Using index info to reconstruct a base tree...
M file1.txt
Falling back to patching base and 3-way merge...
Auto-merging file1.txt
CONFLICT (content): Merge conflict in file1.txt
Failed to merge in the changes.
Patch failed at 0001 Second commit (from NewFeature branch)
The copy of the patch that failed is found in:
c:/Users/Ashley/learnrebase/.git/rebase-apply/patch
When you have resolved this problem, run "git rebase --continue".
If you prefer to skip this patch, run "git rebase --skip" instead.
To check out the original branch and stop rebasing, run "git rebase --abort".
~/learnrebase (NewFeature|REBASE 1/1)
$
Notice that the branch name changed from "NewFeature" to "NewFeature|REBASE 1/1". We are now in a temporary branch where we can resolve our merge conflicts. So, how do we do the merge? Well, git has helpfully updated the file with conflicts to include the changes from both the master and NewFeature branches.
<<<<<<< HEAD
Updated in master branch
Added in master branch
=======
Edited in NewFeature branch
Added in NewFeature branch
>>>>>>> Second commit (from NewFeature branch)
So in our case, we will keep edit this file to this.
Edited in NewFeature branch
Added in master branch
Added in NewFeature branch
Please note that in a real merge, this is a spot where you will quite possibly need to put real thought in to how you merge the changes. Actually doing the merge is as simple as editing the file to make it look how you want it to look. Simply use your text editor of choice and resolve the conflicts as you see fit!
So, now that we've resolved our conflicts, it's time to continue with the rebase. We can think of the rebase as a more convoluted commit (because that's what it really simply is: a commit). Thus, we need to stage our changes just like with any other commit. Then, we follow the instructions that git gave us when we started the rebase operation and enter git rebase --continue
.
~/learnrebase (NewFeature|REBASE 1/1)
~/learnrebase (NewFeature|REBASE 1/1)
$ git add file1.txt
~/learnrebase (NewFeature|REBASE 1/1)
$ git rebase --continue
Applying: Second commit (from NewFeature branch)
Now, if we go check out our git log, something interesting has happened.
Notice that the commit hash for the commit "Second commit (from NewFeature branch)" has changed. Before the rebase, this commit had a hash of c18ec05
, but now it is fdba04a
. Why is this? Well, remember that a git commit represents a snapshot of the the repo at the time of the commit. Well, the snapshot at the time of the 'old' commit is no longer valid. The commit in question now represents a different state for the repo, and thus it has a new hash. This is a new commit, it just shares a commit message! When I started thinking about git branching in this fashion, it became a lot easier for me to understand what is going on.
We can now merge our branch back in to the master branch. This is what happens when a pull request is accepted and merged. To do this, we must switch back to the master
branch and issue the git merge
command, passing the name of the branch to be merged. If there are merge conflicts, then the conflicts can be resolved in the same way discussed above; however, in our case there are no conflicts to resolve, and our merge will be done automatically using the "fast-forward" technique.
$ git checkout master
Switched to branch 'master'
~/learnrebase (master)
$ git merge NewFeature
Updating a7aee78..fdba04a
Fast-forward
file1.txt | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
If we now check our log, we can again see how thinking of git commits as snapshots can help to understand what git is doing.
Notice that the commit hash fdba04a
has not changed. After rebasing our branch to the most recent commit on master
, no further changes to master
occured, so our final checkin on NewFeature
branch is simply inserted in the commit history for the master
branch. If changes had occurred on the master
branch after the rebase for the NewFeature
branch occurred, then a new commit would have been created to represent this new snapshot, even if no merge conflicts existed (for example if all the changes occurred in a different file).
Conclusion
Rebasing a branch is not very difficult, once you understand the concept of how git handles commits. Remember that a git commit is simply a snapshot of your repo at the time and your relationship with git will likely improve. I hope I've helped your understanding of how to work with git.