Git merge conflicts happen when Git can’t automatically figure out how to combine changes from different branches, usually because the same lines of code were modified in both.

Let’s say you have a main branch and a feature branch. You’ve been working on feature, adding a new function. Meanwhile, someone else updated the same part of the file on main. When you try to merge feature into main, Git throws a conflict.

Here’s a typical scenario:

You’re on main, and you run:

git merge feature

Git responds with:

Auto-merging src/app.js
CONFLICT (content): Merge conflict in src/app.js
Automatic merge failed; fix conflicts and then commit the result.

This means Git found a disagreement in src/app.js. It doesn’t know which version of the conflicting code to keep.

Common Causes and How to Fix Them

The core of a merge conflict is always the same: Git sees two different versions of the same text and needs you to decide which one is correct, or if a new version combining both is needed.

  1. Conflicting changes on the same lines: This is the most frequent cause. Both branches modified the exact same lines of code.

    • Diagnosis: Open the conflicted file (e.g., src/app.js). You’ll see markers like <<<<<<<, =======, and >>>>>>>.

      <<<<<<< HEAD
      function greet(name) {
        console.log("Hello, " + name + "!");
      }
      =======
      function greet(name, greeting = "Hello") {
        console.log(greeting + ", " + name + "!");
      }
      >>>>>>> feature
      

      The HEAD section is what’s currently in your main branch. The feature section is what came from the branch you’re merging.

    • Fix: Manually edit the file to contain the desired final version. Remove the <<<<<<<, =======, and >>>>>>> markers. In our example, you might decide to keep the new greeting parameter:

      function greet(name, greeting = "Hello") {
        console.log(greeting + ", " + name + "!");
      }
      

      Then, stage the resolved file:

      git add src/app.js
      
    • Why it works: You’re telling Git, "I’ve reviewed the conflicting changes and this is the final, correct version." git add marks the file as resolved.

  2. One file deleted, another modified: A file was deleted on one branch and modified on another. Git doesn’t know if the modification should be kept or if the deletion should prevail.

    • Diagnosis: Git will list the conflicted file and indicate its status.

      git status
      

      Output might show:

      Unmerged paths:
        (use "git add <file>..." to mark resolution)
          both modified:  config.yml
      

      Or, if a file was deleted on main and modified on feature:

      Unmerged paths:
        (use "git add/rm <file>..." to mark resolution)
          both deleted:   old_feature.txt
          modified elsewhere: old_feature.txt
      
    • Fix: Decide whether to keep the modified file or honor the deletion.

      • To keep the modified file: git add <conflicted_file>
      • To honor the deletion (i.e., delete the file on the merged branch): git rm <conflicted_file> then git commit (Git will usually infer this if you git rm a file that Git thinks is deleted on HEAD). If the file was deleted on HEAD and modified on feature, you might need to git checkout HEAD -- <conflicted_file> to restore the deletion from HEAD’s perspective and then git add <conflicted_file>.
    • Why it works: git add or git rm explicitly tells Git your decision about the file’s final state.

  3. Conflicting renames: A file was renamed on one branch and modified on another. Git gets confused about tracking the file.

    • Diagnosis: git status will often show a file as both "modified" and "deleted" or similar, or mention a rename conflict.

      Unmerged paths:
        (use "git add/rm <file>..." to mark resolution)
          both modified:  src/utils.js
          deleted:        old_util.js
      

      This often happens when old_util.js was renamed to src/utils.js on one branch, and old_util.js itself was modified on another.

    • Fix: You need to tell Git what the final name and content should be.

      • If the rename is correct and the modification should be applied to the new name:

        git mv old_util.js src/utils.js # This stages the rename and the content merge
        git add src/utils.js # If there were actual content conflicts within the renamed file
        

        Or more directly, if Git already detected the rename and modification:

        git add src/utils.js # If the rename is accepted and content is fine
        

        If there were content conflicts within the renamed file, you’d edit src/utils.js to resolve them, then git add src/utils.js.

      • If the rename should be rejected and the file should remain old_util.js with its modifications:

        git checkout HEAD -- old_util.js # Revert to the deletion state from HEAD
        git add old_util.js # Stage the modified version of the old name
        
    • Why it works: git mv is Git’s way of tracking renames. By using git add or git rm, you confirm Git’s understanding of the file’s lifecycle.

  4. Large binary files with conflicting edits: Git doesn’t do well with merging binary files (like images, PDFs, compiled executables). If two branches modify the same binary file, Git can’t merge them.

    • Diagnosis: git status will show the binary file as unmerged.

      Unmerged paths:
        (use "git add <file>..." to mark resolution)
          binary file <filename> has both differences
      
    • Fix: You must choose one version or the other. You cannot merge binary files.

      • To keep the version from the branch you’re merging into (e.g., main): git checkout HEAD -- <binary_file>
      • To keep the version from the branch you’re merging from (e.g., feature): git checkout feature -- <binary_file> Then stage the chosen file:
      git add <binary_file>
      
    • Why it works: You’re explicitly telling Git which of the two identical-but-different binary files to keep.

  5. Submodule conflicts: If you’re using Git submodules, and a submodule has been updated differently in both branches you’re merging, you’ll get a submodule conflict.

    • Diagnosis: git status will show the submodule as "modified".

      Unmerged paths:
        (use "git add <modified_submodule>..." to mark resolution)
          modified:   my_submodule (new commits)
      

      This means the commit hash recorded for my_submodule differs between the branches.

    • Fix: You need to decide which commit of the submodule to use.

      • To use the commit from the HEAD branch:
        git checkout HEAD -- my_submodule
        git add my_submodule
        
      • To use the commit from the feature branch:
        git checkout feature -- my_submodule
        git add my_submodule
        

      You might also need to cd my_submodule and git checkout <specific_commit_hash> if you need a particular version not currently checked out.

    • Why it works: You’re updating the superproject’s record of the submodule’s commit hash to reflect your chosen version.

Completing the Merge

Once you’ve resolved all conflicts and staged the files:

git status # Verify all conflicts are resolved and staged
git commit

Git will usually pre-populate a commit message like "Merge branch 'feature' into main". You can edit it or use it as is.

After fixing all merge conflicts and committing, the next error you might encounter is a CI/CD pipeline failure due to tests not passing, or a new bug introduced by the merge that wasn’t apparent during the conflict resolution.

Want structured learning?

Take the full Git course →