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
~ $ 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).
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.