Resolve a Merge Conflict

Merge conflicts are an inevitable part of collaborative software development, occurring when Git cannot automatically reconcile diverging changes in the…

Merge conflicts are an inevitable part of collaborative software development, occurring when Git cannot automatically reconcile diverging changes in the same part of a file. Understanding how to systematically identify, resolve, and commit these conflicts is a fundamental skill for anyone using Git.

Understanding Merge Conflicts

A merge conflict arises when two or more branches have modified the same lines in a file, or when one branch has deleted a file that another branch has modified. Git's automatic merge algorithm excels at combining changes when they affect different parts of a file or different files entirely. However, when the exact same lines are altered differently, or a file's existence is contested, Git pauses the merge process and flags the conflict for manual intervention.

Common scenarios leading to conflicts include:

  • Two developers modify the same function or block of code.
  • One developer renames a file while another modifies its content.
  • One branch deletes a file that another branch has changed.

When a conflict occurs during operations like git merge or git pull, Git will report the conflict and leave the conflicting files in a special state within your working directory.

Identifying Conflict Markers

Upon encountering a conflict, Git modifies the conflicted files by inserting special markers to delineate the differing versions of the code. These markers clearly show the changes from the current branch (HEAD) and the incoming branch.

<<<<<<< HEAD
// This is the code from my current branch (HEAD)
function calculateSum(a, b) {
    return a + b; // My version returns the sum
}
=======
// This is the code from the branch I'm merging (e.g., 'feature-X')
function calculateSum(num1, num2) {
    return num1 * num2; // Their version returns the product
}
>>>>>>> feature-X/update-calculation

Let's break down these markers:

  • <<<<<<< HEAD: Marks the beginning of the conflicting block from your current branch.
  • =======: Separates the content from your current branch (above) and the incoming branch (below).
  • >>>>>>> <branch-name>: Marks the end of the conflicting block, indicating the content from the branch being merged (e.g., feature-X/update-calculation or a commit hash if merging a specific commit).

The output of git status is crucial for identifying which files are conflicted:

$ git status
On branch main
You have unmerged paths.
  (fix conflicts and run "git commit")
  (use "git merge --abort" to abandon merge)

Unmerged paths:
  (both modified: src/app.ts)

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

In this example, src/app.ts is the conflicted file, indicated by "both modified".

Resolving Conflicts Manually

The core of conflict resolution involves manually editing the files containing conflict markers. You must decide which changes to keep, which to discard, or how to combine them into a single, correct version.

Step 1: Edit the Conflicted File(s)

Open each conflicted file in your preferred text editor. Locate all instances of the <<<<<<<, =======, and >>>>>>> markers. Your task is to modify the file to contain the desired final state, removing all conflict markers in the process.

Consider the previous example:

<<<<<<< HEAD
// This is the code from my current branch (HEAD)
function calculateSum(a, b) {
    return a + b; // My version returns the sum
}
=======
// This is the code from the branch I'm merging (e.g., 'feature-X')
function calculateSum(num1, num2) {
    return num1 * num2; // Their version returns the product
}
>>>>>>> feature-X/update-calculation

If you decide to keep the functionality from your current branch (summation), the resolved file would look like this:

// This is the final, resolved code
function calculateSum(a, b) {
    return a + b;
}

Alternatively, if you decide to incorporate both functionalities (perhaps renaming the existing function and adding the new one, or merging the logic), the resolution might be more complex:

// This is the final, resolved code
function calculateSum(a, b) {
    return a + b;
}

function calculateProduct(num1, num2) {
    return num1 * num2;
}

Ensure that after editing, no <<<<<<<, =======, or >>>>>>> markers remain in the file.

Step 2: Stage the Resolved File(s)

Once you have manually resolved all conflicts in a file, you must tell Git that the file is ready. This is done by staging the file, just like any other change.

git add src/app.ts

Repeat this git add command for every file that was listed as "Unmerged paths" by git status. As you stage files, git status will update, showing them under "Changes to be committed":

$ git status
On branch main
All conflicts fixed but you are still merging.
  (use "git commit" to conclude merge)

Changes to be committed:
        modified:   src/app.ts

Step 3: Commit the Merge

After all conflicted files have been resolved and staged, the final step is to commit the merge. Git will typically pre-populate the commit message with details about the merge. It's good practice to review and, if necessary, refine this message to accurately describe how the conflicts were resolved.

git commit

This command will open your default text editor (e.g., Vim, Nano) with a pre-filled merge commit message. Save and exit the editor to complete the merge.

Example default merge commit message:

Merge branch 'feature-X' into main

# Conflicts:
#       src/app.ts
#
# It looks like you may be editing a merge commit message.
# If you skip this file, an empty message will be used.
# However, you can save your changes and Git will use them.
#
# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# On branch main
# All conflicts fixed but you are still merging.
#
# Changes to be committed:
#       modified:   src/app.ts
#

Using Merge Tools

For more complex conflicts, or when dealing with many conflicted files, a graphical merge tool can significantly simplify the resolution process. Git can be configured to use external merge tools like KDiff3, Meld, Beyond Compare, or VS Code's built-in merge editor.

Configuring a Merge Tool (Example: VS Code)

To configure VS Code as your default merge tool:

git config --global merge.tool vscode
git config --global mergetool.vscode.cmd 'code --wait $MERGED'
git config --global mergetool.vscode.trustExitCode false

Once configured, when a conflict occurs, you can launch the merge tool:

git mergetool

Git will iterate through each conflicted file, opening your configured merge tool. The tool typically presents three panes: local changes (HEAD), remote changes (incoming branch), and the common ancestor, allowing you to select lines or blocks from either side and manually edit the resulting merged file. After saving and closing the merge tool for a file, Git will automatically stage it. Press 'y' when prompted to mark the conflict as resolved and proceed to the next file, or to exit the mergetool process.

Aborting a Merge

If you find the conflicts too complex to resolve, or if you initiated a merge by mistake, you can abort the merge operation and revert your repository to its state before the merge attempt. This is a safe way to back out without losing any work on your current branch.

git merge --abort

This command will:

  • Reset your working directory to the state it was in before you started the merge.
  • Clear any conflict markers.
  • Not discard any uncommitted changes you had prior to the merge attempt.

It's important to note that git merge --abort only works if the merge operation is still in progress and has not yet been committed.

Common Pitfalls and Troubleshooting

  • Forgetting to stage resolved files: Git won't let you commit an incomplete merge. Always use git add <file> after resolving each file's conflicts.
  • Leaving conflict markers: If you commit with conflict markers still present, your code will likely fail, and the markers will appear in your codebase. Always double-check your files for <<<<<<<, =======, and >>>>>>> before committing.
  • Resolving incorrectly: Sometimes, the logical resolution isn't just picking one side over the other. It might involve combining logic or rewriting sections. Thoroughly test your code after a merge, especially after resolving conflicts.
  • Ignoring non-content conflicts: Conflicts aren't always about file content. If a file was deleted on one branch and modified on another, you'll see "deleted by us" or "deleted by them" in git status. You'll need to decide whether to delete the file (git rm <file>) or keep it (git add <file>).
  • Too many conflicts: If a merge involves an excessive number of conflicts, it might indicate a divergent development path, or that the feature branches have lived too long. Consider rebasing smaller, more frequent merges to reduce conflict surface area.

Back to the knowledge base · Ask the AI assistant