DEV Community

dinos
dinos

Posted on • Updated on

Git: Merge VS Rebase

Let's examine a scenario that one may often encounter when collaborating on a project with a version control system (VCS) like Git:

State 0: master branch

master: -- A - B -[HEAD]
Enter fullscreen mode Exit fullscreen mode

Our master branch contains the latest version of our production-ready code that is currently in use. This is the baseline for our new feature/patch.

State 1: developing a feature

master: -- A - B
feature:        \- C - D -[HEAD]
Enter fullscreen mode Exit fullscreen mode

We will start development on a new feature branch and push a few commits.

State 2: master receives an update

master: -- A - B - E
feature:        \- C - D -[HEAD]
Enter fullscreen mode Exit fullscreen mode

While we were working on our branch, master received a number of new commits for a different feature/bugfix. To keep it simple we can consider them as a single commit in this example.

At this time, our branch and master have diverged. This means that although they used to share a common history of commits, the commit tree has differentiated between the two branches after a certain point, which can be visualized as a split instead of a continuous flow.

What does this mean for our project?

Since we will eventually want to merge our separate feature branch back into master, we want to make sure that this can be done without losing any of the commits' contents across all branches that we merge.

In other words, we need to ensure that the final state of our master branch contains both the changes that were added to it while we were working on our feature, and our fresh new changes that originated from our own branch.

How do we proceed?

When we get to a state like this, we will have to decide whether we want to rebase our branch on top of master, or to merge the latest changes from master to our branch.

Even though this may initially sound like the same thing (which is not entirely wrong, since the end result code-wise is the same), there are actually a few differences that revolve around git history that make a technical difference.

Let's consider both cases, starting with rebasing:

State 3a: rebase

master: -- A - B - E
feature:            \- C' - D' -[HEAD]
Enter fullscreen mode Exit fullscreen mode

When we rebase, what we are doing is we are asking Git to take our entire branch, commit by commit, and apply everything as if they were all made after the latest commit in master.

While this may sound simple (which often it is, since Git will attempt to take care of the history rewrite automatically), we may as well encounter what's called a merge conflict. This term means that, for a specific commit, our changes could not automatically be applied to the latest version because some of the lines that we changed happened to have changed on the master branch too.

As Git doesn't know what to do with these conflicting lines by itself, we will have to intervene and manually edit all files with conflicts in order to decide which version to keep; either the current one (whatever the master branch has), or the incoming one (whatever our branch has), or a mix of both by modifying our code accordingly to comply with the new logic that was introduced in the commits we missed.

For each commit with a conflict, we will have to resolve them (duh!), stage the files (git add .) and continue rebasing with git rebase --continue. Whenever we manually resolve a conflict, our changes get added to the rest of those that were originally introduced with that commit, so in reality, we are changing the commits from our branch that caused a conflict with master to make them seem as if they were developed on top of the latest version.

It becomes obvious that the more commits we have and the more changes we have made to different parts of the codebase, then the more conflicts may occur after a full rebase, so we need to be thoughtful of this and make sure that we rebase and squash(!) often to avoid complex conflict resolution.

After we have successfully resolved all conflicts and when the time comes, we will be able to merge our branch with master like so:

State 4a (i): rebase-merge

master: -- A - B - E - C' - D' -[HEAD]
Enter fullscreen mode Exit fullscreen mode

Alternatively, we can combine C' and D' into a single commit (F) and perform what's called a squash and merge:

State 4a (ii): squash-merge

master: -- A - B - E - F -[HEAD]
Enter fullscreen mode Exit fullscreen mode

This is it! We have merged our changes with the master branch, which now has all changes from both features (E and F).

Let's pause at this point and go back to when we were making a decision between rebasing and creating a merge commit, in order to see what would happen in the opposite case:

State 3b: merge commit

master: -- A - B - E -----\
feature:        \- C - D - E' - [HEAD]
Enter fullscreen mode Exit fullscreen mode

In this method, we are creating a new commit with everything that we have missed from the master branch and we are appending it to the end of our branch (also known as HEAD).

This is usually called a merge commit since it merges two branches that have diverged. Similarly to what we saw before, this can also lead to merge conflicts, but the difference here is that it will only occur once - on the merge commit itself and not on each commit of our feature branch that happened to have line conflicts.

After we resolve all merge conflicts in the way we described above (edit files to resolve conflicts, git add ., git merge --continue), we can proceed with merging with master whenever we are ready:

State 4b (i): rebase-merge

master: -- A - B - E ----- E' - [HEAD]
                \- C - D -/
Enter fullscreen mode Exit fullscreen mode

Or, once again, with squashing:

State 4b (ii): squash-merge

master: -- A - B - E - F - [HEAD]
Enter fullscreen mode Exit fullscreen mode

Notice how states 4a (ii) and 4b (ii) are identical - as they should be since we know that the end result should be the same, at least when looking exclusively at the codebase.

If you have any questions or suggestions, feel free to comment below!

Oldest comments (0)