Git 101: How to Handle Merge Conflicts

In the last post, I talked about how to create a Git repository and upload it to GitHub. In this post, I’m going to talk about how to resolve Git conflicts.

Setting Up Our Environment

First, we’re going to create a Git Repository for the user Doug. Since I already covered that in the last post, I’m bring to breeze through those steps below:

$ mkdir doug
$ cd doug
$ git init
Initialized empty Git repository in /path/to/doug/.git/
$ touch README.md
$ git add README.md 
$ git commit -m "First commit"
[master (root-commit) d86a7e2] First commit
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 README.md

At this point, we have a repository created for the user Doug. Now I’m going to clone that repository for the user Andrew:

$ cd ..
$ git clone doug andrew
$ ls -l
total 0
drwxr-xr-x  4 doug  staff  136 Nov 28 16:59 andrew
drwxr-xr-x  4 doug  staff  136 Nov 28 16:47 doug
$ cd andrew
$ ls -l
total 0
-rw-r--r--  1 doug  staff  0 Nov 28 16:59 README.md
$ git remote -v
origin  /path/to/doug (fetch)
origin  /path/to/doug (push)

Note that Andrew’s remote of “origin” points to where the repo was cloned from. In this case, Doug’s directory. Having two copies of the same Git repository in adjacent directories is entirely acceptable, but not commonly done. It does help us for this example, though. 

Finally, I’m going to return to Doug’s repo and set up a remote pointing to Andrew’s repo:

$ cd doug
$ git remote add origin ../andrew/
$ git remote -v
origin  ../andrew/ (fetch)
origin  ../andrew/ (push)
#
# Since we manually added a remote, we also need to tell 
# Git that when we do a git pull, the default upstream of the 
# master branch in our repo is the master branch from the origin remote.
#
$ git branch --set-upstream-to=origin/master master
Branch master set up to track remote branch master from origin.
$ git pull -v
From ../andrew
 = [up to date]      master     -> origin/master
Already up-to-date.

Adding Content

Now we’re going to add content for both Doug and Andrew:

$ cd doug
$ echo "foxes" > candids.txt
$ git add candids.txt 
$ git commit -m "Added foxes"
[master bbd8d94] Added foxes
 1 file changed, 1 insertion(+)
 create mode 100644 candids.txt
 
$ cd ../andrew/
$ git pull
remote: Counting objects: 4, done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 3 (delta 0), reused 0 (delta 0)
Unpacking objects: 100% (3/3), done.
From /path/to/doug
   d86a7e2..bbd8d94  master     -> origin/master
Updating d86a7e2..bbd8d94
Fast-forward
 candids.txt | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 candids.txt
$ echo "deer" > cervidae.txt
$ git add cervidae.txt 
$ git commit -m "Added deer"
[master 84ecfa2] Added deer
 1 file changed, 1 insertion(+)
 create mode 100644 cervidae.txt

Note that before we before we make any commits to Andrew’s repository, we first do a git pull. This is generally good practice when working on an active repository, to make sure that you have the latest and greatest changes.

Finally, we’re going to go back to Doug’s directory and pull in Andrew’s changes:

$ cd ../doug/
 $ git pull
Updating bbd8d94..84ecfa2
Fast-forward
 cervidae.txt | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 cervidae.txt

When Conflicts Happen

A graphical representation of a Git Conflict

So far, this has worked pretty well. Both Doug and Andrew made changes to separate files and Git handled them with no difficulties. This is the way any good VCS (Version Control System) should work. But sometimes, conflicts arise. For example, what happens if both Doug and Andrew edit the same file? Let’s find out.

Getting back to the theme of animals, Doug is a fan of leopards. Andrew, on the other hand likes cheetahs. So we’re going to have both users create the same file called big-cats.txt and see what happens when they try to stomp on each others’ changes.

First, Doug creates leopards:

$ cd doug/
$ echo "leopards" > big-cats.txt
$ git add big-cats.txt 
$ git commit -m "Leopards"
[master 1a417b1] Leopards
 1 file changed, 1 insertion(+)
 create mode 100644 big-cats.txt

Now, Andrew creates his cheetahs:

$ cd andrew/
$ echo "cheetahs" > big-cats.txt
$ git add big-cats.txt 
$ git commit -m "Cheetahs"
[master ccdc16f] Cheetahs
 1 file changed, 1 insertion(+)
 create mode 100644 big-cats.txt

At this point, both Doug and Andrew have repositories that are exactly the same, except for the most recent commit (HEAD) in which the same file has been created, but with different contents. Everything will be fine, until either Doug or Andrew attempt to pull each others’ changes. In this case, Doug will pull Andrew’s changes:

$ cd doug/
$ git pull
remote: Counting objects: 4, done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 3 (delta 0), reused 0 (delta 0)
Unpacking objects: 100% (3/3), done.
From ../andrew
   84ecfa2..ccdc16f  master     -> origin/master
Auto-merging big-cats.txt
CONFLICT (add/add): Merge conflict in big-cats.txt
Automatic merge failed; fix conflicts and then commit the result.

What just happened? Git caught on that two separate changes affected the same file. At this point, Git did the equivalent of throwing up its hands and said “I have no idea what to do here, I need human intervention”. At this point, we are in a “half-commited” state and need to resolve the conflict so that we can finish up the commit and merge. Running “git status” will show the conflicted files as “unmerged”:

$ git st
# On branch master
# Your branch and 'origin/master' have diverged,
# and have 1 and 1 different commit each, respectively.
#   (use "git pull" to merge the remote branch into yours)
#
# You have unmerged paths.
#   (fix conflicts and run "git commit")
#
# Unmerged paths:
#   (use "git add <file>..." to mark resolution)
#
#       both added:         big-cats.txt
#
no changes added to commit (use "git add" and/or "git commit -a")

How do we fix this? First, we need to look at the content of the file big-cats.txt which was listed as the culprit:

$ cat big-cats.txt 
<<<<<<< HEAD
leopards
=======
cheetahs
>>>>>>> ccdc16f38db0d7200d4b1573c768fd8e362303fb

The notation here is pretty straightforward. The line that starts with “HEAD” shows the disputed lines in Doug’s commit. On the bottom half of the disputed section are the lines from commit ccdc16f, which was made by Andrew. At this point, a human has to edit the file and remove the commit IDs, the center separator (“=======”), and decide if he wants to keep Doug’s changes, Andrew’s changes, or both.

In this case, since it is a text file, we’ll opt to keep both changes, leaving the file looking like this:

$ cat big-cats.txt 
leopards
cheetahs

If this were actual code, it is far more likely that only one person’s set of changes would survive.

Once the changes are made, we can add this file back into Git and finish the marge:

$ git add big-cats.txt 
$ git commit -m "Merged big cat changes"
[master 96dac47] Merged big cat changes

Finally, Andrew would do a pull on his end to incorporate the changes:

$ git pull
remote: Counting objects: 9, done.
remote: Compressing objects: 100% (4/4), done.
remote: Total 6 (delta 2), reused 0 (delta 0)
Unpacking objects: 100% (6/6), done.
From /path/to/doug
   bbd8d94..96dac47  master     -> origin/master
Updating ccdc16f..96dac47
Fast-forward
 big-cats.txt | 1 +
 1 file changed, 1 insertion(+)
$ cat big-cats.txt 
leopards
cheetahs

Assuming he made no further changes to that file, it would be a run of the mill git pull which would bring down Doug’s changes to the file and place them in Andrew’s copy of the code.

In Closing

This was a long post, and again I apologize for that. But it was necessary to lay out both the conditions that can lead up to a merge conflict in Git, and how to resolve that contact. I hope the post was informative. Feel free to reach out to me with any further questions about Git, or leave a comment on this post.