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
  • **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