Git Conflicts, Part 3 of 3

GIT2qqsxdd

Welcome to the third and final part in our short series about Git. While the first part covered the very basics of Git and the second part focused on merging and rebasing this post is going to cover a topic I’ve complete ignored until now: Git conflicts.

Simply put, conflicts happen whenever parallel branches of development modify the same piece of code. When these branches are merged back together a conflict occurs, because Git can’t tell how to combine these conflicting changes. While most people I’ve talked to seem to understand why conflicts happen during merges, there seems to be some mystification about conflicts during a rebase.

Please be aware that this post will mainly cover the “why”, not the “how” of conflicts during a rebase. However, I’m not going to be able to provide you with a one-stop solution for these types of conflicts (hint: it doesn’t exist).

 

I am conflicted

Conflicts during a merge are easy to understand. Two branches modify the same line of code, so Git doesn’t know how to combine those lines during a merge. But what about a rebase? No worries, conflicts during a rebase are just as easily explained once we take a closer look at what Git actually does during this operation.

Let’s take a step back and start at the beginning. Imagine that the block of code below shows the “original” state of the measure method (its previous history is unimportant). That is, it shows the current state of the method before we, or anybody else started to work on it again.

public Size measure(View view) {
// do some measuring
return size;
}

Fig. A — The original measure method.

Let’s say we’re tasked with expanding the method to take padding into account. Once this work is complete the method might look as follows:

public Size measure(View view, Size padding) {
// do some measuring
// account for padding
return size + padding;
}

Fig. B — The measure method, after it was modified by us.

When those changes are committed, Git stores them in the form of a change set. A change set is like an instruction manual that tells Git how to get from the code as it looks in the first block to how it looks in the second block. Each commit contains one change set (for one or multiple files). A conflict occurs when Git tries to apply a change set to a piece of code that was altered in the meantime.

Continuing with the above example, imagine somebody else was also tasked with updating the measure method. Their job was to expand the method by adding support for margin to it. The code block below shows the code as it looks on this other person’s branch after they modified it. Since the other developer was faster than us, their changes have already been merged back into master. Which means we need to rebase our work on top of the modified code.

public Size measure(View view, Size margin) {
// do some measuring
// account for margin
return size + margin;
}

Fig. A’ — The “new” measure method.

During the rebase Git will now try to apply the changes that are necessary to go from the code as it looked in figure A to the code as it looks in figure B. However, the code no longer looks like in figure A, it now looks like in figure A’. This means Git cannot figure out how to apply our changes. We now have a conflict.

If a conflict occurs during a rebase Git will stop replaying the commits and notify us about the conflict. Thanks to the commit history of the branch we’re rebasing onto, Git is able to figure out where our changes should’ve gone. It knows how it got from A to A’. It just can’t figure out how to apply our changes on top of that.

Solving a conflict during a rebase is pretty much the same as solving a conflict during a merge, even though the reason for the conflict is slightly different. A merge conflict requires us to “merge” the changes of two different branches that modified the same piece of code. A conflict during a rebase requires us to figure out how to re-do our changes on top of a piece of code that changed since we originally made those changes. In both cases we need to create something that incorporates both our changes and those of someone else.

In our case the solution might look like the code below:

public Size measure(View view, Size margin, Size padding) {
// do some measuring
// account for margin
// account for padding
return size + margin + padding;
}

Since Git stopped with the rebase as soon as it encountered a conflict we need to tell it to continue with the rebase once the conflict has been solved. Here’s how:

git add example.txt
git rebase --continue

The first command tells Git that we fixed the conflict for that particular file. Obviously if there had been multiple conflicts, we would’ve needed to solve all of them and add all of the affected files. The second command tells Git to continue with the rebase. Depending on the number of commits in the branch that is being rebased it’s entirely possible to get multiple conflicts (worst case: one for each commit). Each conflict needs to be resolved before Git can continue with the rebase. This greatly differs from the way conflicts are resolved during a merge. During a merge Git would’ve taken all of the changes from both branches, combined them and then reported a single “huge” merge conflict. As a solution we would’ve resolved all of the conflicts at once and then created a new merge commit.

A rebase results in exactly the same conflicts as a merge, the only difference is that they occur “one by one”. So don’t be worried of you have to continue a rebase more than once, this is perfectly normal. Conflicts are simply resolved individually one a per-commit basis instead of all at once as during a merge. Last but not least, if a rebase doesn’t seem to work out for some reason, there’s always the possibility to “–abort” it. This will simply abort the rebase and leave things as before.

 

Advanced rebasing

One additional use of a rebase is the use of the “–onto” parameter. By default a rebase is used to rebase the changes of the current branch on top of another branch. The onto parameter allows to rebase an arbitrary set of commits on top of any other arbitrary commit.

Imagine the following scenario: A new feature branch was created in order to fix a bug in master. However, by accident, this branch wasn’t branched off of master, but instead it was based on top of another branch the developer was currently working on. This means the bug fix cannot be merged back into master without introducing features from the incomplete feature branch. This is not what we want. Here’s what this might look like:

git-conflicts
Using the command “git rebase master” will not work in this case. The bugfix branch is based on top of a feature branch that is based on top of master. This means from Git’s point of view our bugfix branch is already based on top of master. So the command simply won’t do anything. So what do we have to do to tell Git to take the commands in the bugfix branch and replay them directly on top of the master branch?

The first thing to do is to find out the commit hashes of two commits:

  • The commit hash of the commit our branch is currently based on top of (5e92fad)
  • The commit hash of the commit we want to rebase our branch onto (6fbade9)

The short hash is usually sufficient to do this, but in most cases it will be easier to simply copy the commit hash from the GUI tool instead of writing down the short hash. Once we know the hashes we can do a rebase like this:

git checkout feature/bugfix
git rebase 5e92fad --onto 6fbade9

This will take the commits that are based on top of feature/incomplete (=5e92fad) and rebase them onto master (=6fbade9). After the rebase completes our history will now look like this:

git-conflicts-show

 

Conclusion

Using git rebase to keep feature branches up to date can result in a cleaner, easier to read commit history. Rebasing works so long as there is never more than one developer working on the same feature branch (at the same time). If more than one developer is working on a feature branch there must be a clear communication between all involved developers whenever the branch is rebased. At this point every other developer will have to throw away their local copy and check out a fresh copy. It should be obvious this doesn’t work if they already committed work on top of their local copy of the branch. In this case rebasing is ruled out in favour of merges.

I hope you had fun reading my short series on Git. If you have and questions or found an error, please use the contact form below to leave a comment and I’ll be sure to respond or update the posts accordingly. Thank you for your attention.

Part 1: An Introduction to Git
Part 2: Git Merge and Rebase

3 thoughts on “Git Conflicts, Part 3 of 3

  1. Pingback: Short Git Series by M-Way Solution | BacApa

  2. Pingback: An Introduction to Git, Part 1 of 3 | Thinking Mobile | BacApa

  3. Pingback: Web Development Reading List #100 – Smashing Magazine

Leave a Reply

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