Merging

Merging: Integrating Divergent Development Lines

Merging combines divergent branches of development into a unified history, integrating changes from multiple sources into a single branch. Understanding Git’s merge mechanics—from simple fast-forwards to complex three-way merges—enables effective collaboration and clean repository management.

Merge or rebase? Read our Merge vs Rebase deep dive to understand when to use each approach and their trade-offs.

Understanding Merge Operations

At its core, a merge operation answers one question: how do we combine the changes from two (or more) branches that have diverged from a common ancestor?

The Fundamental Merge Problem:

# Starting point: Both branches diverge from commit B
      C---D  (feature-branch)
     /
A---B---E---F  (main)

# Goal: Combine changes from both branches
# Result: Create merge commit M
      C---D
     /     \
A---B---E---F---M  (main)

Git’s merge strategies determine how this combination happens, ranging from trivially simple (fast-forward) to algorithmically complex (recursive with criss-cross merges).

Merge Types: From Simple to Complex

Type 1: Already Up-to-Date (Degenerate Case)

The simplest merge scenario occurs when the branch being merged is already fully present in the current branch—no divergence exists.

Scenario:

# main already contains all commits from feature-branch
A---B---C---D  (main)
     \
      C---D  (feature-branch)

# Attempting merge
git checkout main
git merge feature-branch
# Already up-to-date.

Technical Analysis:

  • HEAD of feature-branch is an ancestor of current branch HEAD
  • No new commits introduced
  • Repository state unchanged
  • Index and working tree remain unmodified

Why Git Handles This: Prevents spurious merge commits when no actual integration work occurs. Attempting to merge a branch whose commits are already present would create meaningless merge commits that obscure history without adding value.

Practical Occurrence: Common after pulling from remote when local branch already has all remote changes, or when merging a branch that was previously merged and hasn’t advanced.

Type 2: Fast-Forward (Degenerate Case)

Fast-forward merges occur when your current branch HEAD is a direct ancestor of the branch being merged—a straight-line history exists.

Scenario:

# feature-branch extends main linearly
A---B  (main)
     \
      C---D  (feature-branch)

# Fast-forward merge
git checkout main
git merge feature-branch
# Updating abc123..def456
# Fast-forward

# Result: main moves forward to feature-branch tip
A---B---C---D  (main, feature-branch)

Technical Behavior:

  1. Git verifies current branch HEAD is ancestor of merge target
  2. Moves branch pointer forward to target commit
  3. Updates index to match target commit tree
  4. Updates working tree files
  5. No merge commit created—simply pointer advancement

Command Variations:

# Default: allow fast-forward
git merge feature-branch

# Prevent fast-forward, force merge commit
git merge --no-ff feature-branch
# Creates explicit merge commit even when fast-forward possible

# Only merge if fast-forward possible (abort otherwise)
git merge --ff-only feature-branch

When to Force Merge Commits (--no-ff):

Use Case 1: Feature Integration Documentation

# Document feature completion
git checkout main
git merge --no-ff feature-auth
# Commit message: "Merge feature-auth: OAuth2 integration"

Benefit: Explicit record of feature integration point. git log --first-parent shows only feature completions, not internal feature commits.

Use Case 2: Workflow Policy Enforcement

# Team policy: all features merge to main via pull requests
# GitHub/GitLab require merge commits for audit trail
git merge --no-ff feature-branch

Result: Clear demarcation between feature development and integration.

Fast-Forward in Tracking Branches:

# Local tracking branch behind remote
git fetch origin
# origin/main is ahead of local main

git checkout main
git merge origin/main
# Fast-forward (common pattern)

Rationale: Tracking branches simply record remote state. Local main has no independent commits, so fast-forward naturally applies fetched commits.

Type 3: True Merge (Three-Way Merge)

When both branches have independent commits since their common ancestor, Git performs a three-way merge using the merge base as reference point.

Scenario:

# Divergent branches
      C---D  (feature-branch)
     /
A---B---E---F  (main)

# Common ancestor: B

Three-Way Merge Algorithm:

Git compares three trees to determine final state:

  1. Merge Base: Common ancestor commit (B)
  2. Current Branch: Your branch tip (F)
  3. Other Branch: Merging branch tip (D)

Decision Logic:

BaseCurrentOtherResultReasoning
foofoobarbarOnly other changed → take other
foobarfoobarOnly current changed → take current
foobarbazCONFLICTBoth changed → manual resolution
foobarbarbarBoth changed same way → automatic
foo(deleted)foo(deleted)Only current deleted → delete
foofoo(deleted)(deleted)Only other deleted → delete
(none)foo(none)fooOnly current added → keep
(none)(none)barbarOnly other added → keep
(none)foobarCONFLICTBoth added differently → manual

Successful Three-Way Merge:

git checkout main
git merge feature-branch
# Auto-merging file.py
# Merge made by the 'recursive' strategy.
#  file.py | 5 +++++
#  1 file changed, 5 insertions(+)

# Result: Merge commit M created
      C---D
     /     \
A---B---E---F---M  (main)

Merge Commit Characteristics:

  • Two parents: F (current branch) and D (merged branch)
  • Tree object: Combined state of both branches
  • Commit message: Auto-generated or user-provided

Viewing Merge Commit:

git show HEAD
# commit abc123def456... (merge commit)
# Merge: f1a2b3c d4e5f6g
# Author: Developer <[email protected]>
# Date: Thu Oct 30 10:00:00 2024
#
#     Merge branch 'feature-branch'

Type 4: Octopus Merge (Multiple Branches)

Git supports merging more than two branches simultaneously, creating commits with three or more parents.

Scenario:

# Three feature branches to merge
      C---D  (feature-a)
     /
A---B---E---F  (feature-b)
     \
      G---H  (feature-c)

# Octopus merge
git checkout main
git merge feature-a feature-b feature-c

Technical Behavior:

  • Internally calls recursive merge strategy multiple times
  • Cannot handle conflicts—aborts on first conflict
  • Creates single commit with multiple parents
  • Primarily aesthetic—equivalent to sequential merges

Practical Use:

# Combine approved feature branches
git checkout develop
git merge feature-auth feature-payments feature-notifications

# Result: Single merge commit with 3+ parents
      C---D
     /     \
A---B---E---F---I  (develop)
     \     /
      G---H

Limitations:

# Octopus merge encounters conflict
git merge feature-a feature-b feature-c
# Auto-merging file.py
# CONFLICT (content): Merge conflict in file.py
# Automatic merge failed; fix conflicts and then commit the result.
# Could not merge feature-a with feature-b

Resolution: Merge branches sequentially when conflicts exist:

git merge feature-a
# Resolve conflicts
git add file.py
git commit

git merge feature-b
# Resolve conflicts
git add file.py
git commit

git merge feature-c

Visual Impact:

git log --graph --oneline
# *   Merge branches 'feature-a', 'feature-b', 'feature-c'
# |\|\
# | | * feature-c commits
# | * | feature-b commits
# * | | feature-a commits
# |/ /
# * main commits

When to Use: Primarily for visual appeal in commit graphs. Functionally equivalent to sequential merges, but creates cleaner topology for simultaneous feature integration.

Merge Strategies: How Git Decides

Git employs different algorithms depending on merge complexity. Understanding these strategies explains Git’s behavior during edge cases.

Strategy 1: Resolve (Legacy)

The original Git merge strategy, now superseded by recursive but still available.

Algorithm:

  • Operates only on two branches
  • Locates single common ancestor
  • Performs direct three-way merge
  • Simple but limited

Invocation:

git merge -s resolve feature-branch

Limitation: Handles only single merge base. Fails with criss-cross merges where multiple potential merge bases exist.

Strategy 2: Recursive (Default)

Git’s default strategy since 2005, handling complex merge scenarios including criss-cross merges.

Key Innovation: When multiple merge bases exist, recursively merge them into temporary merge base, then use that for final merge.

Criss-Cross Merge Scenario:

# Multiple integration points create multiple merge bases
      A---B---C  (feature-branch)
     /       /
O---M-------N  (main)
     \     /
      D---E

# Merge bases: M and N (both are common ancestors)

Recursive Strategy Process:

  1. Identify all merge bases (M and N)
  2. Create temporary merge of M and N
  3. Use temporary merge as base for merging C and current main
  4. Discard temporary merge base
  5. Commit final merge result

Why This Works: By merging the merge bases, Git creates a synthetic common ancestor that incorporates changes from both branches, providing better context for conflict resolution.

Automatic Invocation:

# Git automatically selects recursive for two-branch merges
git merge feature-branch
# Merge made by the 'recursive' strategy

Benefits Over Resolve:

  • Handles complex integration histories
  • Fewer false conflicts
  • Better rename detection
  • Default for Linux kernel development (proven at scale)

Strategy 3: Ours (Specialty)

Merge multiple branches but keep only current branch’s content, recording other branches in commit parents.

Use Case: Document that branches were merged without actually incorporating their changes.

Scenario: You manually integrated changes from another branch (perhaps with heavy modifications) and want to mark branches as merged to prevent future merge attempts.

# Manual integration already done
git checkout main
# ... manually copied and modified changes from experimental ...

# Record experimental as merged
git merge -s ours experimental
# Merge made by the 'ours' strategy.

# Result: experimental commits in history, but main content unchanged

Technical Effect:

  • Creates merge commit with multiple parents
  • Current branch tree unchanged
  • Other branches recorded as ancestors
  • Prevents Git from attempting future automatic merges

Common Workflow:

# Experimental feature tested but not wanted exactly
git checkout main
git merge -s ours feature-risky

# Git now thinks feature-risky is merged
# Future merges won't try to reintegrate its commits

Strategy 4: Subtree (Specialty)

Merges a branch into a subdirectory of current branch, automatically determining correct placement.

Use Case: Integrating a separate project as subdirectory without submodules.

# Import library project into lib/ subdirectory
git remote add library-project git://example.com/library.git
git fetch library-project

# Subtree merge
git merge -s subtree library-project/main

# Git automatically places library-project content in appropriate subtree

Automatic Detection: Git analyzes tree structures to determine where the subtree belongs, eliminating need for manual specification.

Performing Merges: Practical Workflows

Basic Merge Workflow

# Ensure clean working tree
git status
# On branch main
# nothing to commit, working tree clean

# Merge feature branch
git merge feature-auth

# Possible outcomes:
# 1. Fast-forward
# 2. Automatic three-way merge
# 3. Merge conflict

Conflict-Free Merge Example

# Merge without conflicts
git checkout main
git merge feature-payments

# Output:
# Auto-merging src/payments.py
# Auto-merging tests/test_payments.py
# Merge made by the 'recursive' strategy.
#  src/payments.py       | 145 ++++++++++++++++++++++++++++++++++++++++
#  tests/test_payments.py | 87 +++++++++++++++++++++++++
#  2 files changed, 232 insertions(+)

# Verify merge
git log --oneline --graph -5
# *   abc123 Merge branch 'feature-payments'
# |\
# | * def456 Add payment processing
# | * ghi789 Add payment tests
# * | jkl012 Update main documentation
# * | mno345 Fix main bug
# |/
# * pqr678 Common ancestor

Merge Commit Messages

Default Merge Message:

git merge feature-branch
# Opens editor with:
# Merge branch 'feature-branch'
#
# # Please enter a commit message to explain why this merge is necessary,
# # especially if it merges an updated upstream into a topic branch.

Custom Message:

# Provide message inline
git merge feature-auth -m "Merge feature-auth: Implement OAuth2 authentication

Integrates OAuth2 authentication system with Google and GitHub providers.
All tests passing. Ready for production deployment.

Closes #234, #256"

Detailed Merge Documentation:

# Open editor for detailed message
git merge feature-database-migration

# Edit message:
Merge feature-database-migration: Database schema v2

Major database restructuring to support multi-tenancy:
- Added tenant_id to all tables
- Created tenant management tables
- Implemented row-level security policies
- Updated all queries for tenant isolation

Performance impact:
- Query performance maintained via proper indexing
- Migration tested on production-sized datasets
- Rollback strategy documented in DB_MIGRATION.md

Breaking changes:
- API endpoints now require X-Tenant-ID header
- Database connection pooling config updated

Team review: @alice @bob
Database review: @database-team

Refs: ARCH-123, ARCH-145

Merge Conflicts: Understanding and Resolution

When Conflicts Occur

Conflicts arise when both branches modified the same lines of the same files in incompatible ways.

Conflict Scenario:

# Feature branch modifies authentication
# File: auth.py, Line 42
return jwt.encode(user.email, secret_key)

# Main branch modifies same line
# File: auth.py, Line 42
return jwt.encode(user.id, secret_key)

# Attempting merge
git merge feature-branch
# Auto-merging auth.py
# CONFLICT (content): Merge conflict in auth.py
# Automatic merge failed; fix conflicts and then commit the result.

Conflict Markers Explained

Git annotates conflicted files with three-way diff markers:

def authenticate(user):
<<<<<<< HEAD
    # Current branch version (main)
    return jwt.encode(user.id, secret_key)
=======
    # Merging branch version (feature-branch)
    return jwt.encode(user.email, secret_key)
>>>>>>> feature-branch

Marker Anatomy:

  • <<<<<<< HEAD: Start of current branch content
  • =======: Separator between versions
  • >>>>>>> feature-branch: End of merging branch content

Three-Way Context (extended markers):

def authenticate(user):
<<<<<<< HEAD (main)
    return jwt.encode(user.id, secret_key)
||||||| merged common ancestors
    return jwt.encode(user.username, secret_key)
=======
    return jwt.encode(user.email, secret_key)
>>>>>>> feature-branch

Middle Section (merged common ancestors): Shows original version from merge base, providing context for changes made by both branches.

Conflict Resolution Process

Step 1: Identify Conflicted Files

git status
# On branch main
# You have unmerged paths.
#   (fix conflicts and run "git commit")
#
# Unmerged paths:
#   (use "git add <file>..." to mark resolution)
#
#	both modified:   auth.py
#	both modified:   config.py

Step 2: Examine Conflicts

# View conflict details
git diff
# Shows combined diff with conflict markers

# Or use specific diff
git diff --ours    # Changes from HEAD
git diff --theirs  # Changes from merging branch
git diff --base    # Changes from merge base

Step 3: Manual Resolution

Open conflicted file and edit to resolve:

# Original conflict
def authenticate(user):
<<<<<<< HEAD
    return jwt.encode(user.id, secret_key)
=======
    return jwt.encode(user.email, secret_key)
>>>>>>> feature-branch

# Resolution: Combine both approaches
def authenticate(user):
    # Use email as primary identifier, include id for backward compat
    payload = {'email': user.email, 'user_id': user.id}
    return jwt.encode(payload, secret_key)

Remove conflict markers—all <<<, ===, >>> lines must be deleted.

Step 4: Stage Resolution

# Mark file as resolved
git add auth.py

# Check status
git status
# On branch main
# All conflicts fixed but you are still merging.
#   (use "git commit" to conclude merge)
#
# Changes to be committed:
#
#	modified:   auth.py

Step 5: Complete Merge

# Commit merge with resolution
git commit

# Editor opens with default message:
# Merge branch 'feature-branch'
#
# Conflicts:
#	auth.py
#
# Add resolution details if needed

Resolution Strategies

Strategy 1: Accept One Side Completely

# Accept current branch version (ours)
git checkout --ours auth.py
git add auth.py

# Accept merging branch version (theirs)
git checkout --theirs config.py
git add config.py

git commit

Use Case: One branch has correct solution, other should be discarded. Common when merging long-lived branches where recent version supersedes older work.

Strategy 2: Use Merge Tool

# Launch configured visual merge tool
git mergetool

# Tool displays three panels:
# - LOCAL (your changes / HEAD)
# - BASE (common ancestor)
# - REMOTE (their changes / merging branch)

# Select chunks visually, save, exit
git commit

Popular Merge Tools:

  • kdiff3: Three-way merge with ancestor context
  • meld: Visual diff and merge tool
  • vimdiff: Text-based three-way merge
  • p4merge: Perforce visual merge tool
  • Beyond Compare: Commercial option

Configuration:

# Set merge tool
git config --global merge.tool kdiff3

# Disable backup files (.orig)
git config --global mergetool.keepBackup false

# Use tool
git mergetool

Strategy 3: Abort and Restart

# Abort merge in progress
git merge --abort

# Working directory restored to pre-merge state
git status
# On branch main
# nothing to commit, working tree clean

# Try different approach (rebase, manual integration, etc.)

Safety Note: git merge --abort only works if merge hasn’t been committed. Once merge commit exists, use git reset to undo (see Undoing Changes).

Advanced Conflict Resolution

Viewing Conflict Diff:

# Combined diff format (shows both sides)
git diff
# diff --cc auth.py
# --- a/auth.py
# +++ b/auth.py
# @@@ -40,4 -40,4 +40,8 @@@ def authenticate(user)
#       if not user:
#           return None
# ++<<<<<<< HEAD
#  +    return jwt.encode(user.id, secret_key)
# ++=======
# +     return jwt.encode(user.email, secret_key)
# ++>>>>>>> feature-branch

Inspecting Merge State:

# Show three versions side by side
git show :1:auth.py  # Base (common ancestor)
git show :2:auth.py  # Ours (current branch)
git show :3:auth.py  # Theirs (merging branch)

# Extract specific version to file
git show :2:auth.py > auth.py.ours
git show :3:auth.py > auth.py.theirs

# Compare and decide
diff auth.py.ours auth.py.theirs

Conflict History Analysis:

# Show commits that modified conflicted file
git log --merge auth.py

# Show changes from both branches
git log --merge --left-right --oneline auth.py
# < abc123 Update auth to use ID
# > def456 Update auth to use email

Rerere (Reuse Recorded Resolution):

Enable automatic conflict resolution replay:

# Enable rerere globally
git config --global rerere.enabled true

# First conflict resolution recorded
git merge feature-branch
# CONFLICT in auth.py
vim auth.py  # Resolve manually
git add auth.py
git commit

# Later, same conflict encountered
git merge another-feature
# Resolved 'auth.py' using previous resolution.
# Auto-merged auth.py

How Rerere Works:

  1. Records conflict state and resolution
  2. Stores in .git/rr-cache/
  3. On future identical conflict, auto-applies recorded resolution
  4. Developer verifies and commits

Common Merge Scenarios

Scenario 1: Update Feature Branch with Latest Main

# Feature branch falls behind main
git checkout feature-auth
git fetch origin

# Check divergence
git log --oneline --graph main..HEAD
# * def456 Feature commit 2
# * abc123 Feature commit 1

git log --oneline --graph HEAD..main
# * pqr789 Main commit 2
# * mno345 Main commit 1

# Merge main into feature branch
git merge main

# Resolve any conflicts
# Continue feature development

Scenario 2: Integrate Completed Feature

# Feature complete, tested, approved
git checkout main
git pull origin main  # Get latest main

# Merge feature with documented integration
git merge --no-ff feature-payments -m "Merge feature-payments: Stripe integration

Complete payment processing implementation:
- Stripe API integration
- Payment webhook handlers
- Refund processing
- Comprehensive test suite

Tested in staging environment.
Ready for production deployment.

Closes #345, #367, #389"

# Push to remote
git push origin main

Scenario 3: Emergency Hotfix Merge

# Critical bug in production
git checkout main
git pull origin main

# Create hotfix branch
git checkout -b hotfix-security-1.2.1 v1.2.0

# Implement fix
vim src/security/auth.py
git add src/security/auth.py
git commit -m "fix(security): patch authentication bypass

CVE-2024-12345 allowed unauthenticated access when
session cookie expired during request.

Fixed by validating session before all requests."

# Merge to main
git checkout main
git merge --no-ff hotfix-security-1.2.1

# Tag release
git tag -a v1.2.1 -m "Security hotfix release"

# Merge to develop
git checkout develop
git merge --no-ff hotfix-security-1.2.1

# Push everything
git push origin main develop v1.2.1

# Clean up
git branch -d hotfix-security-1.2.1

Scenario 4: Pull Request Workflow

# Developer creates feature branch
git checkout -b feature-user-profiles
# ... commits ...
git push origin feature-user-profiles

# Create pull request via GitHub/GitLab UI

# Code review happens
# Reviewer requests changes
git checkout feature-user-profiles
# ... make changes ...
git add .
git commit -m "address review feedback"
git push origin feature-user-profiles

# Approval received
# Maintainer merges via UI (creates merge commit on main)

# Developer updates local repository
git checkout main
git pull origin main
git branch -d feature-user-profiles
git push origin --delete feature-user-profiles

Merge vs. Rebase: When to Use Each

This is covered extensively in Merge vs. Rebase, but here’s a quick decision matrix:

SituationUse MergeUse Rebase
Integrating into main/develop
Shared feature branch
Personal feature branch update⚠️
Preserving feature development context
Creating linear history
Already pushed commits

Golden Rule: Never rebase commits that have been pushed to shared branches. Merge is safe for all collaboration scenarios.

Summary: Merge Operations as Integration Workflow

Core Principles:

  1. Fast-forward when possible: Clean linear history for tracking branches
  2. Force merge commits for features: Document integration points with --no-ff
  3. Resolve conflicts thoughtfully: Consider context from both branches
  4. Test after merging: Merge commits can introduce integration bugs
  5. Document complex merges: Detailed commit messages explain resolution choices

Merge Workflow Checklist:

# 1. Prepare
git status  # Clean working tree
git checkout main
git pull origin main  # Latest main

# 2. Merge
git merge feature-branch

# 3. If conflicts
vim conflicted-file.py  # Resolve
git add conflicted-file.py
git commit

# 4. Verify
git log --oneline --graph -5
npm test  # Run test suite

# 5. Push
git push origin main

Integration Philosophy: Merges are about combining work from multiple developers while preserving the context of when and why integration occurred. Unlike rebasing, merging maintains historical truth—commits happened in parallel, and merge commits document their convergence.


Merge Configuration for Better Workflows

Configure Git’s merge behavior to suit your workflow and improve conflict resolution:

Essential Merge Settings

# Better conflict markers (3-way diff)
git config --global merge.conflictstyle diff3

# This shows three versions during conflicts:
# <<<<<<< HEAD (yours)
# ||||||| base (common ancestor)
# ======= theirs
# >>>>>>> feature-branch

Why diff3? Seeing the common ancestor helps you understand what both sides were trying to change, making conflict resolution significantly easier.

Merge Strategy Configuration

# Always create merge commits (no fast-forward by default)
git config --global merge.ff false

# This preserves feature branch structure in history
# Compare: linear vs. branched history

When to use: Set this globally if you want to always see when features were integrated, even for simple ahead branches.

Merge Tool Configuration

# Set default merge tool
git config --global merge.tool vimdiff  # or: kdiff3, meld, vscode

# Don't keep .orig backup files
git config --global mergetool.keepBackup false

# Don't prompt before each file
git config --global mergetool.prompt false

# Example for VS Code
git config --global merge.tool vscode
git config --global mergetool.vscode.cmd 'code --wait --merge $REMOTE $LOCAL $BASE $MERGED'

Per-Repository Merge Settings

For project-specific requirements:

# In specific repository
cd ~/my-project

# Different default branch merge strategy
git config merge.ff only  # Fast-forward only, fail if not possible

# Or per-branch settings
git config branch.main.mergeoptions "--no-ff"
git config branch.develop.mergeoptions "--ff-only"

Recommended Configuration for Teams

# Comprehensive team setup
git config --global merge.conflictstyle diff3
git config --global merge.tool vimdiff
git config --global mergetool.keepBackup false
git config --global merge.ff false  # If team prefers explicit merge commits

# Verify current settings
git config --global --get-regexp merge

Next Steps: Explore Merge vs. Rebase for strategic guidance on integration approaches, and Undoing Changes for handling merge mistakes.