DevOps

Resolving Merge Conflicts in Git

The nightmare of every Git user is the error message merge failed. In this case, Git has failed to merge two commits independently.

 

As a result, you must analyze and solve the problem by yourself. Of course, this situation always occurs at the worst possible time; for example, you’re about to leave your office and want to do a quick pull/push after your last commit to save your work in the remote repository.

 

As the name suggests, merge conflicts are triggered by a merge process. However, since Git applies the merge code to other commands as well, a merge conflict can occur with various commands, such as git pull, git stash pop, or git rebase, to name the three “most popular” candidates.

 

In this post, we’ll explain why merge conflicts occur and provide tips on how to resolve them. We’ll also point to an even bigger problem: Sometimes, Git doesn’t see a conflict and performs the merge operation without a hitch. But the next time you test it, the code doesn’t work, and all the developers blame each other. Such problems can occur when two separate parts of the code are changed so that they no longer fit together.

 

For Git, whether you perform dozens of commits in either of two branches and only then do a merge, or if you regularly initiate a merge every two or three commits, doesn’t matter. In each case, Git compares only the latest commit from both branches and the common base (i.e., the first parent commit from both branches).

 

However, if conflicts do occur, the fewer files or locations in the code that are affected and the younger the code, the easier problems will be to resolve. You and your fellow developers will then know immediately why the changes were made and can determine which change is correct more quickly.

 

For this reason, you should run git merge or git pull regularly (unless other reasons exist not to do so).

 

Collisions in the Code

The most common cause of merge conflicts are changes to the same code made in different branches or by different developers. Let’s assume the following statement exists in a source file (code.py):

 

# original code (main)

maxvalue = 20

 

Then, developer Anna changes the line in the following way:

 

# in the branch of Anna (branch1)

maxvalue = 30

 

Simultaneously, Ben finds that a smaller maxvalue is also sufficient and saves memory and so writes the following code:

 

# in the branch of Ben (branch2)

maxvalue = 10

 

The attempt to merge branch1 starting from branch2 fails. Git cannot determine which of the two changes is better or “more correct.”

 

git checkout branch2

git merge branch1

   Auto-merging code.py

   CONFLICT (content): Merge conflict in code.py

   Automatic merge failed; fix conflicts and then commit

   the result.

 

First, you’ll now need to realize what state your project directory is in. Git has already changed the files where the changes can be made without any problems and marked them for the next commit. The problematic files have also been changed, but they now contain both versions of the code with special markers.

 

git status

   On branch branch2

   You have unmerged paths.

       (fix conflicts and run "git commit")

       (use "git merge --abort" to abort the merge)

 

   Unmerged paths:

       (use "git add <file>..." to mark resolution)

       both modified: code.py

 

Git expects you to edit the code.py file with an editor, manually resolve the conflicts, mark the file for the next commit, and finally commit the file yourself. In an editor, the code in question now has the following lines:

 

<<<<<<< HEAD

maxvalue = 10

=======

maxvalue = 30

>>>>>>> branch1

 

In this case, the first variant (between <<< and ===) contains the code of the current branch or (in case of a merge conflict on a pull) your own code. Between === and >>> follows the code of the foreign branch or (in case of a pull) the code from the remote repository. Depending on the editor, the two branches are highlighted in a color, and commands may be available to quickly activate one or the other variant.

 

In any case, you must now remove the conflict markers and decide on a code variant. Usually, an appropriate step is to also leave a comment to document how the code came to be:

 

# maxvalue set as proposed by Anna

# (merge conflict Jan 2022)

maxvalue = 30

 

Now, you need to provide the file for the next commit and perform the commit. This completes the merge process:

 

git add code.py

git commit -m 'merge branch1 into branch2'

 

In practice, larger merge processes often involve multiple files and code passages. Accordingly, resolving all conflicts can be tedious.

 

Merge Tools

You can resolve merge conflicts in any editor by searching for the text passages in question, deciding on a variant in those places, and deleting the alternative suggestion along with the conflict markers. In difficult cases, however, seeing all three code versions side by side could be helpful. The git mergetool command can provide help in this regard:

 

git mergetool --tool meld

   Merging: f2settings.py

 

   Normal merge conflict for 'f2settings.py':

       {local}: modified file

       {remote}: modified file

 

git commit

 

git mergetool launches an external program (for our example, we used meld) that juxtaposes two or three code versions of the file in conflict (see below):

  • On the left, the version that was current when the two branches were split (i.e., the first common parent of both branches)
  • In the middle, the local/current version (i.e., the file from the active branch)
  • On the right, the version from the branch to be added or (in case of a pull operation) the version from the remote repository

“meld” Merge Tool with Three Variants of a PHP File

 

Some merge tools don’t display the parent version. The operation of each tool also varies. The goal is to change the local version of the file so that the correct changes are there. Then, you can save the file and exit the program. Finally, you must use git commit to complete the merge process.

 

git mergetool requires that a suitable external tool be installed beforehand. When you run git mergetool --tool-help, the command shows which tools are relevant and which are installed. We’ve had good experiences with meld (see https://meldmerge.org). A Windows version is available for download from the project website. The Linux version can be easily installed on many distributions (e.g., on Ubuntu) with apt install meld. macOS isn’t officially supported, but the package management systems brew, Fink, and MacPorts contain corresponding packages. On Windows, the tortoisemerge command from TortoiseGit is also a good choice.

 

git mergetool is designed to help resolve merge conflicts as conveniently as possible. However, we want to be upfront that the operation of various merge tools is not trivial and will require practice. In simple cases, you’ll typically reach your goal more quickly without an explicit merge tool.

 

Binary File Conflicts

For text files, Git can determine the changes between two or three versions of the file. This comparison isn’t possible with binary files (i.e., images, Word documents, PDF, ZIP files, etc.). If a binary file has different contents in two branches, Git has no clue which is the “better” file. You cannot integrate parts of one binary file into another; a binary file can only be considered in its entirety.

 

git checkout branch1

git merge branch2

   warning: Cannot merge binary files: image.png

   (HEAD vs. branch1)

   CONFLICT (content): Merge conflict in image.png

   Automatic merge failed; fix conflicts and then commit

   the result.

 

In this case, you can use git checkout to explicitly use either the version of the current/own branch (--ours, namely, branch1) or that of the other/foreign branch (--theirs, namely, branch2). The double hyphens separate the options and references of git checkout from the filename that follows.

 

Using git commit -a, you can complete the commit. The -a option is required for the file modified with git checkout to be included. (Alternatively, of course, you can run git add <file> upfront.) We’ve intentionally not specified the -m option. This option launches an editor, as is usual with git merge, where you can change the default commit message

for the merge process.

 

git checkout --ours -- image.png        (use 'own' version)

git commit -a                           (perform commit)

 

git checkout --theirs -- image.png      (use 'foreign' version)

git commit -a                           (perform commit)

 

If a merge conflict occurs during rebasing, the checkout options -- ours and --theirs have the inverse meaning!

 

This almost absurd behavior stems from the fact that, during rebasing, foreign commits are used as the basis, and the own ones are adjusted accordingly. The merge process therefore takes place in the reverse direction.

 

Merge Abort and Undo

A merge process that has been started but that has been interrupted due to a conflict can be canceled quite easily with the following command:

 

git merge --abort

 

Your project will then be in the same state as it was before you started the merge process. If the merge conflict occurred as part of git pull, the new commits have already been downloaded, which means git fetch is already done. Only the merge process belonging to the pull action is still pending.

 

Of course, git merge --abort won’t solve all your problems, but you can at least enjoy the weekend in peace. You should by all means avoid trying to force a merge process through; you shouldn’t simply throw up your hands and say, “It’ll work somehow.”

 

If aborting the merge is already too late, you can use git reset to return to the last commit before the merge process. For this task, you must pass the hash code of the last commit to the command. As a rule, you can specify HEAD~ instead of the hash code. This notation denotes the predecessor of the current commit:

 

git reset --hard <lasthash> resp. HEAD~

 

By default, Git also takes into account whitespace (i.e., blank spaces and tab characters) when comparing code files. If two developers disagree on the best way to indent code, significant merge problems may arise.

 

On one hand, the issue can be resolved by implementing clear rules (i.e., “No one changes the indentation of other people’s files!”). On the other hand, you could use the command git merge -X ignore-all-space, which allows Git to ignore whitespace and tab characters when comparing code files. (man git-diff documents some additional whitespace options.)

 

Content-Related Merge Conflicts

A syntax error in the code is usually easier to fix than a logic error, where the code is syntactically correct but returns incorrect results. The same is true with Git: If Git reports a conflict, you can usually fix it quickly with a little practice or after consulting with the other members.

 

What’s much more annoying is when Git does not detect a conflict, but your code doesn’t work (correctly) anymore after the merge process. How can this happen?

 

Imagine that Ben has programmed the function myfunc in file B and also called it in a few places. He finds out that the design of the function isn’t ideal and so changes the order of the first two parameters and adds an optional third parameter. Then, he accordingly adjusts the places in the code where he calls myfunc. With a quick grep, he makes sure that nobody else uses his function yet—so nothing can happen. Commit, pull/push, and end of work.

 

On the same day, Anna also works on the code. After a pull in the morning, she discovers myfunc and uses it to greatly simplify her code in file A. “A godsend,” she thinks. (At that point, myfunc is still in its original state.) She also ends the day with commit, a pull, and a push. Git doesn’t recognize any problem. Ben only changed file B, Anna only changed A, and the code can be merged effortlessly.

 

The next day begins with frustration for both team members: Already at the first attempt errors occur when running the program (with what is now for both sides current versions of the files A and B).

 

In the scenario described, the correction is of course trivial. Much worse is when an error isn’t that obvious, only occurs in a rare constellation of circumstances, and is discovered a month later or when the interaction of two apparently unrelated changes in the code triggers a security problem.

 

How can such problems be avoided? Only through the consistent use of automated (unit) tests. You should always keep in mind that Git applies formal rules to the merge process, but it doesn’t “understand” your code at all.

 

MERGE Files

The .git/HEAD file contains a reference to the head file of the current branch. In the case of a merge process that failed due to a conflict, the .git directory contains a whole bunch of other files with status information:

  • MERGE_HEAD contains the hash code of the branch to be merged. In an octopus merge, the file contains the hash codes of all branches accordingly.
  • Typically, MERGE_MODE is empty. Only if you run git merge --no-ff will the file contain appropriate information.
  • MERGE_MSG contains the intended commit message. (The COMMIT_EDITMSG file also contains the text of the last commit message and isn’t relevant for the merge process.)
  • ORIG_HEAD contains the hash code of the active branch.

As soon as you complete or cancel the commit, the MERGE files will disappear again. (ORIG_HEAD will be retained.)

 

Editor’s note: This post has been adapted from a section of the book Git: Project Management for Developers and DevOps Teams by Bernd Öggl and Michael Kofler.

Recommendation

Git: Project Management for Developers and DevOps Teams
Git: Project Management for Developers and DevOps Teams

Get started with Git—today! Walk through installation and explore the variety of development environments available. Understand the concepts that underpin Git’s workflows, from branching to commits, and see how to use major platforms, like GitHub. Learn the ins and outs of working with Git for day-to-day development. Get your versioning under control!

Learn More
Rheinwerk Computing
by Rheinwerk Computing

Rheinwerk Computing is an imprint of Rheinwerk Publishing and publishes books by leading experts in the fields of programming, administration, security, analytics, and more.

Comments