Git Internals: How Git Merge Really Works
Shrijith Venkatramana

Shrijith Venkatramana @shrsv

About: Founder @ hexmos.com. Building https://hexmos.com/livereview

Joined:
Jan 4, 2023

Git Internals: How Git Merge Really Works

Publish Date: Jul 13
10 0

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
Enter fullscreen mode Exit fullscreen mode

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(+)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Output:

Updating abc123..def456
Fast-forward
 file.txt | 1 +
 1 file changed, 1 insertion(+)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Output:

Merge made by the 'recursive' strategy.
 file.txt | 2 ++
 1 file changed, 2 insertions(+)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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:

  1. Identifies the merge base.
  2. Computes diffs between the merge base and each branch tip.
  3. Applies changes to create a new tree.
  4. 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
Enter fullscreen mode Exit fullscreen mode

Output:

Auto-merging file.txt
CONFLICT (content): Merge conflict in file.txt
Automatic merge failed; fix conflicts and then commit the result.
Enter fullscreen mode Exit fullscreen mode

Git marks conflicts in file.txt:

<<<<<<< HEAD
Main content
=======
Feature content
>>>>>>> feature
Enter fullscreen mode Exit fullscreen mode

You resolve by editing file.txt, then running:

git add file.txt
git commit
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Output (during a conflict):

100644 abc123... 1 file.txt
100644 def456... 2 file.txt
100644 ghi789... 3 file.txt
Enter fullscreen mode Exit fullscreen mode

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:

  1. Open the conflicting file(s).
  2. Edit to combine changes or choose one version.
  3. Mark resolved with git add.
  4. 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Output:

Merge made by the 'recursive' strategy.
 file.txt | 1 +
 1 file changed, 1 insertion(+)
Enter fullscreen mode Exit fullscreen mode

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.

Comments 0 total

    Add comment