Introduction
Git merge conflicts occur when two branches have made changes to the same lines in a file, or when one branch deletes a file that another branch modified, preventing Git from automatically combining the changes. Merge conflicts are a natural part of collaborative development but can become complex when involving binary files, large refactors, long-lived branches, or multiple concurrent contributors. Common causes include editing the same lines in different branches, one branch renaming/moving a file while another modifies it, whitespace and line ending differences across platforms, binary file modifications that cannot be merged textually, cherry-picking commits that conflict with existing changes, rebase operations replaying commits onto conflicting history, and merge strategy selection inappropriate for the situation. The fix requires understanding Git's three-way merge algorithm, properly interpreting conflict markers, selecting appropriate merge strategies, using automation tools like rerere, and implementing team workflows that minimize conflict complexity. This guide provides production-proven techniques for resolving merge conflicts across feature branch workflows, release management, and CI/CD pipelines.
Symptoms
- Git merge command fails with "CONFLICT (content): Merge conflict in filename"
- Git status shows "both modified" for files
- Pull request shows "This branch has conflicts that must be resolved"
- Git rebase stops with "Could not apply <commit>..."
- Conflict markers (<<<<<<, ======, >>>>>>) appear in files
- Git cannot determine which changes to keep
- Binary file shows "cannot merge binary files"
- Large refactor branch conflicts with many feature branches
- CI/CD pipeline fails on merge check
Common Causes
- Same file lines modified in both branches
- File deleted in one branch, modified in another
- File renamed/moved in one branch, modified in another
- Whitespace changes conflicting with content changes
- Line ending differences (CRLF vs LF) across platforms
- Cherry-pick target already has conflicting changes
- Rebase onto branch with divergent history
- Long-lived feature branch drifting from main
- Multiple developers editing same configuration files
- Generated code that changes on each build
Step-by-Step Fix
### 1. Diagnose merge conflicts
Check conflict status:
```bash # View current merge/rebase state git status
# Output during merge conflict: # On branch main # You have unmerged paths. # (fix conflicts and run "git commit") # (use "git merge --abort" to abort the merge) # # Unmerged paths: # (use "git add <file>..." to mark resolution) # both modified: src/app.js # both modified: package.json # deleted in their branch: src/old-feature.js
# Output during rebase conflict: # rebase in progress; onto abc1234 # You are currently rebasing branch 'feature' on 'abc1234'. # (fix conflicts and then run "git rebase --continue") # (use "git rebase --abort" to abort the rebase) # # Changes to be committed: # (use "git restore --staged <file>..." to unstage) # modified: src/config.js # # Unmerged paths: # (use "git restore --staged <file>..." to mark resolution) # both modified: src/app.js
# View conflict markers in file cat src/app.js
# Conflict marker format: # <<<<<<< HEAD (or <<<<<<< branch-name in some configs) # Changes from current branch (the branch you're merging INTO) # ======= # Changes from incoming branch (the branch being merged) # >>>>>>> feature-branch (or commit SHA during rebase)
# Count conflicts git diff --name-only --diff-filter=U | wc -l
# List all conflicted files git diff --name-only --diff-filter=U
# For each conflicted file, show conflict type git diff --diff-filter=U --raw ```
Understand merge base:
```bash # Find the common ancestor (merge base) git merge-base HEAD feature-branch
# Output: abc1234def5678... # This is the commit where the branches diverged
# Visualize the divergence git log --oneline --graph --all --decorate
# See what changed in each branch since divergence MERGE_BASE=$(git merge-base HEAD feature-branch)
echo "=== Changes in HEAD since divergence ===" git log --oneline ${MERGE_BASE}..HEAD
echo "=== Changes in feature-branch since divergence ===" git log --oneline ${MERGE_BASE}..feature-branch
# Compare the actual file differences echo "=== HEAD version of file ===" git show HEAD:src/app.js
echo "=== feature-branch version of file ===" git show feature-branch:src/app.js
echo "=== Base version of file ===" git show ${MERGE_BASE}:src/app.js ```
### 2. Resolve conflict markers manually
Basic conflict resolution:
```bash # Open conflicted file in editor # Most IDEs highlight conflict regions with visual markers
# Example conflict: ```
javascript
// src/app.js
<<<<<<< HEAD
const API_URL = 'https://api.example.com';
const TIMEOUT = 5000;
=======
const API_URL = 'https://api-staging.example.com';
const RETRY_COUNT = 3;
>>>>>>> feature-branch
// Resolved version (choose one, combine, or create new):
``javascript
const API_URL = 'https://api.example.com'; // Keep production URL from HEAD
const TIMEOUT = 5000; // Keep timeout from HEAD
const RETRY_COUNT = 3; // Add new feature from branch
```bash # After editing, remove conflict markers and save
# Mark file as resolved git add src/app.js
# Verify resolution git status # File should now be in "Changes to be committed" section
# Complete the merge git commit -m "Merge feature-branch: resolve API config conflict"
# Or for rebase git rebase --continue ```
Complex multi-section conflicts:
```bash # File may have multiple conflict regions # Resolve each section independently
cat src/config.js
# <<<<<<< HEAD # Section 1 from HEAD # ======= # Section 1 from branch # >>>>>>> branch # # ... some unchanged code ... # # <<<<<<< HEAD # Section 2 from HEAD # ======= # Section 2 from branch # >>>>>>> branch
# Use git mergetool for visual resolution git mergetool
# Common merge tools: # - vimdiff (default on many systems) # - meld (recommended GUI) # - kdiff3 # - p4merge # - beyond compare
# Configure merge tool git config --global merge.tool meld git config --global mergetool.meld.path meld git config --global mergetool.prompt false
# Or for VS Code git config --global merge.tool vscode git config --global mergetool.vscode.path "code" git config --global mergetool.vscode.cmd "code --wait \$MERGED" ```
Using mergetool effectively:
```bash # Configure preferred merge tool git config --global merge.tool meld
# For macOS with Xcode git config --global merge.tool opendiff
# For Windows with Beyond Compare git config --global merge.tool bc git config --global mergetool.bc.path "C:/Program Files/Beyond Compare 4/BCompare.exe"
# Run mergetool for all conflicted files git mergetool --tool=meld
# Run mergetool for specific file git mergetool --tool=meld src/app.js
# Mergetool layout (vimdiff example): # Left: HEAD (current branch / local) # Middle: MERGED (working file to edit) # Right: feature-branch (incoming / remote) # Bottom: BASE (common ancestor, for reference)
# Navigate between conflicts # ]c - next conflict # [c - previous conflict # :wq - save and close
# After resolving all files git add . git commit # For merge # or git rebase --continue # For rebase ```
### 3. Handle file deletion and rename conflicts
Deleted file conflicts:
```bash # Scenario: Branch A deletes file, Branch B modifies it
git status # deleted by them: src/old-feature.js # both modified: src/app.js
# Option 1: Keep the deletion (accept their change) git rm src/old-feature.js
# Option 2: Keep the file (undo deletion) git checkout HEAD -- src/old-feature.js # Or git checkout --ours src/old-feature.js # During merge git checkout --theirs src/old-feature.js # During rebase (inverted)
# Verify and complete git add src/old-feature.js ```
Rename/move conflicts:
```bash # Scenario: Branch A renames file, Branch B modifies it
git status # renamed: src/utils.js -> src/helpers/utils.js # both modified: src/utils.js
# Option 1: Accept the rename and apply modifications # Manually copy changes to the new location mkdir -p src/helpers git show HEAD:src/utils.js > src/helpers/utils.js # Edit src/helpers/utils.js to include changes from other branch git rm src/utils.js git add src/helpers/utils.js
# Option 2: Keep original location git checkout HEAD -- src/utils.js git add src/utils.js
# Option 3: Use both files (if both locations make sense) git checkout HEAD -- src/utils.js git checkout feature-branch:src/utils.js > src/helpers/utils.js git add src/utils.js src/helpers/utils.js ```
### 4. Use merge strategies
Understand merge strategy options:
```bash # Recursive strategy (default for two branches) git merge -s recursive feature-branch
# Options for recursive: git merge -s recursive -X theirs feature-branch # Prefer incoming changes git merge -s recursive -X ours feature-branch # Prefer current branch git merge -s recursive -X patience feature-branch # Patience diff algorithm git merge -s recursive -X histogram feature-branch # Histogram diff algorithm git merge -s recursive -X ignore-space-change feature-branch git merge -s recursive -X ignore-all-space feature-branch
# Use 'theirs' to automatically accept incoming changes git merge -s recursive -X theirs feature-branch
# Use 'ours' to keep current branch (for resolving conflicts your way) git merge -s recursive -X ours feature-branch
# Note: -X ours/-X theirs only affects conflicting lines # Non-conflicting changes from both sides are still merged ```
Octopus strategy (multiple branches):
```bash # Merge multiple branches at once git merge -s octopus feature-1 feature-2 feature-3
# Octopus is default for multiple branches # Automatically resolves non-conflicting merges # Fails if there are conflicts (use recursive instead)
# If octopus fails due to conflicts, use recursive git merge feature-1 feature-2 feature-3 # Git will automatically choose recursive ```
Ours strategy (ignore other branches):
```bash # Create merge commit that ignores all changes from other branch # Useful for superseding a branch or marking it as merged
git merge -s ours feature-branch # Creates merge commit but keeps only HEAD content
# Record that branch is merged without taking changes # History will show the merge, but no code changes
# Common use case: superseding a branch git checkout main git merge -s ours old-feature-branch git branch -d old-feature-branch ```
Subtree merge strategy:
```bash # Merge external repository as subdirectory git remote add library https://github.com/example/library.git git fetch library
# Merge with subtree strategy git merge -s subtree -X subtree=src/library library/main
# Or use git subtree command git subtree add --prefix=src/library library main --squash git subtree pull --prefix=src/library library main --squash ```
### 5. Automate with git rerere
Enable rerere (reuse recorded resolution):
```bash # Enable rerere globally git config --global rerere.enabled true git config --global rerere.autoupdate true
# Or per-repository cd /path/to/repo git config rerere.enabled true
# Rerere automatically records successful conflict resolutions # And reapplies them when same conflict occurs again
# Rerere data is stored in .git/rr-cache/ # Can be shared across team members ```
Use rerere in practice:
```bash # First conflict resolution (manual) git merge feature-branch # Conflict occurs in src/config.js
# Resolve conflict manually # Edit file, resolve markers, save git add src/config.js git commit
# Rerere has now recorded this resolution
# Later, same conflict occurs (cherry-pick, rebase, or different branch) git cherry-pick abc1234 # Conflict in src/config.js
# Rerere automatically applies recorded resolution # Check what rerere resolved git status # Should show file as resolved
# Verify the resolution is correct cat src/config.js
# Continue git add src/config.js git cherry-pick --continue ```
Manage rerere cache:
```bash # View recorded resolutions git rerere status
# Show what's in rerere cache ls .git/rr-cache/
# Each directory is a conflict signature (SHA) # 'preimage' = original conflict # 'postimage' = recorded resolution
# Replay a specific resolution git rerere replay <commit-sha>
# Clear rerere cache git rerere clear
# Export rerere cache for sharing tar czf rerere-cache.tar.gz .git/rr-cache/
# Import rerere cache on another machine tar xzf rerere-cache.tar.gz ```
Share rerere across team:
```bash # Store rerere cache in repository (for team sharing) mkdir -p .git-share/rr-cache git config rerere.cache .git-share/rr-cache
# Or sync rerere cache via CI/CD # .github/workflows/rerere-sync.yml name: Rerere Sync on: push jobs: sync-rerere: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4
- name: Download rerere cache
- run: |
- curl -sL $RERERE_CACHE_URL | tar xzf - -C .git/rr-cache
- name: Enable rerere
- run: git config rerere.enabled true
- name: Rebase to trigger rerere
- run: git rebase origin/main
- name: Upload updated rerere cache
- run: |
- tar czf rerere-cache.tar.gz .git/rr-cache/
- curl -X POST $RERERE_CACHE_URL -T rerere-cache.tar.gz
`
### 6. Handle rebase conflicts
Rebase vs merge conflicts:
```bash # Rebase conflict is similar to merge conflict # But resolution process differs slightly
git rebase main # Output: # Rebasing (3/10) # error: could not apply abc1234... Feature commit # Could not apply abc1234... Feature commit
# View conflict git status git diff
# Resolve conflict # Edit file, remove markers git add src/app.js
# Continue rebase (NOT commit) git rebase --continue
# Abort rebase if needed git rebase --abort
# Skip this commit (if conflict is unresolvable) git rebase --skip ```
Interactive rebase for conflict management:
```bash # Before rebasing, plan the order interactively git rebase -i main
# Editor opens with commit list: # pick abc1234 First feature commit # pick def5678 Second feature commit # pick ghi9012 Third feature commit
# Reorder to minimize conflicts: # - Move refactoring commits before feature commits # - Group related changes together # - Squash small fixup commits
# Squash fixup commits to reduce conflict points # pick abc1234 Main feature # fixup def5678 Fix typo in feature # fixup ghi9012 Add missing import
# Or reorder completely # pick def5678 Second feature commit (now first) # pick abc1234 First feature commit (now second) # pick ghi9012 Third feature commit ```
Resolve rebase with exec:
```bash # Run commands between commits during rebase git rebase -i main
# In editor: # pick abc1234 Commit 1 # exec npm install && npm test # pick def5678 Commit 2 # exec npm install && npm test
# If exec fails, rebase stops for inspection # Fix issues and continue git rebase --continue ```
### 7. Prevent conflicts with workflow
Frequent integration:
```bash # Regularly merge main into feature branch git checkout feature-branch git fetch origin git merge origin/main
# Or rebase feature branch on main git checkout feature-branch git fetch origin git rebase origin/main
# Recommended: Integrate at least daily # More frequent = smaller conflicts = easier resolution ```
Coordinate on shared files:
```bash # Use CODEOWNERS for critical files # .github/CODEOWNERS /src/config.js @tech-lead /package.json @tech-lead /db/schema.sql @dba-team
# CODEOWNERS requires review before merge # Prevents conflicting changes from being merged
# Communicate changes to shared files # Post in team chat when modifying shared config # Use draft PRs to signal work in progress ```
Branch organization:
```bash # Short-lived feature branches (max 1-2 weeks) # Long-lived branches accumulate more conflicts
# Feature flags instead of long branches # Merge to main frequently, hide incomplete work behind flags
# if (featureFlags.newFeature) { # // new implementation # } else { # // existing implementation # }
# Trunk-based development # All developers commit to main frequently # Reduces merge complexity significantly ```
CI/CD conflict detection:
```yaml # GitHub Actions - Check for merge conflicts name: Check Conflicts on: pull_request: types: [opened, synchronize, reopened]
jobs: check-conflicts: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0
- name: Check for conflicts
- run: |
- git fetch origin main:main
- if ! git merge-base --is-ancestor HEAD main; then
- if git merge main 2>&1 | grep -q "CONFLICT"; then
- echo "::error::Pull request has merge conflicts"
- exit 1
- fi
- fi
# Or use marketplace action - uses: eps1lon/actions-label-merge-conflict@v2 with: dirtyLabel: "has-conflicts" repoToken: "${{ secrets.GITHUB_TOKEN }}" commentOnDirty: "This PR has conflicts. Please resolve them." ```
Prevention
- Integrate changes from main into feature branches daily
- Use short-lived feature branches (under 2 weeks)
- Communicate when modifying shared configuration files
- Enable rerere to automate repeated conflict resolutions
- Configure merge tool for visual conflict resolution
- Use CODEOWNERS for critical shared files
- Implement feature flags instead of long-lived branches
- Run conflict checks in CI/CD pipeline
- Train team on merge conflict resolution best practices
- Consider trunk-based development for reducing conflicts
Related Errors
- **Merge conflict**: Standard conflict requiring manual resolution
- **Rebase conflict**: Conflict during commit replay
- **Cherry-pick conflict**: Conflict when applying specific commit
- **Binary file conflict**: Cannot merge binary files automatically
- **File mode changed**: Permission/executable bit conflicts