Git — Conflict Resolution
Merge conflicts are an inevitable part of collaborative development. They occur when Git cannot automatically reconcile changes from two branches that modified the same part of a file in different ways.
While conflicts can seem intimidating at first, understanding how they work and having a systematic resolution strategy makes them straightforward to handle. This guide covers everything from conflict markers to advanced tools like git rerere.
Git is remarkably good at merging. It can automatically merge changes to different parts of a file, renames, and even formatting changes. But three scenarios always cause conflicts:
Same Line, Different Changes
Two branches modify the exact same line of a file in different ways. Git does not know which version to keep.
One Deletes, One Modifies
One branch deletes a section that another branch modifies. Git cannot decide whether to keep the deletion or the modification.
Adjacent Lines Changed
Changes on adjacent lines in different branches can sometimes cause ambiguity, especially with function boundaries or import statements.
When a conflict occurs, Git inserts conflict markers into the affected file. Understanding these markers is the first step to resolving any conflict.
| 1 | # Conflict marker structure in a file: |
| 2 | <<<<<<< HEAD |
| 3 | const MAX_RETRIES = 3; |
| 4 | const TIMEOUT = 5000; |
| 5 | ======= |
| 6 | const MAX_RETRIES = 5; |
| 7 | const TIMEOUT = 10000; |
| 8 | const BACKOFF = 1.5; |
| 9 | >>>>>>> feature/config |
| 10 | |
| 11 | # Marker breakdown: |
| 12 | # <<<<<<< HEAD -- Start of "our" changes (current branch) |
| 13 | # ======= -- Separator between "ours" and "theirs" |
| 14 | # >>>>>>> feature/config -- End of "their" changes (incoming branch) |
| 15 | |
| 16 | # For rebase conflicts (opposite semantics): |
| 17 | # <<<<<<< HEAD -- The changes being replayed |
| 18 | # ======= -- Separator |
| 19 | # >>>>>>> main -- The base branch changes |
info
When Git detects a conflict during a merge or rebase, it reports which files have conflicts and pauses the operation:
| 1 | # Merge conflict output |
| 2 | git merge feature/config |
| 3 | # Auto-merging src/config.js |
| 4 | # CONFLICT (content): Merge conflict in src/config.js |
| 5 | # Automatic merge failed; fix conflicts and then commit. |
| 6 | |
| 7 | # Check which files have conflicts |
| 8 | git status |
| 9 | # On branch main |
| 10 | # You have unmerged paths. |
| 11 | # (fix conflicts and run "git commit") |
| 12 | # (use "git merge --abort" to abort the merge) |
| 13 | # |
| 14 | # Unmerged paths: |
| 15 | # both modified: src/config.js |
| 16 | |
| 17 | # View the conflict around problematic regions |
| 18 | git diff |
| 19 | # Shows the conflict markers and surrounding context |
The most fundamental way to resolve conflicts is to edit the file directly. Here is a systematic approach:
| 1 | # Step 1: Open the conflicting file in your editor |
| 2 | # Look for <<<<<<<, =======, >>>>>>> markers |
| 3 | |
| 4 | # Step 2: Edit the file to resolve each conflict |
| 5 | # Before: |
| 6 | <<<<<<< HEAD |
| 7 | const MAX_RETRIES = 3; |
| 8 | ======= |
| 9 | const MAX_RETRIES = 5; |
| 10 | const BACKOFF = 1.5; |
| 11 | >>>>>>> feature/config |
| 12 | |
| 13 | # After resolving (keep both, with our retry count but add backoff): |
| 14 | const MAX_RETRIES = 3; |
| 15 | const BACKOFF = 1.5; |
| 16 | |
| 17 | # Step 3: Remove all conflict markers, including <<<<<<<, =======, >>>>>>> |
| 18 | |
| 19 | # Step 4: Stage the resolved file |
| 20 | git add src/config.js |
| 21 | |
| 22 | # Step 5: Continue the operation |
| 23 | git commit # for merge |
| 24 | # OR |
| 25 | git rebase --continue # for rebase |
| 26 | |
| 27 | # Check if all conflicts are resolved |
| 28 | git diff --check |
pro tip
git mergetool launches a visual merge tool that displays the three-way diff: the common ancestor, the current branch (local), the incoming branch (remote), and the merged result.
| 1 | # Configure your preferred merge tool |
| 2 | git config --global merge.tool vscode |
| 3 | git config --global mergetool.vscode.cmd "code --wait $MERGED" |
| 4 | |
| 5 | # Common merge tools: |
| 6 | # - vscode : Visual Studio Code |
| 7 | # - meld : Visual diff/merge tool |
| 8 | # - kdiff3 : KDiff3 (cross-platform) |
| 9 | # - p4merge : Perforce Merge |
| 10 | # - beyondcompare: Beyond Compare |
| 11 | |
| 12 | # Launch the merge tool for conflicting files |
| 13 | git mergetool |
| 14 | |
| 15 | # Launch for a specific file |
| 16 | git mergetool src/config.js |
| 17 | |
| 18 | # Don't prompt before launching |
| 19 | git mergetool --no-prompt |
| 20 | |
| 21 | # Keep backup .orig files (or disable them) |
| 22 | git config --global mergetool.keepBackup false |
| 23 | |
| 24 | # After resolving in the tool, save and close |
| 25 | # Git automatically stages the resolved file |
When git mergetool launches, it typically shows three panes:
| Pane | Content | Label |
|---|---|---|
| Left | Your current branch's version | LOCAL |
| Center | The merged result (editable) | MERGED |
| Right | The incoming branch's version | REMOTE |
| Bottom (sometimes) | The common ancestor | BASE |
git rerere ("reuse recorded resolution") remembers how you resolved a conflict and automatically re-applies that resolution the next time the same conflict occurs. This is invaluable for long-lived branches that are rebased frequently.
| 1 | # Enable rerere globally |
| 2 | git config --global rerere.enabled true |
| 3 | |
| 4 | # Or enable per-repository |
| 5 | git config rerere.enabled true |
| 6 | |
| 7 | # With rerere enabled, when you resolve a conflict: |
| 8 | # 1. Git records the pre-image (conflicting state) |
| 9 | # 2. You resolve the conflict |
| 10 | # 3. Git records the post-image (resolution) |
| 11 | # 4. Next time the same conflict occurs -> auto-resolved |
| 12 | |
| 13 | # Check what rerere has recorded |
| 14 | git rerere status |
| 15 | |
| 16 | # Show the recorded resolutions |
| 17 | git rerere diff |
| 18 | |
| 19 | # Clear rerere cache for current conflict |
| 20 | git rerere clear |
| 21 | |
| 22 | # Forget a specific resolution |
| 23 | git rerere forget src/config.js |
| 24 | |
| 25 | # List all recorded resolutions |
| 26 | ls -la .git/rr-cache/ |
| 27 | |
| 28 | # Rerere is especially useful with: |
| 29 | # - Long-lived feature branches |
| 30 | # - Rebasing feature branches onto main |
| 31 | # - Cherry-picking between branches |
best practice
Sometimes a merge or rebase goes wrong and you need to start over. Git provides clean abort mechanisms for both operations.
| 1 | # Abort a merge (returns to pre-merge state) |
| 2 | git merge --abort |
| 3 | |
| 4 | # Abort a rebase (returns to pre-rebase state) |
| 5 | git rebase --abort |
| 6 | |
| 7 | # Abort a cherry-pick |
| 8 | git cherry-pick --abort |
| 9 | |
| 10 | # Start over with a clean merge strategy: |
| 11 | # If you know a merge will be complex, try: |
| 12 | git merge --no-commit feature |
| 13 | # This stages the merge but doesn't commit it, |
| 14 | # allowing you to inspect and fix issues before committing. |
| 15 | |
| 16 | # Use --strategy-option to prefer one side: |
| 17 | # Keep our version on conflicting files |
| 18 | git merge -X ours feature |
| 19 | |
| 20 | # Keep their version on conflicting files |
| 21 | git merge -X theirs feature |
| 22 | # WARNING: These discard the other side's changes silently! |
| 23 | |
| 24 | # For rebase, use --strategy-option: |
| 25 | git rebase -X theirs main # prefer rebased branch on conflict |
| 26 | git rebase -X ours main # prefer main on conflict |
warning
Small, Frequent Merges
Merge or rebase with main daily. The smaller the divergence, the fewer conflicts. A feature branch that diverges for weeks is guaranteed to have painful conflicts.
Communicate About Shared Code
When working on files likely to conflict (shared configs, core modules, interfaces), coordinate with teammates. Clear communication prevents two people from making conflicting changes simultaneously.
Refactor Before Not During
If a major refactor is needed, do it in a dedicated branch, merge it to main first, then rebase feature branches. This avoids the nightmare of merging a refactor with ongoing feature work.
Use File Ownership Conventions
In larger teams, establish clear ownership of specific files or modules. When people know who owns what, accidental concurrent edits to the same file are reduced.
Automated Formatting
Use Prettier, ESLint, or Black to enforce consistent formatting. Formatting conflicts (tabs vs spaces, trailing commas) generate spurious conflicts that automated tools eliminate.
Follow this checklist every time you encounter a merge conflict:
- Understand the conflict: identify which lines conflict and why
- Communicate: if the conflict involves a teammate's work, discuss the resolution
- Edit the file: remove markers and keep the desired code
- Test: ensure the resolved code compiles and passes tests
- Stage: git add <file>
- Continue: git commit (merge) or git rebase --continue
- Don't force: if stuck, use git merge --abort or git rebase --abort
Enable rerere Globally
git config --global rerere.enabled true. It has no performance cost and can save hours when rebasing long-lived branches.
Commit Often, Merge Often
Small commits and frequent merges mean fewer conflicts per merge and simpler resolution when conflicts do occur.
Use Visual Merge Tools
Configure a merge tool like VS Code, Meld, or KDiff3. The three-pane view makes it much easier to understand and resolve complex conflicts.
Don't Force Push After Merge
If you resolved conflicts during a merge, never force push the result. Use a normal push and let your team pull the merge commit normally.