Git: Merging & Rebasing basics

In the Git: Keeping in sync post we learned how to merge the orgin/master commits into our local master branch. Then in Git: Effective branching using workflows we learned about how to use branches effectively. What we haven’t yet touched on yet though is rebasing and its affect on merging.

Commit log

Before we get started on merging and rebasing, let’s first see how we can view our git log as we will need to do it throughout this post:

In a nutshell, the above command shows us the last three commits which were made in this repo. If we want to get a little fancier, we can have git draw a graph for us:

Note: The above commands were taken from this StackOverflow thread. As per the screenshots on StackOverflow, the output is colour coded which definitely helps!

You’re probably wondering what the *| and \ characters represent. They’re used to show us the branches and commits which have been created in this repo. By sticking to a git workflow, your log will be tidy and easy to follow. If you don’t, it’s highly likely your log is already or soon will be a complete mess! But I digress…

Creating a branch

Let’s start by creating a new branch off master called develop:

Now let’s make it track remotely:

Finally, let’s confirm that it is indeed being tracked remotely:

Great, the above tells us that develop is tracking origin/develop. We’re good to go!

Merging

Let’s now delve into the whole reason (or at least, one of the reasons) for this post – Merging.

As its name suggests, performing a merge results in two branches being merged. While it might sound easy enough, that isn’t the end of the story. The state of the two branches (the origin and local branches) determines the way in which they will be merged. Let’s take a look at how to initiate a Fast Forward merge, as well as a Recursive merge.

Fast Forward

Fast Forward merges are very straightforward (pun intended!). They can be used when there is no divergence between the remotely tracked origin branch and our  local branch.

It’s known as a Fast Forward merge because git is able to simply move the branch’s pointer forward to the tip of the merged branch. It’s as simple as that. This is in contrast to other merging methods whereby complex merging strategies are required.

The benefit of a Fast Forward merge is that it keeps your git log graph linear/clean. Let’s now look at an example.

Below is what our newly created develop branch looks like:

At this point in time, develop and origin/develop are in sync. However, let’s say we go to lunch and when we come back, John has made a few commits and performed a  push  to the origin/develop branch:

In the above output we can see that the fetch command was used to sync our machine’s copy of origin/develop with the remote server’s version. Because we saw new commits come through, we then looked at the git log and saw there were in fact two new commits.

So now our origin/develop has been updated, but our local develop branch still hasn’t. Because we haven’t made any commits to our local branch, we can see that the path between develop and origin/develop is straight:

As a result of this, if we do a git merge or a git pull (which, as mentioned in the Git: Keeping in sync post, does a git fetch and git merge in a single command), it will result in a Fast Forward merge:

Let’s now take a look at what our git log looks like:

Excellent, develop and origin/develop are in sync, and we’ve got a tidy graph log.

Recursive merge

As mentioned previously in this post, a Fast Forward merge can only take place when there’s no divergence between the origin and local branches. Let’s see what happens when they do diverge.

Using the same example as before, let’s say John made a few commits and used push to send them to the origin/develop branch. We then do a git fetch and check git status as well as  git log to find out how many commits were made:

git tells us that we’re two commits behind origin/develop. Instead of doing a git merge like we did last time, this time we make a couple of commits of our own:

Let’s now take a look at the git status  and git log outputs:

There are two important pieces of information in this output:

  1. git tells us that our develop branch has diverged from origin/develop.
  2. Our git log graph isn’t linear like it was last time. We can see that our develop branch diverged from origin/develop at commit a655b2f .

The good news is that we can still use our trusty git merge (or git pull) command to bring our local back into sync with the origin branch. However, due to the divergence, a Recursive merge will be used instead of a Fast Forward merge:

Let’s now take a look at our git log outputs:

As we can see, 7ae9fcf is a merge commit and has resulted in our local develop branch and the origin/develop branch being brought back into sync.

Rebasing

While the two preceding merging sections resulted in the same outcome, they took two different paths to get there. It’s important to note that some prefer to avoid the Recursive merging strategy because it pollutes the log history and goes against their chosen git workflow.

You might be thinking though, “are you telling me I have to do a git fetch and git merge  or a git pull every single time I’m about to do a git push just so I can avoid polluting my git log?!”. The answer to that question, thankfully, is no. This is where rebasing comes into play.

What rebasing does is it puts your local commits at the end of the remote (origin) commits, therefore creating a linear path resulting in the allowance of a Fast Forward merge. Wow, that was a bit of a mouthful, but rest assured it’s much easier than it sounds.

As with the previous example, let’s say John has made a couple of commits and used push to send them to origin/develop. We then make a change and commit it. We’re then ready to push our changes to origin/develop but find that there’s a divergence:

As we saw in the Recursive merge section of this post, if we do a git merge now we’ll end up with an untidy git log graph. Let’s then do a rebase instead:

Let’s now take a look at our git log outputs:

Hooray! We’ve managed to merge the branches even though they were diverged, while still maintaining a linear (clean) history.

How does rebasing work?

Rebasing gets its name from the fact that it changes the commit which your subsequent commits are based off – in other words, your commits are rebased onto another commit.

Let’s take a look at another example. Let’s say the following commits were made in this order:

Commit: Author: Filename: Branch:
1 John menu.py origin/delveop
2 Will module.py develop
3 John food.py origin/develop

The git log outputs confirm that the above is correct:

(It’s worth noting that although the first output makes it appear that the local develop branch includes the 3feff89 commit, the second output shows that it is in fact on a separate branch.)

Let’s have a look at the git status output:

OK, no surprises there. Let’s now do the rebase and see what happens:

And finally, let’s take a look at our git log outputs:

Wait a minute, what happened to our git log graph? Last time we checked it there was a diverge. And, why is our module.py commit at the tip of the branch? Shouldn’t it be in the middle of John’s two commits given that it was the second commit to be made out of the three?

These mysterious happenings are due to the way in which rebasing works. Because we rebased  origin/develop onto our local develop branch, git removes commits, brings our develop branch into sync with the origin/develop branch and then re-applies our commits to the tip of the newly synced branch.

As we saw above, this results in two things:

  • A linear log (as was the case with a Fast Forward merge)
  • Our local commits, regardless of whether they were made before and/or after the remote’s commits, will be re-committed as if they were always committed after the origin’s commits

Knowledge Base

See the Git section of my Knowledge Base for more information.

As always, if you have any questions or have a topic that you would like me to discuss, please feel free to post a comment at the bottom of this blog entry, e-mail at will@oznetnerd.com, or drop me a message on Twitter (@OzNetNerd).

Note: This website is my personal blog. The opinions expressed in this blog are my own and not those of my employer.

Leave a Reply

Your email address will not be published. Required fields are marked *