Git Three-Way Merge
When working with Git, you'll frequently need to combine changes from different branches. Git's three-way merge is a powerful mechanism that intelligently resolves changes between branches. In this tutorial, we'll explore how three-way merges work, when they occur, and how to handle merge conflicts.
What is a Three-Way Merge?
A three-way merge is Git's method for combining two different branch histories when they have diverged from a common base commit. Unlike a fast-forward merge (which simply moves the branch pointer forward), a three-way merge is necessary when both branches have new commits that the other doesn't contain.
The "three-way" refers to the three commits involved in the merge:
- The common ancestor commit (base)
- The commit at the tip of your current branch
- The commit at the tip of the branch you're merging in
Git uses all three commits to determine the final state of your files.
When Does Git Use a Three-Way Merge?
Git performs a three-way merge when:
- You run
git merge <branch>
to merge another branch into your current branch - Both branches have diverged (have unique commits)
- A fast-forward merge isn't possible
Visualizing a Three-Way Merge
Let's visualize how a three-way merge works:
In this diagram:
- Commit B is the common ancestor (base)
- Commits C and D were made on the feature branch
- Commits E and F were made on the main branch
- The final merge commit combines changes from both branches
Three-Way Merge in Action
Let's walk through a practical example of a three-way merge:
Step 1: Create and switch to a new branch
# Create a new branch and switch to it
git checkout -b feature-login
Step 2: Make changes in your feature branch
# Edit files and make commits
echo "function login() { /* login code */ }" > login.js
git add login.js
git commit -m "Add login functionality"
Step 3: Switch back to main and make different changes
# Switch to main branch
git checkout main
# Make different changes
echo "function register() { /* register code */ }" > register.js
git add register.js
git commit -m "Add registration functionality"
Step 4: Merge the feature branch into main
git merge feature-login
Output:
Merge made by the 'recursive' strategy.
login.js | 1 +
1 file changed, 1 insertion(+)
create mode 100644 login.js
In this example, Git performs a three-way merge because:
- Both branches (main and feature-login) have diverged from their common ancestor
- Each branch contains changes the other doesn't have
- Git creates a new merge commit to represent the combined changes
Understanding the Merge Algorithm
When Git performs a three-way merge, it follows these steps:
-
Identify the common ancestor: Git finds the most recent commit that both branches share.
-
Compare file states: Git compares each file in the three commits:
- Base version (common ancestor)
- Your current branch version
- The branch being merged in
-
Apply non-conflicting changes automatically:
- If a file was modified in only one branch, Git applies those changes
- If identical changes were made in both branches, Git applies the change once
- If a file was added in one branch but not the other, Git includes the new file
-
Detect conflicts: If the same part of a file was modified differently in both branches, Git marks it as a conflict.
Handling Merge Conflicts
Sometimes Git can't automatically resolve differences during a three-way merge, resulting in a merge conflict. Here's how to handle them:
Example of a Merge Conflict
Let's say both branches modified the same line in a file called utils.js
:
In the main branch:
function formatDate(date) {
return date.toLocaleDateString('en-US');
}
In the feature branch:
function formatDate(date) {
return date.toISOString().split('T')[0];
}
When you try to merge, Git will show:
Auto-merging utils.js
CONFLICT (content): Merge conflict in utils.js
Automatic merge failed; fix conflicts and then commit the result.
The conflicted file will contain markers:
function formatDate(date) {
<<<<<<< HEAD
return date.toLocaleDateString('en-US');
=======
return date.toISOString().split('T')[0];
>>>>>>> feature
}
Resolving Merge Conflicts
Follow these steps to resolve a merge conflict:
-
Open the conflicted files: Look for the conflict markers (
<<<<<<<
,=======
,>>>>>>>
) -
Edit the files to resolve conflicts: Choose one version or create a new combined version
-
Remove the conflict markers: Delete the
<<<<<<<
,=======
, and>>>>>>>
lines -
Add the resolved files:
bashgit add utils.js
-
Complete the merge by committing:
bashgit commit -m "Merge feature branch, resolve date formatting conflict"
Best Practices for Three-Way Merges
To make three-way merges smoother:
-
Merge frequently: Regular merges from the main branch into your feature branch reduce the likelihood of major conflicts.
-
Communicate with your team: If multiple people are working on the same files, coordinate changes.
-
Use meaningful commit messages: Clear commit messages help understand the purpose of changes when resolving conflicts.
-
Test after merging: Always verify that your code works correctly after a merge.
-
Use merge tools: Visual merge tools can make conflict resolution easier. Configure Git to use your preferred tool:
bashgit config --global merge.tool meld
Alternatives to Three-Way Merge
Git offers alternatives to the standard three-way merge:
Rebase
Instead of merging, you can rebase your branch on top of the target branch:
git checkout feature
git rebase main
Rebasing rewrites your commit history by replaying your changes on top of the target branch, resulting in a linear history without merge commits.
Squash Merge
A squash merge combines all changes from your feature branch into a single commit:
git checkout main
git merge --squash feature
git commit -m "Add feature X"
This approach simplifies history but loses the individual commits from the feature branch.
Summary
Git's three-way merge is a powerful mechanism for combining divergent branch histories. It:
- Uses three commits (base, current, and merging) to determine the final state
- Automatically resolves most changes
- Creates a merge commit to represent the combined histories
- Identifies conflicts when it can't automatically determine the correct result
Understanding how three-way merges work helps you manage your Git projects effectively and handle merge conflicts confidently.
Exercises
-
Create two branches that modify different files, then merge them. Note how Git creates a merge commit.
-
Create two branches that modify the same file but different lines, then merge them. Note how Git automatically resolves this.
-
Create two branches that modify the same line in a file, then merge them. Practice resolving the conflict.
-
Try using the
git merge --abort
command to cancel a merge with conflicts. -
Compare a three-way merge with rebasing by experimenting with both approaches on the same branches.
Additional Resources
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)