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:
- Git verifies current branch HEAD is ancestor of merge target
- Moves branch pointer forward to target commit
- Updates index to match target commit tree
- Updates working tree files
- 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-branchWhen 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-branchResult: 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: BThree-Way Merge Algorithm:
Git compares three trees to determine final state:
- Merge Base: Common ancestor commit (B)
- Current Branch: Your branch tip (F)
- Other Branch: Merging branch tip (D)
Decision Logic:
| Base | Current | Other | Result | Reasoning |
|---|---|---|---|---|
| foo | foo | bar | bar | Only other changed → take other |
| foo | bar | foo | bar | Only current changed → take current |
| foo | bar | baz | CONFLICT | Both changed → manual resolution |
| foo | bar | bar | bar | Both changed same way → automatic |
| foo | (deleted) | foo | (deleted) | Only current deleted → delete |
| foo | foo | (deleted) | (deleted) | Only other deleted → delete |
| (none) | foo | (none) | foo | Only current added → keep |
| (none) | (none) | bar | bar | Only other added → keep |
| (none) | foo | bar | CONFLICT | Both 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-cTechnical 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---HLimitations:
# 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-bResolution: 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-cVisual Impact:
git log --graph --oneline
# * Merge branches 'feature-a', 'feature-b', 'feature-c'
# |\|\
# | | * feature-c commits
# | * | feature-b commits
# * | | feature-a commits
# |/ /
# * main commitsWhen 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-branchLimitation: 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:
- Identify all merge bases (M and N)
- Create temporary merge of M and N
- Use temporary merge as base for merging C and current main
- Discard temporary merge base
- 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' strategyBenefits 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 unchangedTechnical 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 commitsStrategy 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 subtreeAutomatic 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 conflictConflict-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 ancestorMerge 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-145Merge 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-branchMarker 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-branchMiddle 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.pyStep 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 baseStep 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.pyStep 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 neededResolution 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 commitUse 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 commitPopular 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 mergetoolStrategy 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-branchInspecting 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.theirsConflict 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 emailRerere (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.pyHow Rerere Works:
- Records conflict state and resolution
- Stores in
.git/rr-cache/ - On future identical conflict, auto-applies recorded resolution
- 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 developmentScenario 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 mainScenario 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.1Scenario 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-profilesMerge vs. Rebase: When to Use Each
This is covered extensively in Merge vs. Rebase, but here’s a quick decision matrix:
| Situation | Use Merge | Use 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:
- Fast-forward when possible: Clean linear history for tracking branches
- Force merge commits for features: Document integration points with
--no-ff - Resolve conflicts thoughtfully: Consider context from both branches
- Test after merging: Merge commits can introduce integration bugs
- 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 mainIntegration 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-branchWhy 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 historyWhen 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 mergeNext Steps: Explore Merge vs. Rebase for strategic guidance on integration approaches, and Undoing Changes for handling merge mistakes.