Overview
Working with Git, especially in collaborative environments, often results in multiple developers working on the same codebase. As a result, merge conflicts are a natural part of version control systems like Git. Understanding why they occur is critical for resolving them efficiently and minimizing disruption to your workflow.
In this post, we’ll explore:
- What merge conflicts are.
- Common causes of merge conflicts.
- Examples of scenarios where merge conflicts occur.
- How Git identifies and handles conflicts.
- Best practices to avoid and resolve conflicts.
1. What Are Merge Conflicts?
In Git, a merge conflict happens when Git is unable to automatically merge changes made to a file by different contributors. This typically occurs during a merge operation (e.g., merging a branch into another) or when rebasing changes.
Merge conflicts arise when multiple changes are made to the same line or region of a file, and Git cannot determine which version should take precedence.
A Simplified Example:
Imagine two developers working on the same file. Developer A changes line 25 of a file, while Developer B modifies the same line 25 in a different branch. When Git attempts to merge the branches, it encounters two different changes to the same line, which creates a merge conflict. Git will pause the merge process and ask you to resolve the conflict manually.
2. Common Causes of Merge Conflicts
Merge conflicts can happen for a variety of reasons, and understanding these common scenarios can help you avoid them or at least be better prepared to resolve them when they occur.
2.1. Conflicting Changes to the Same Line
The most common cause of merge conflicts is when two developers make changes to the same line of code. If both developers modify the same lines in a file, Git cannot automatically determine which version is correct, so a merge conflict occurs.
Example:
- Developer A changes line 20 in a file to
const apiUrl = "https://api.dev.com"; - Developer B changes the same line 20 in another branch to
const apiUrl = "https://api.staging.com";
When Git tries to merge these changes, it encounters a conflict because it cannot know whether to use Developer A's version or Developer B's version of apiUrl.
2.2. Conflicting Changes to the Same File Section
Even if changes are made to different lines in the same file, conflicts can still occur if those changes are close together or if they affect how the code behaves as a whole. This is common in scenarios where multiple people are modifying different parts of the same function, method, or configuration block.
Example:
- Developer A adds a new parameter to a function in one branch.
- Developer B modifies the function’s logic in another branch.
When these changes are merged, Git may not know how to reconcile them, leading to a conflict.
2.3. Deleting a File Modified in Another Branch
Conflicts can also happen when a file is deleted in one branch but modified in another. Git can't decide whether to keep the modifications or to respect the deletion, so it flags the file as conflicting.
Example:
- Developer A deletes
config.jsonin one branch. - Developer B adds new settings to
config.jsonin another branch.
When merging, Git will not know whether to keep the changes made by Developer B or delete the file as Developer A intended.
2.4. Branch History Divergence
The more divergent the history of the branches being merged, the more likely you are to run into conflicts. Divergent branch histories mean that the two branches have evolved significantly apart, with many changes, making it harder for Git to perform an automatic merge.
This usually happens when:
- Long-lived feature branches are not frequently merged with the main branch, causing significant changes to accumulate on both sides.
- There are parallel development streams, where developers work on different aspects of the same codebase for long periods without syncing their changes.
3. Examples of Merge Conflict Scenarios
Let's break down a few practical examples to better understand how and when merge conflicts occur in real-world development scenarios.
Example 1: Conflict Due to Overlapping Changes
In this case, two developers make changes to the same part of a file but in different branches.
- Initial File:
function fetchData() {
let apiUrl = "https://api.example.com";
return fetch(apiUrl);
}
- Developer A (on
feature-branch-a) changes the function to use a different API URL:
function fetchData() {
let apiUrl = "https://api.development.com";
return fetch(apiUrl);
}
- Developer B (on
feature-branch-b) changes the variable name and modifies the function slightly:
function fetchData() {
let apiEndpoint = "https://api.example.com";
return fetch(apiEndpoint);
}
When Git tries to merge these changes, it will be unable to automatically merge the differences because both developers altered the same line of code.
Example 2: Deleting vs. Modifying a File
In this example, one developer deletes a file while another modifies it.
- Developer A (on
cleanup-branch) deletes a fileconfig.json:
git rm config.json
git commit -m "Remove unused config file"
- Developer B (on
feature-branch) updates the same file with new configurations:
{
"apiKey": "12345",
"timeout": 5000
}
When Git tries to merge the two branches, it will flag the conflict because it can’t determine whether the file should be deleted (as Developer A intended) or kept with the new changes (as Developer B intended).
4. How Git Identifies and Handles Conflicts
Git uses a three-way merge algorithm to try and merge changes. When you initiate a merge, Git compares the following three points:
- Common ancestor: The last shared commit between the two branches.
- Your changes: Changes in your current branch.
- Their changes: Changes in the branch you’re trying to merge.
If the changes don't overlap or affect the same lines of code, Git will merge the changes automatically. If they do conflict, Git will halt the process and report a merge conflict, asking you to resolve it manually.
How Git Marks Conflicting Sections
When a conflict occurs, Git marks the conflicting section of the file with conflict markers:
<<<<<<< HEAD
// Your changes (current branch)
let apiUrl = "https://api.development.com";
=======
// Their changes (other branch)
let apiEndpoint = "https://api.example.com";
>>>>>>> feature-branch-b
You must then edit the file to manually resolve the conflict by choosing the correct version or integrating both changes.
5. Best Practices to Avoid and Resolve Merge Conflicts
While merge conflicts are inevitable in collaborative development, you can follow certain best practices to reduce their frequency and resolve them more easily.
5.1. Frequent Pulls and Syncs
Avoid long-lived branches by frequently syncing your feature branches with the main or master branch. Regularly pulling changes from the main branch will minimize the amount of divergence between branches, reducing the likelihood of conflicts.
5.2. Smaller, Atomic Commits
Make small, focused commits that solve one problem at a time. Larger, sweeping changes increase the chances of introducing conflicts, especially in heavily modified files. Atomic commits are easier to resolve because they affect fewer lines of code.
5.3. Communicate with Your Team
Good communication within the team is key to avoiding conflicts. Let your teammates know when you’re working on critical files or functions. Use code ownership tools (available in GitLab and GitHub) to assign certain files or directories to specific developers or teams, which helps coordinate changes.
5.4. Use Feature Flags
Instead of working on long-lived feature branches, consider using feature flags to hide incomplete work. This allows you to merge changes into the main branch without deploying unfinished features, reducing the likelihood of conflicts from extended branch isolation.
5.5. Rebasing Before Merging
Before merging your branch, consider rebasing it on top of the target branch (e.g., main). Rebasing allows you to apply your changes on top of the latest commits from the main branch, integrating changes incrementally and reducing merge conflicts during the final merge.
git checkout feature-branch
git fetch origin
git rebase origin/main
This approach resolves conflicts incrementally, rather than all at once at merge time.
5.6. Use Code Review Tools
Code review tools such as merge requests (MRs) in GitLab or pull requests (PRs) in GitHub encourage team collaboration and careful code inspection before merging. They provide opportunities to spot potential conflicts early on.
Conclusion
Merge conflicts are a natural part of using Git, especially in collaborative projects with many developers working on the same codebase. Understanding why merge conflicts happen—whether due to conflicting changes, file deletions, or diverging branch histories—can help you handle and prevent them more effectively.
By following best practices, such as syncing your branches frequently, making small commits, and communicating within your team, you can significantly reduce the risk of merge conflicts and resolve them with confidence when they do occur.