At some point when working with a partner, you will get a merge conflict in git. This is fine and does not mean you are using git wrong or that your files are irrevocably broken. This document is meant to provide an overview of how to resolve your first or many merge conflicts. A conflict usually happens when both partners modify similar regions of a file. Let’s see how this situation arises and see how to fix it. Suppose two collaborators, Kacey and Zach, start with the following code

test1.c
#include <stdio.h>

/* TODO: add declarations for helper, mystery here */
void func1();

int main(){

  return 0;
}

Creating a merge conflict

The partners agree that Kacey should write the function helper while Zach writes mystery. Suppose Kacey adds the helper prototype, then adds, commits, and pushes her change to the shared repo.

Kacey prototype
#include <stdio.h>
void helper(); /* Kacey */

int main(){

  return 0;
}
Kacey push
[lab]$ git add test1.c
[lab]$ git commit -m "added helper"
[lab]$ git push

Now suppose Zach does not know Kacey pushed her changes. He adds his prototype for mystery and has no problem adding and committing his change locally.

Zach prototype
#include <stdio.h>

/* TODO: add declarations for helper, mystery here */
void func1();
void mystery(); /* Zach */

int main(){

  return 0;
}
Zach commit
[lab]$ git add test1.c
[lab]$ git commit -m "added mystery"

Since add and commit are local changes, each partner is free to modify his or her code locally without conflicts. But what happens when Zach tries to push?

Zach push
[lab]$ git push
To github.swarthmore.edu:CS101/lab.git
 ! [rejected]        main -> main (fetch first)
error: failed to push some refs to 'github.swarthmore.edu:CS101/lab.git'
hint: Updates were rejected because the remote contains work that you do not
hint: have locally. This is usually caused by another repository pushing to
hint: the same ref. If you want to integrate the remote changes, use
hint: 'git pull' before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

Oops! This looks like a scary error message. But this is not too worrisome. It just says someone else, in this case Kacey, has pushed before and Zach has to pull those changes before he can push his changes. So let’s try that.

Zach pull
[lab]$ git pull
Auto-merging test1.c
CONFLICT (content): Merge conflict in test1.c
Automatic merge failed; fix conflicts and then commit the result.

Well, grumble. More errors. Git noted that there is a conflict in test1.c and tells Zach that he must fix them and commit the result. Note when we run git status it will list conflicted files as both modified: and tells us that we have unmerged paths.

Zach status
[lab]$ git status
On branch main
Your branch and 'origin/main' have diverged,
and have 1 and 1 different commits each, respectively.
  (use "git pull" if you want to integrate the remote branch with yours)

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:   test1.c

no changes added to commit (use "git add" and/or "git commit -a")

Fixing a conflict

Okay. First, don’t panic. We will soon fix this. But second, git is giving some bad advice. If we look just at the unmerged paths section it says "use git add <file>…​ to mark resolution". So, we might be tempted to run git add to make this message go away. But the part we missed reading was the line above that says we need to fix conflicts and then run git commit. The real truth is that we need to first fix the conflicts, then run both add and commit. Let’s see what the file looks like currently.

Merged file
$ cat test1.c
#include <stdio.h>

/* TODO: add declarations for helper, mystery here */
void func1();
<<<<<<< HEAD
void mystery(); /* Zach */
=======
void helper(); /* Kacey */
>>>>>>> 26451221b23c56f259ba7ba42fd3ef7fe7ca9671

int main(){

  return 0;
}

Note that git has added some tags, <<<<<, ====, and >>>>> to mark the regions of the file that have the conflict. Because of these tags, test1.c will not compile. Git ruined everything! Not really. We can fix this. Git was just confused. Both Kacey and Zach modified the same file and git was unsure if there should be one function called helper, one function called mystery, or two separate functions. In this case, git might not know what to do, but Zach and Kacey discussed their plan before. It should be two functions. All Zach needs to do is delete the tags <<<<<, ====, and >>>>>, keeping changes from both partners. The new correctly merged file looks like this now.

Zach corrections
#include <stdio.h>

/* TODO: add declarations for helper, mystery here */
void func1();
void mystery(); /* Zach */
void helper();  /* Kacey */

int main(){

  return 0;
}

After checking that the merged code compiles again, Zach is ready to resolve the conflict and push back the merged file.

Resolved merge conflict
[lab]$ git add test1.c
[lab]$ git commit -m "merged prototype changes"
[lab]$ git push

Enumerating objects: 10, done.
Counting objects: 100% (10/10), done.
Delta compression using up to 16 threads
Compressing objects: 100% (6/6), done.
Writing objects: 100% (6/6), ... bytes | ... KiB/s, done.
Total 6 (delta 2), reused 0 (delta 0), pack-reused 0 (from 0)
remote: Resolving deltas: 100% (2/2), completed with 1 local object.
To github.swarthmore.edu:CS101/lab.git
   2645122..7bff848  main -> main

Won’t this seesaw back and forth? Isn’t Kacey going to have the same problem? Nope. Git can see that Zach marked the conflict as resolved, so if Kacey pulls, assuming she hasn’t made recent local changes, she’ll get Zach’s merged version.

Does this always happen?

Won’t merging always result in conflict? The previous conflict is resolved and Kacey and Zach can go back to editing code. Maybe Kacey decides to clean up the documentation while Zach decides to implement his mystery function. Let’s assume Zach pushes his changes first.

Zach implementation
#include <stdio.h>

/* TODO: add declarations for helper, mystery here */
void func1();
void mystery(); /* Zach */
void helper(); /* Kacey */

int main(){

  return 0;
}

void mystery(){
  printf("Zach's mystery function\n");
}
Zach push
[lab]$ git add test1.c
[lab]$ git commit -m "building a mystery"
[lab]$ git push

No problems here. Zach pushed his changes to the shared repo. Simultaneously, Kacey has been working on some documentation changes at the top of the file.

Kacey documentation
#include <stdio.h>

/* func1 part of starter code */
void func1();

/* runs core algorithm */
void mystery();

/* a utility function */
void helper();

int main(){

  return 0;
}
Kacey commit
[lab]$ git add test1.c
[lab]$ git commit -m "added documentation"

When Kacey tries to push, she’ll get the same error Zach got in the previous example where git complains that she has to pull some changes first. Oh no, here comes another conflict.

Kacey pull
$ git pull
X11 forwarding request failed on channel 0
remote: Enumerating objects: 5, done.
remote: Counting objects: 100% (5/5), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 3 (delta 1), reused 3 (delta 1), pack-reused 0 (from 0)
Unpacking objects: 100% (3/3), 340 bytes | 9.00 KiB/s, done.
From github.swarthmore.edu:CS101/lab
   7bff848..c697816  main       -> origin/main
Auto-merging test1.c
Merge made by the 'ort' strategy.
 test1.c | 4 ++++
 1 file changed, 4 insertions(+)

Hey! No conflict! Git was smart enough in this case to auto merge. Kacey modified some stuff at the top of the file, while Zach was editing near the bottom. There’s no ambiguity regarding what belongs where, so git mashes the two files together in a sensible way.

Once the excitement of git auto merging subsides, remember that even though the files were successfully merged, this happened on Zach’s local copy. To share with Zach, she still needs to push the changes.

Kacey push
[lab]$ git status
On branch main
Your branch is ahead of 'origin/main' by 2 commits.
  (use "git push" to publish your local commits)

[lab]$ git push

Zach can now run git pull and both partners will have the most up to date code.

Weird editor settings

In the case of a successful auto-merge when running git pull, git will write a commit message for you announcing its triumph. Git gives you an opportunity to modify this message by bringing up your default editor. For some emacs users, this so called default editor might be the dreaded vim. First, don’t panic. Just hit the <ESC> key then type :wq and press enter. Of course there is a colon in front of the wq, why wouldn’t it be there? If you never want to experience vim again, change your default editor. First try the easy way.

change default git editor
git config --global core.editor "emacs"

Or replace emacs above with vim, or your editor of choice. Using vscode is too heavy handed, though it can be done with the option "code --wait". nano, emacs, or vim are all sensible choices.

If that doesn’t solve the problem, open up the file called ~/.bash_profile using your preferred editor, e.g.,

editing .bash_profile for editor settings
[~]$ xemacs ~/.bash_profile &

Edit the lines that say EDITOR=vi and VISUAL=vi to be your preferred editor, e.g., EDITOR=emacs. While you are at it, it might help to change the line a few lines above that says LESS='-eM' to say LESS='-eRMX'. This will make git log and git diff a bit more colorful if you use those tools. After changing your ~/.bash_profile, the changes will take effect the next time you log into the CS machines. If you want the changes to take effect immediately, type source ~/.bash_profile at the terminal prompt.

Discarding edits

Some merge conflicts are difficult to resolve, but most can be fixed relatively easily by communicating effectively with your partner/team. Sometimes you realize what you really want is just the original copy of your partner’s file or the original copy of your files. There is nothing really to merge, you just want to accept one version as being correct. Say you have a conflict and you realize what you really want is your partner’s copy that you pulled, but which git merged into an ugly non-compiling, conflicted version. If you run the following command you get the copy of the file as it was pushed, throwing away your changes.

Accept partner’s version
[lab]$ git checkout --theirs test1.c

If your partner pushed a bad copy of the file and you have the ideal replacement, use:

Accept your version
[lab]$ git checkout --ours test1.c

You won’t need to edit the file by hand anymore, but you still need to mark the conflict as resolved using git add, git commit, and git push, just like you would for previous merge conflicts or any updates you wish to share.

If after running checkout with --theirs or --ours, but before adding and committing the new version, you want to go back to the messed up merged version and fix it by hand, you can do that too.

Review merged version
[lab]$ git checkout --merge test1.c

References