Hi there! I'm Shrijith Venkatrama, founder of Hexmos. Right now, I’m building LiveAPI, a first of its kind tool for helping you automatically index API endpoints across all your repositories. LiveAPI helps you discover, understand and use APIs in large tech infrastructures with ease.
Git is the backbone of modern version control, and while most developers use commands like git merge
daily, the magic behind it often stays hidden. Understanding how Git handles merges under the hood can make you a better developer—whether you're debugging a tricky conflict or optimizing your workflow. This post dives into the internals of Git merge, breaking down its mechanics with clear examples and practical insights. Let’s peel back the curtain and see what’s going on.
What Happens When You Run git merge
?
When you type git merge branch-name
, Git combines the histories of two branches into one. It’s not just slapping code together—Git uses a structured process to decide how changes are integrated. The key players here are commit objects, trees, and the index. Merging involves comparing snapshots, finding common ancestors, and applying changes.
There are two main types of merges:
- Fast-forward merge: If the target branch hasn’t diverged, Git moves the branch pointer forward.
- Three-way merge: If branches have diverged, Git creates a new merge commit using a common ancestor.
Here’s a quick example to set the stage:
# Create a repo and initial commit
git init
echo "Initial content" > file.txt
git add file.txt
git commit -m "Initial commit"
# Create a branch, make changes
git checkout -b feature
echo "Feature change" >> file.txt
git commit -a -m "Add feature"
# Switch back, make a different change
git checkout main
echo "Main change" >> file.txt
git commit -a -m "Update main"
# Merge the feature branch
git merge feature
Output: Git detects divergence and performs a three-way merge, creating a merge commit. You’ll see something like:
Merge made by the 'recursive' strategy.
file.txt | 2 ++
1 file changed, 2 insertions(+)
This sets up the foundation. Let’s dive deeper into how Git makes this happen.
The Role of Commit Objects in Merging
Every commit in Git is a snapshot of your project, stored as a commit object. These objects contain:
- A reference to a tree object (the project’s file structure).
- Parent commit(s).
- Metadata like author and message.
During a merge, Git uses commit objects to find the merge base—the common ancestor of the branches being merged. You can see this with:
git merge-base main feature
Output: A commit hash, like abc123...
, representing the merge base.
For our earlier example, Git identifies the initial commit as the merge base, then compares it with the tips of main
and feature
. This comparison drives the merge process. If you’re curious about commit objects, run git cat-file -p <commit-hash>
to inspect one.
Learn more about commit objects.
Fast-Forward vs. Three-Way Merge: What’s the Difference?
Git chooses between a fast-forward or three-way merge based on branch history. Here’s a breakdown:
Merge Type | When It Happens | Outcome |
---|---|---|
Fast-Forward | Target branch has no unique commits | Moves branch pointer, no new commit |
Three-Way Merge | Both branches have unique commits | Creates a new merge commit |
Fast-Forward Example
# Starting point: main is ahead, feature is behind
git checkout -b feature
echo "Feature work" >> file.txt
git commit -a -m "Feature work"
# Fast-forward merge
git checkout main
git merge feature
Output:
Updating abc123..def456
Fast-forward
file.txt | 1 +
1 file changed, 1 insertion(+)
Three-Way Merge Example
# Divergent branches
git checkout main
echo "Main update" >> file.txt
git commit -a -m "Main update"
git checkout feature
echo "Feature update" >> file.txt
git commit -a -m "Feature update"
git checkout main
git merge feature
Output:
Merge made by the 'recursive' strategy.
file.txt | 2 ++
1 file changed, 2 insertions(+)
Fast-forward is simpler but only works when the target branch hasn’t moved forward. Three-way merges handle divergence by combining changes, which we’ll explore next.
How Git Finds the Merge Base
The merge base is the commit where two branches diverged. Git uses the Lowest Common Ancestor (LCA) algorithm to find it. This is critical for three-way merges, as Git compares the merge base with both branch tips to compute differences.
You can find the merge base manually:
git merge-base main feature
Output: A commit hash, e.g., abc123...
.
To visualize:
-
Main branch:
A -> B -> C
-
Feature branch:
A -> D -> E
-
Merge base:
A
Git then diffs:
- Merge base (
A
) vs.main
(C
). - Merge base (
A
) vs.feature
(E
).
These diffs guide how changes are combined. If you want to see this in action, use git log --graph --oneline
to visualize branch history.
Resolving Changes with the Merge Algorithm
Git’s recursive merge strategy is the default for three-way merges. It:
- Identifies the merge base.
- Computes diffs between the merge base and each branch tip.
- Applies changes to create a new tree.
- Creates a merge commit with two parents.
Here’s an example with a conflict:
# On main
git checkout main
echo "Main content" > file.txt
git commit -a -m "Main change"
# On feature
git checkout feature
echo "Feature content" > file.txt
git commit -a -m "Feature change"
# Merge with conflict
git checkout main
git merge feature
Output:
Auto-merging file.txt
CONFLICT (content): Merge conflict in file.txt
Automatic merge failed; fix conflicts and then commit the result.
Git marks conflicts in file.txt
:
<<<<<<< HEAD
Main content
=======
Feature content
>>>>>>> feature
You resolve by editing file.txt
, then running:
git add file.txt
git commit
The recursive strategy handles most cases, but for complex histories, you might explore strategies like ours
or theirs
(e.g., git merge -s ours
).
Git merge strategies documentation.
The Index and Working Tree During Merges
The index (staging area) is where Git stages changes during a merge. When you run git merge
, Git updates:
- The working tree (your files).
- The index to reflect the merged state.
If conflicts arise, the index holds conflict markers, and you must resolve them before committing. You can inspect the index with:
git ls-files --stage
Output (during a conflict):
100644 abc123... 1 file.txt
100644 def456... 2 file.txt
100644 ghi789... 3 file.txt
Here, stages 1
, 2
, and 3
represent the merge base, main
, and feature
versions of file.txt
. Resolving conflicts updates the index, which you finalize with git commit
.
Handling Merge Conflicts Like a Pro
Conflicts happen when Git can’t automatically reconcile changes. Common cases:
- Same line edited differently in both branches.
- File deleted in one branch, modified in another.
To resolve:
- Open the conflicting file(s).
- Edit to combine changes or choose one version.
- Mark resolved with
git add
. - Complete the merge with
git commit
.
Example resolution:
# file.txt with conflict
<<<<<<< HEAD
Main content
=======
Feature content
>>>>>>> feature
# Edit to resolve
echo "Combined content" > file.txt
git add file.txt
git commit
Pro tip: Use git mergetool
for a GUI to resolve conflicts faster. Tools like vimdiff
or meld
can simplify the process.
Practical Tips for Smarter Merges
Here are some actionable tips to make merges smoother:
- Keep commits small: Smaller changes reduce conflict risk.
-
Rebase before merging: Use
git rebase
to linearize history, avoiding unnecessary merge commits. -
Use
--no-ff
: Force a merge commit even for fast-forward cases (git merge --no-ff feature
). -
Check history: Run
git log --graph --oneline
to understand branch relationships before merging.
Example of a non-fast-forward merge:
git checkout main
git merge --no-ff feature
Output:
Merge made by the 'recursive' strategy.
file.txt | 1 +
1 file changed, 1 insertion(+)
This creates a merge commit even if a fast-forward was possible, preserving branch history.
What’s Next: Leveraging Git Merge Knowledge
Understanding Git’s merge internals empowers you to handle complex workflows with confidence. You can debug conflicts faster, choose the right merge strategy, and optimize your branch structure. Try experimenting with git merge-base
or git log --graph
in your next project to see the mechanics in action. If you’re working on a team, share this knowledge to streamline collaboration—fewer merge headaches mean more time coding.