A core feature of version control is managing many developers' concurrent edits to a single codebase. Several people start with the same version of the codebase, make edits to it independently, and then push those edits to the central repository. Their version control system "merges" these several edits to make a single, new, consistent codebase.
Recall that in the commit log, commits form a directed acyclic graph. Most commits have exactly one parent (directly preceding) commit. However, a "merge" commit (the result of a merge) have several parents — typically one for each developer whose work is being merged. Here's an example:
How does the version control system merge files? If the several people work on different files, then the merge is simple — the system merely takes the edited version of every modified file.
If several people work on different sections of the same file, then the merge may still be possible — the system creates a new file with the updated sections from each developer.
However, sometimes several people touch the same few lines of code. In this case, the system sees two different ways of changing the same code, and it can't be sure which to accept. It presents this to you as a "merge conflict". These are the conflicts that the remainder of this document will investigate.
Note that some version control systems, including Git, will allow N-way merges — that means it might attempt to merge more than two commits. This increases the complexity of the merge, but the strategy for resolving conflicts is the same as with a common two-way conflict, so this document will only address two-way merges.
You can cause a simple conflict by running the conflict.sh
script that accompanies this document. The script creates a scenario where Abraham Lincoln and you, his private assistant, have both made changes to his first draft of the Gettysburg Address. He has made major content changes, and you've made some minor grammar changes.
After running the script, run git pull in tutorial-repos/git-local/
. Git will try to merge your changes and Lincoln's automatically, and might succeed in some cases, but it's likely that you'll have to resolve some conflicting changes yourself. A conflict is always the result of a failed automated merge.
When you run pull in your repository to get Lincoln's changes, Git automatically tries to merge his version into yours. This is when you'll see any conflicts.
$ git pull
...
Auto-merging gettysburg_address.txt
CONFLICT (content): Merge conflict in gettysburg_address.txt
Automatic merge failed; fix conflicts and then commit the result.
Reviewing the repository's status shows all the files that contain conflicts. If you ever have conflicts in multiple files, the running git status will tell you which files you've already resolved and which ones remain unresolved.
$ git status
...
You have unmerged paths.
(fix conflicts and run "git commit")
Unmerged paths:
(use "git add <file>..." to mark resolution)
both modified: gettysburg-address.txt
...
If you examine the Gettysburg Address, you'll see conflict markers (<<<<<<<
, ==========
, >>>>>>>>>>
) that denote the locations of conflicts (line numbers are included in the following except for your convenience).
$ cat gettysburg-address.txt
1: Four score and seven years ago our fathers brought forth,
<<<<<<< HEAD
2: upon this continent a new nation conceived in liberty, and
=======
2: on this continent, a new nation, conceived in Liberty, and
>>>>>>> 247b7c36efaebd2b93e0891857d57ef251bc9d8f
3: dedicated to the proposition that all men are created equal.
...
From this notation, we can see that line two is in conflict, but lines one are three are not. The first line two, which begins with "upon this", is from the HEAD
commit (the latest local commit), and the second line two, which begins with "on this" is from the remote commit with the given identifier (247b7c3...).
Lines one and three aren't in conflict because either (a) neither commit modified those lines or (b) Git successfully merged them automatically. In most merges, the overwhelming majority of edits are merged automatically and only a few need manual attention.
Git tracks conflict status for each file, and prevents you from committing files that are marked as conflicted. To clear the conflicted state, you need to make edits to that file and then stage that file by running the add command. If you make mistakes, you can abort the merge completely and try again by running git merge --abort
.
The general strategy for resolving a merge conflict is to edit the file so that (a) the distinctive conflict markers are gone and (b) the logic from both commits is preserved as intended.
Preserving the logic is the more difficult task, because it requires knowledge of another commit's intent and functionality. This is one reason we write unit tests! If the unit tests are good, then you should be able to prove you've preserved the other commit's logic by running the relevant tests.
In virtually all cases, resolving a conflict in a file will be more complex than simply accepting one of the remote or local in its entirety. Typically you will want to compare the local, remote, and common ancestor and choose a specific combination of all three.
The following sections detail various tools and methods for resolving conflicts. The most direct way is to resolve the conflicts in your favorite text editor, but there are also graphical tools you can use (including OpenDiff on Mac, KDiff on Linux, and EMerge and VimDiff on Linux and Mac).
This is the most flexible way of resolving merge conflicts. It can be confusing because the conflict markers interrupt the text and make it hard to discern the text's original intent, but it is also the only way to correctly resolve most conflicts. It's important to learn this method before using any graphical tools (which are discussed below).
The first step is to open the file in a text editor and modify it so that it's correct. You'll have to remove all of the conflict markers (lines inserted by Git starting with <<<<<<<
, =======
, or >>>>>>>
).
$ vim gettysburg-address.txt
Be careful when you resolve conflicts! The resulting file must be syntactically and semantically correct. Once the file is correct, save it and close the editor.
After saving a new, correct version of the file, add it to the staging and then commit it. For merge commits Git pre-populates the editor with a default commit message. In some cases you can accept the default, but in most cases you should write your own, more descriptive message. In this case, use the message "Apply grammar edits to final draft".
$ git add gettysburg-address.txt
$ git commit
Apply grammar edits to final draft
Conflicts:
gettysburg-address.txt
#
# It looks like you may be committing a merge.
...
Reviewing the log now, you can see that it follows a "diamond" shape (the latest commit has two parents, both of which had the same parent).
$ git log --oneline --graph
* d75cf7e Apply grammar edits to final draft
|\
| * 247b7c3 Add final draft of my address
* | 56c2f97 Make grammar edits to the address
|/
* 10f2157 Add first draft of my address
Now you've successfully resolved a merge conflict!
The following command opens some tool that allows you to find, understand, and resolve conflicts. These tools are useful for understanding the intent of both commits, but can be confusing to use to resolve conflicts.
$ git mergetool # [-t TOOL]
This opens a window that displays the contents of the files and indicates the location of conflicts. Which tools opens depends on your operating system, your installed merge tools, and whether you've set a default tool.
It's often most useful to use the graphical tools to understand the conflicting edits and a text editor to resolve the conflicts. This is because, as stated above, resolving a conflict is often more complicated than just accepting the remote or local (and these are the main options a graphical tool presents).
Diagrams below are presented as an overview. Don't worry about reading the text in the diagrams carefully — it's better for you to run mergetool
on your own computer and follow along.
Here is what OpenDiff looks like. You can see from the titles on each pane that the left one is the local version and the right is the remote version.
Notice that it highlights in blue differences within each conflicting line, and highlights in gray groups of conflicting lines. The currently-selected conflict group is outlined in red.
At this point, you will notice the options provided at the bottom right. You'll use this dropdown menu to describe how to resolve the currently-selected conflict. In the first case, you can choose to accept either the left or the right.
After selecting a particular option, you can choose another conflict and resolve it in the same way.
However, the other conflicts are more complicated; none of options in the dropdown menu describe how to resolve this conflict. In this case, you'll have to open the file in a text editor and merge them manually (you'll probably end up with some combination of both the remote and the local, or neither). However, even as you modify the file in a text editor, you should leave this window open to help you find and understand the conflicts that exist.
You can close and re-open this tool at any time. Remember that the merge isn't final until you commit!
This is a common merge tool on Linux (shown below). Unlike OpenDiff, it shows four panes of text: one (top left) for the BASE
or common ancestor commit, another (top middle) for the local commit, a third (top right) for the remote commit, and a fourth (on bottom) for the output. As a side note, remember to verify which pane is which in your particular merge tool — even two tools that show four panes might show different versions in each pane.
KDiff is showing three panes (it includes the original, BASE
, commit), so this is really a three-way merge.
It will also show you where the conflicts are and let you choose, for each conflict, which code to place into the output pane. You'll do this by clicking a button in the UI with the number of the pane from which you want to take the code. You can take code from multiple panels in any order by clicking multiple the buttons for multiple panes.
KDiff also allows you to scroll back and forth between conflicts and manually edit the contents of the output pane. It's a very good conflict resolution tool if you know how to use it!
These are special multi-pane views in Emacs and Vim, respectively. They're complicated, and not recommended unless you're a serious Emacs or Vim user. I'd recommend manually resolving the conflicts using your favorite text editor or using a graphical tool (such as OpenDiff, above).
If they pop up unexpectedly in all their three- or four-pane glory, just close them. You can close Emacs with "CTRL-X, CTRL-C" and Vim by hitting Escape, typing ":q!", and then hitting Enter.
Here's what VimDiff might look like (EDiff looks similar):
As in other graphical editors, you can see the local commit on the left pane, the BASE
(common parent) in the middle, the remote on the right, and the output below. Note that this is a different order than KDiff!
If you want to know if you resolved the conflicts correctly, the quick and easy answer is: write and run tests. If every commit includes both code and a test for that code, then you can test the correctness of any merge by running the tests. Otherwise, you'll have to verify its correctness through manual testing, talking to the author of the other commit, and inspection.
Oh-my-goodness-yes-that-can-happen.
Here's a story from one of the author's internships. I was working on refactoring a module I'd written and pushed already. I ended up removing several large chunks of code and rewriting them differently in a different place. That was fine and normal.
As I was doing that, my co-worker was working on a long-running project. He had his own feature branch to keep his development separate from trunk (the main development branch), exactly as he should. He was also regularly pulling from trunk into his branch so that he stayed up-to-date with the rest of the company's work — this meant smaller merges, fewer conflicts, and easier conflicts when they appeared. He was doing everything right.
My co-worker updated his branch with the version of the module that I'd already pushed, and then I pushed my update to the module. Soon after, my co-worker merged his feature branch into trunk (possibly without merging trunk into his feature branch again).
In my refactor, there were some places that I added code and some places that I removed it. When the version control software compared the trunk version to the feature branch version, it saw that the trunk had some lines the feature branch didn't (these were the lines I added in the refactor) and the feature branch had some lines the trunk didn't (these were the lines I removed in the refactor).
Our version control was happy to merge these by including all the lines from both trunk and the feature branch. In other words, it added again the lines I'd specifically removed from trunk.
In this case, the modifications were such that the bad merge didn't fail any tests. Later on, when I was reviewing the code again, I was confused to see some lines that I thought I'd removed. On a hunch, I checked the commit logs — yep, last modified by an automated merge. When something looks funny and it was last modified in a merge, there's a real chance someone (or something) made a mistake.
And that's how any version control system, even operated by carefully and responsibly, can still make a mistake (and potentially cause a problem).
You've committed (and possibly pushed) a mistake. Oh well. It happens. Nobody's going to blame you. How do you fix it?
The best strategy, which works in all cases, is: make your corrections and add them to a new commit on top of the faulty merge.
The super-fancy strategy, which can accidentally delete your work, is harder than the strategy above, but can help you save face (if you really think embarrassment is worse than deleting work), is: delete the bad merge so that it's like it never existed, re-run the merge, and this time resolve the conflicts correctly. This only works if you haven't pushed the faulty merge (user-friendly version control such as Mercurial will prevent you from rewriting history like this, but Git won't, so be careful!).
How can you avoid as many conflicts as possible?
Push and pull early and often. There are two reasons to commit: first, to save a logical unit of work, and second, to communicate to your team that you're working on a particular section of code. The sooner you notice a conflict, the easier it'll be to fix and the more conflicts you'll avoid in the future. If you haven't pushed or pulled in a few days, you should be feeling nervous.
Make an effort to work on independent pieces of code. Don't be stupid. Don't assign two people to work on to the same code at the same time. Make one of them go second, or neither of them is going to finish, let alone finish right.
If you see a conflict, mention it to the other committer. This is the best way to avoid future conflict (it's a good general rule that communication resolves conflict. There is a corollary, though, which is that it only works of neither of you is an a**hole). This tip goes along with the previous one — if you accidentally start working on the same code, then you should speak up as soon as you figure it out.
Here are some other general tips for using version control:
Avoid the -f
or --force
flags. Using the force flag probably means you're doing something wrong.
Don't just do what Git tells you (seriously). If Git presents a warning or error and suggests a command you can run to fix it, make sure you understand the problem, the message, and what the command will do before you run the suggested command. You'll probably find that the suggested command is not what you want.
Learn Git by learning its implementation. Most Git commands are backed up directly by low-level operations on the Git data structures (not much abstraction here!). Learning about how Git is implemented can help you predict Git's behavior and remember its commands.