Escaping a git merge hell

Alberto de Murga
6 min readMay 23, 2022

--

Git is probably the most popular version control system around. For many years, the git flow strategy has been the recommended way of working and system to manage and organise branches in the projects, only to be replaced by a more modern GitHub flow which is an iteration of the same ideas. In an ideal world, it provides a solid system to manage the development of new features and at the same time maintain existing ones by using short-lived branches. There are other alternatives like the trunk-based system that provide a different flavour but with similar ideas behind it. However, the reality is always something completely different.

The reality check

Some time ago a colleague left my team, so I inherited the project he was working on. He was working on two big features at the same time in that project that was the result of collaboration with two other teams. He had two branches, one per feature, that were branched out of the main branch months ago and have significant changes in each of them, many of those changes on the same files. The task that he left before leaving was to integrate all that code back to master. There were all types of conflicts: files moved and/or renamed, different versions of dependencies, different competing changes on the same lines… This is where the merge hell started.

Me, thinking I “finished” the feature: 96 files +53086 -7735

The best solution is to avoid the problem

This type of merge conflict never has an easy solution, so the best solution is to avoid the problem. When working on a feature or fix, use short-lived branches as much as possible. Ideally, the age of the branch should not be longer than a couple of weeks, and it should be compromised of a few commits with not many changes. This way the merge will be likely resolved automatically without much effort.

If you are working on a big feature, try to split it into different parts. Once you start with a part, create a new branch for it and merge it into the main branch before starting with the next part. If this branch is used for continuous deployments, you can use feature flags or leave the function or module present in the code but without making use of it in “main logic” or other places of the codebase besides the tests.

If it is not possible to split the feature into different parts and you are required to keep a long-lived branch, merge (or rebase) the main branch into your branch as often as possible. That way you will get the possible updates from the main branch in your codebase (and hopefully save some effort by benefiting from code from other developers) and it will keep possible conflicts manageable, as you will be solving them in small amounts. If you are bound to deal with merge conflicts, it is much easier when they are one or two in a single file rather than a dozen in multiple files.

I’m already screwed, how do I fix this?

If you can relate to the introduction of this article, I am sorry, there is not a straightforward solution. How to fix the situation depends on the project you are working on and the conflicts you are getting. However, there is some general advice that in my experience has proven helpful.

Me, looking at the merge conflicts.

Before getting started

Linters, unit tests and integration tests are your best friends. If you complete a merge, the project runs and all the tests run successfully, it means the merge was successful. There is a chance that there are still issues that were not covered by the tests or if the test were affected by the conflicts, but at least you can have the confidence that most of the project is working as it should.

It is recommended merging the main branch into the conflicting branches before trying to integrate them. You will get the latest stable version of the code into your branch, and you can solve the first conflicts. Conflicts between the main branch and the feature branch are normally less abundant and easier to fix because you have a reliable source of truth (normally, the main branch is right), which is much less clear when you must compete for features for the same lines of code.

Having the feature branches updated with the latest changes from the main branch and with all the tests running successfully is the best starting point we can aim for. As the last suggestion, local branches are free, so do this in a new fresh branch so you don’t mess up with the original feature branch. In the end, the situation can always get worse, and it is better if you can restart the whole process.

Dealing with conflicts

Start with the branch that looks easier to integrate. Our final objective is to integrate all the feature branches into the main branch, so if you can get one out of the way easy it is a win. In this context, we are having two feature branches that have been developed at the same time, so none of them is righter than the other.

The “small one”: 16 files +1859 -395

Once you have decided where to start, it is time to get hands-on. First, you need to decide if you want to merge or rebase. A merge will incorporate the changes of the commits in the feature branch into the main branch. The changes in the feature branch are mixed with the ones in the main branch. There are different strategies that you can choose, and they will give different results. A rebase will apply the changes of the commits on top of the target branch. A benefit of this is that you can resolve me the merge conflict of each commit individually, rather than all at once, but on the other hand, it is probably a longer process as you might have conflicts that get solved in later commits.

Although they sound quite similar, they are different approaches with more nuances than I can cover in a few words. In most cases, if you have not so many small commits, it is better to rebase than merge. If you have many commits, they cover many different paths, or they change several times the same lines of code (for instance, iterations on designs that slightly change colours or positions of elements), it is better to merge.

These statements still depend heavily on your codebase and situation. Different methods with different strategies will give different results. In many cases, it is worth trying different approaches and seeing which one gives better results.

Abandon all hope, ye who enter here

Once you start your merge, git will give you a long list of files with merge conflicts. There are a few things that you can try to help you in the process.

  • Ignore the merge conflicts on any file that can be generated again. Merge the rest of the codebase, delete these files, and generate them again. It will save you a lot of time and effort.
  • Git has support for many tools regarding conflict resolutions. Spend some time choosing the one that gives you the best results or you feel more comfortable with. Don’t hesitate to use any other tool that makes your work easier.
  • Beware when you have conflicts in the dependency files ( package.json, go.mod, requirements.txt…). Do not remove dependencies until you are sure that they are not needed at the very end of the merge. Add any dependency suggested during the conflict resolution and decide at the end if it is necessary. If you have conflicting versions, pick up the one with the highest minor/patch version. If you have conflict major versions, brace yourself because that means that the code in one of the branches is incompatible with the other.
  • Run linters and tests often. The linters will ensure that the code is at least syntactically correct (it has no errors), and the tests will ensure that the code is semantically correct (it does what is expected).
  • If you happen to be dealing with frontend code, especially CSS, this is going to be problematic as the application can be syntactically and functionally perfect but still look like 💩. I don’t know any trick for this but if someone knows, please let me know.

Giving up

Sometimes, the mess is so big that there is no other solution than rewriting the whole thing from scratch. It is a miserable experience and feels like a defeat, but sometimes we need to swallow our pride and try to move forward. In the end, the purpose is to deliver some product and not fix some code jigsaw.

Did you like this post? Let me know on Twitter!

--

--

Alberto de Murga

Software engineer at @bookingcom. I like to make things, and write about what I learn. I am interested in Linux, git, JavaScript and Go, in no particular order.