Mid-level 7 min · March 06, 2026

Git Merge Conflict — --theirs Wiped a Security Header

A --theirs merge silently dropped a security header, exposing an API.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • Git merge conflicts happen when two branches edit the same lines — Git stops to ask for human input
  • Conflict markers show your version (HEAD), the incoming version, and (with diff3) the common ancestor
  • Three resolution methods: manual edit, three-way merge tool, or --ours/--theirs for whole files
  • Using diff3 conflict style reduces resolution errors by ~40% by revealing the original base
  • Most production incidents come from blind --theirs that silently discard critical environment configs
  • Prevention: daily sync with main keeps conflicts small and rare
Plain-English First

Imagine two chefs are both improving the same recipe book at the same time — one changes the pasta sauce on page 12, and the other also rewrites that exact same page. When they try to combine their books into one final version, nobody knows which sauce to keep. Git hits the same wall: two developers edited the same line of code, and Git genuinely cannot decide whose version wins. A merge conflict is just Git raising its hand and saying 'I need a human to sort this out.'

Every team that uses Git will eventually hit a merge conflict. It is not a sign something went wrong — it is a sign your team is working in parallel, which is exactly what Git is built for. The problem is that most developers treat conflicts like a fire alarm: panic, smash the keyboard, accept every incoming change, and hope for the best. That approach silently breaks production code and ruins trust in your codebase.

Merge conflicts exist because Git tracks changes at the line level. When two branches modify the same line (or adjacent lines) independently, Git cannot apply both changes automatically without risking data loss. So it stops, marks the battlefield inside the file, and waits for you. The conflict markers Git leaves behind are not cryptic error messages — they are a structured diff you can read and act on deliberately.

By the end of this article you will be able to: read and interpret conflict markers without guessing, manually resolve conflicts with confidence, use a three-way merge tool to handle complex conflicts visually, abort or retry a merge cleanly when things go sideways, and adopt the team habits that prevent unnecessary conflicts in the first place.

Why Git Merge Conflicts Happen — and When to Expect Them

Git merges work beautifully most of the time because the changes on two branches touch different files or different sections of the same file. Git's merge algorithm is smart enough to combine those non-overlapping edits automatically. Conflicts only surface when the algorithm genuinely cannot make a safe decision.

There are three common triggers. First, two developers edit the exact same line in the same file on different branches. Second, one developer edits a block of code while another deletes that entire file. Third, two developers rename the same file to different names. Each of these cases forces Git to stop and defer to a human.

The branch strategy your team uses matters enormously here. Long-lived feature branches that drift far from main are conflict factories — by the time you merge, dozens of lines may have diverged. Short-lived branches merged frequently are the antidote. Knowing this changes how you plan your work, not just how you fix conflicts after they happen.

Conflicts during a rebase feel slightly different from conflicts during a merge because a rebase replays commits one at a time, so you might resolve the same conceptual conflict multiple times. Understanding which operation triggered the conflict changes how you resolve it.

trigger_merge_conflict.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#!/usr/bin/env bash
# -----------------------------------------------------------
# Demonstration: deliberately create a merge conflict so you
# can see exactly what Git reports and what it writes into
# the conflicted file.
# -----------------------------------------------------------

# 1. Create a fresh repo so this is fully self-contained
mkdir recipe-api && cd recipe-api
git init
git config user.email "demo@thecodeforge.io"
git config user.name "CodeForge Demo"

# 2. Add a shared starting file on main (the "common ancestor")
cat > sauce.py << 'EOF'
def get_sauce():
    return "tomato"
EOF

git add sauce.py
git commit -m "chore: initial sauce implementation"

# 3. Branch A: the backend dev changes the sauce to 'arrabiata'
git checkout -b feature/arrabiata-sauce
sed -i 's/tomato/arrabiata/' sauce.py   # edit the same line
git add sauce.py
git commit -m "feat: switch sauce to arrabiata"

# 4. Back on main: a second dev changes the sauce to 'marinara'
git checkout main
sed -i 's/tomato/marinara/' sauce.py   # edit the SAME line differently
git add sauce.py
git commit -m "feat: switch sauce to marinara"

# 5. Attempt to merge — this is where Git raises its hand
git merge feature/arrabiata-sauce
Output
Auto-merging sauce.py
CONFLICT (content): Merge conflict in sauce.py
Automatic merge failed; fix conflicts and then commit the result.
Why Git Stops Instead of Guessing
Git could pick a winner automatically — but if it chose wrong, you would have a silent bug with no trace in history. Stopping and flagging the conflict is the safer, auditable choice. The conflict IS the safety net.
Production Insight
Long-lived branches are the #1 conflict factory in every team I've consulted with.
If your feature branch lives more than 2 days, expect at least one conflict on merge.
Rule: merge or rebase from main every day — it's cheaper than a post-merge bug hunt.
Key Takeaway
Conflicts are not failures — they are Git's way of asking for a human decision.
The three triggers are: same line, one edits one deletes, conflicting renames.
Prevention beats resolution: short branches + daily syncs = minimal conflicts.

Reading Conflict Markers — Decoding What Git Actually Wrote in Your File

After a conflict, Git edits the file directly and inserts markers to show you both sides of the disagreement. Most developers scan these markers, pick a side, and move on. That works for trivial conflicts. For anything real, you need to read all three pieces of information Git gives you.

The structure is always the same. The <<<<<<< HEAD line opens the block and the content below it is what YOUR current branch has. The ======= line is the divider. Everything below the divider down to >>>>>>> feature/branch-name is what the incoming branch contributed. Between those three markers is the full picture of the disagreement.

What most guides skip is the base — the version that BOTH branches started from before they diverged. Git stores this internally. Knowing the base tells you whether both developers added new logic (in which case you likely need to keep both), or whether they both rewrote the same existing logic (in which case you need to pick the right one). Without the base, you are reading a debate without knowing what both parties agreed on before it started.

You can surface the base yourself with git diff --diff-filter=U or by configuring Git to use the diff3 conflict style, which embeds the ancestor version directly between the markers. This single setting prevents enormous amounts of bad conflict resolution.

read_conflict_markers.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#!/usr/bin/env bash
# -----------------------------------------------------------
# Part 1: Enable diff3 style BEFORE you merge — this adds the
# common ancestor (base) into the conflict block so you have
# full context. Set it globally so every future conflict is clearer.
# -----------------------------------------------------------

git config --global merge.conflictStyle diff3

# -----------------------------------------------------------
# Part 2: After running the conflict from the previous section,
# cat the file to see all three layers Git embedded.
# -----------------------------------------------------------

cat sauce.py

# -----------------------------------------------------------
# Part 3: The output below shows what diff3 style looks like.
# Read it from top to bottom:
#
#   <<<<<<< HEAD            <- your branch starts here
#     return "marinara"     <- what YOUR branch says
#   ||||||| merged common ancestors  <- BASE (what BOTH branches started from)
#     return "tomato"       <- the original value before anyone changed it
#   =======                 <- incoming branch starts here
#     return "arrabiata"    <- what the INCOMING branch says
#   >>>>>>> feature/arrabiata-sauce
#
# Decision logic:
#   - Both changed "tomato" to something different => editorial conflict, pick one
#   - One added new lines, other didn't change them => keep both
#   - Base is already gone on both sides => both deleted, no conflict needed
# -----------------------------------------------------------

# Part 4: List ALL conflicted files in a repo (useful in large merges)
git diff --name-only --diff-filter=U

# Part 5: See a detailed diff of just the conflicted regions
git diff
Output
# Output of: cat sauce.py
def get_sauce():
<<<<<<< HEAD
return "marinara"
||||||| merged common ancestors
return "tomato"
=======
return "arrabiata"
>>>>>>> feature/arrabiata-sauce
# Output of: git diff --name-only --diff-filter=U
sauce.py
Set diff3 Today — Not After Your Next Bad Merge
Run git config --global merge.conflictStyle diff3 right now. Without the ancestor block in your conflict markers, you are always making decisions with incomplete information. This one setting has saved countless hours of debugging bad resolutions.
Production Insight
I've seen teams commit the wrong version of a critical math function because they didn't see the base.
Without diff3, you're guessing which change was intentional vs. which was a refactor side effect.
Rule: always know the base before choosing a side — diff3 makes it visible.
Key Takeaway
Conflict markers show three pieces: yours, theirs, and (with diff3) the ancestor.
The ancestor is the key — it tells you what both sides started from.
Set merge.conflictStyle diff3 globally — it's a one-time config that pays forever.

Resolving Conflicts Three Ways — Manual Edit, git mergetool, and Theirs/Ours

There is no single correct way to resolve a conflict — the right approach depends on how complex the conflict is and what the code is doing. You have three practical options and each has a place.

Manual editing works well for simple conflicts: open the file, delete the markers, keep the correct code, save. This forces you to read the code carefully, which is often exactly what you should do. It breaks down when conflicts span dozens of lines or when you cannot tell which version is correct without seeing both in context side-by-side.

A three-way merge tool (like VS Code, IntelliJ, or vimdiff) shows you the base, the current branch, and the incoming branch in three panes simultaneously, with a fourth result pane you build from clicking to accept each hunk. This is the right approach for complex conflicts involving multiple changed functions.

The --ours and --theirs flags are a power tool for a specific situation: when you know categorically that one entire side is correct. They are NOT a shortcut for laziness. Use --ours to keep the current branch's version of a file in full, and --theirs to take the incoming branch's full version. Misusing these flags is one of the most common ways teams silently discard real code changes.

After any resolution method, the workflow is identical: stage the file, verify the state, and commit.

resolve_conflict_three_ways.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#!/usr/bin/env bash
# -----------------------------------------------------------
# METHOD 1: Manual edit
# Open the file in your editor, remove the markers, keep the
# correct content, then stage and commit.
# -----------------------------------------------------------

# Manually edit sauce.py to contain only the correct content:
cat > sauce.py << 'EOF'
def get_sauce():
    # Product decision: use arrabiata for the spicy menu launch
    return "arrabiata"
EOF

# Confirm no conflict markers remain (grep returns nothing = good)
grep -n '<<<<<<<\|=======\|>>>>>>>' sauce.py \
    && echo "WARNING: markers still present" \
    || echo "Clean — no conflict markers found"

# Stage the resolved file
git add sauce.py

# Confirm Git sees it as resolved (no 'UU' entries should remain)
git status

# Commit the merge
git commit -m "fix: resolve sauce conflict — use arrabiata for spicy launch"


# -----------------------------------------------------------
# METHOD 2: Configure VS Code as your merge tool, then launch it
# Run these once to set it up globally:
# -----------------------------------------------------------

git config --global merge.tool vscode
git config --global mergetool.vscode.cmd 'code --wait $MERGED'

# Then during a conflict, run:
# git mergetool
# VS Code opens with the three-way merge editor. Accept hunks
# using the UI buttons. Save and close. Git marks it resolved.


# -----------------------------------------------------------
# METHOD 3: Accept one full side (use carefully and deliberately)
# --ours  = keep the current branch's entire file
# --theirs = take the incoming branch's entire file
# -----------------------------------------------------------

# Scenario: The incoming branch has a regenerated lock file
# (e.g. package-lock.json) and you always want to take the
# incoming version because it was built from the correct
# dependency tree.
git checkout --theirs package-lock.json
git add package-lock.json
# Then commit as normal.

# IMPORTANT: --ours/--theirs operates on WHOLE FILES.
# There is no partial --theirs. If you need to mix lines
# from both sides, you must use method 1 or 2.
Output
# Method 1 output:
Clean — no conflict markers found
On branch main
All conflicts fixed but you are still merging.
(use "git commit" to conclude merge)
Changes to be committed:
modified: sauce.py
[main a3f2c91] fix: resolve sauce conflict — use arrabiata for spicy launch
Watch Out: Staging Without Resolving
Running git add filename stages the file regardless of whether conflict markers are still inside it. Git trusts you. If you forget to remove the <<<<<<< lines, you will commit literal conflict markers into your source code. Always grep for markers before staging on large conflicts: grep -r '<<<<<<<' .
Production Insight
The --theirs flag is the most dangerous command in Git when used on config files.
A senior engineer once used git checkout --theirs . and silently deleted a critical API key.
Rule: never use --theirs on files you haven't read line-by-line at least once.
Key Takeaway
Manual editing gives you full control; merge tools give you visual context.
--ours/--theirs are for whole-file take-overs, not lazy shortcuts.
Always verify by staging and then diffing against HEAD~1 before committing.

Aborting, Retrying, and Preventing Conflicts Before They Happen

Sometimes mid-conflict you realize the merge itself is wrong — the wrong branches were targeted, or you need to pull in new commits before continuing. Git gives you a clean escape hatch: git merge --abort. This rewinds everything back to exactly where you were before you ran the merge. No damage done.

For rebases the equivalent is git rebase --abort. And if you are mid-cherry-pick, git cherry-pick --abort does the same. Always abort cleanly rather than trying to manually reset files — the abort commands guarantee a clean working tree.

Retryin a merge after aborting is as simple as re-running the original merge command, ideally after fetching new changes or after your teammates have confirmed the affected code.

Prevention is worth ten resolutions. The habits that eliminate most conflicts are: merge or rebase from main frequently (at least daily on active branches), keep feature branches short-lived and narrowly scoped, split large files into smaller modules so fewer developers edit the same file, and communicate before refactoring shared utilities. Code ownership boundaries in large teams reduce conflict rates dramatically because they reduce the probability that two people edit the same lines at the same time.

abort_and_prevent_conflicts.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#!/usr/bin/env bash
# -----------------------------------------------------------
# ABORTING: clean escape from a merge gone wrong
# -----------------------------------------------------------

# You are mid-merge and realize the branch targets are wrong
git merge --abort
# Git prints nothing on success — run git status to confirm:
git status
# Expected: "nothing to commit, working tree clean"

# Same pattern for rebase and cherry-pick:
# git rebase --abort
# git cherry-pick --abort


# -----------------------------------------------------------
# PREVENTION PATTERN 1: Sync your feature branch with main daily
# This is the single most effective conflict-prevention habit.
# -----------------------------------------------------------

git checkout feature/payment-refactor

# Option A — Merge main into your branch (preserves history, safer for shared branches)
git fetch origin
git merge origin/main
# If there are conflicts here, they are small because you synced recently.
# Resolve them, then continue your work.

# Option B — Rebase onto main (linear history, better for solo feature branches)
git fetch origin
git rebase origin/main
# Rebase replays YOUR commits on top of the latest main.
# Each commit is replayed individually, so resolve conflicts per-commit if needed.
git rebase --continue   # run this after resolving each commit's conflicts


# -----------------------------------------------------------
# PREVENTION PATTERN 2: Check what a merge WOULD conflict on
# before actually running it (dry-run style inspection)
# -----------------------------------------------------------

# Find files changed on both branches since they diverged:
git fetch origin
MERGE_BASE=$(git merge-base HEAD origin/main)   # find the common ancestor commit
echo "Common ancestor: $MERGE_BASE"

# Files your branch touched:
git diff --name-only "$MERGE_BASE" HEAD

# Files main touched since that ancestor:
git diff --name-only "$MERGE_BASE" origin/main

# The overlap between these two lists = your likely conflict zones
# Review those files with your teammate BEFORE merging
Output
# After git merge --abort:
On branch main
nothing to commit, working tree clean
# After git merge-base:
Common ancestor: 4d8a1f3c2e9b0a7f6d5c4b3a2e1f0d9c8b7a6f5e
# Files your branch touched:
sauce.py
routes/menu.py
# Files main touched:
sauce.py
tests/test_sauce.py
# Overlap: sauce.py — coordinate with teammate before merging!
Pro Tip: The Daily Sync Habit
Set a calendar reminder: every morning before writing new code, run git fetch origin && git rebase origin/main on your feature branch. Teams that do this report conflicts shrinking from multi-hour ordeals to five-minute fixes. The longer you wait, the more divergence accumulates.
Production Insight
I've seen a 3-week feature branch cause 19 separate conflicts on merge day.
The team spent 6 hours resolving them — and introduced 2 bugs that escaped testing.
Rule: merge main into your feature branch daily. It's not optional.
Key Takeaway
Always abort cleanly with git merge --abort — never manually reset.
Prevention: daily sync with main, short branches, and code ownership.
Use git merge-base to predict conflict zones before merging.

Post-Merge Verification — Ensuring the Merge Didn't Break Anything

The merge is committed, but you're not done yet. A surprising number of bugs originate from conflict resolutions that looked right but silently broke logic. You need a verification checklist that goes beyond 'it compiles'.

First, diff the merge commit against its first parent: git diff HEAD~1 — this shows every change that came into your branch because of the merge. Look especially at files that were conflicted. If the diff shows lines you didn't expect, you might have chosen the wrong hunk.

Second, run the test suite. But don't stop at green — check the test coverage on the conflicted files. If those files have no tests, that's a red flag. Add a test that exercises the merged code path.

Third, use git log --first-parent to see the merge commit as a single unit. This is especially important in code review — reviewers should not have to dig through the full merge diff if you've already verified it.

Finally, if the merge touched configuration or infrastructure files, run a validation script that checks for required keys or settings. Automate this in CI so no merge goes to production without passing config validation.

post_merge_verification.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#!/usr/bin/env bash
# -----------------------------------------------------------
# POST-MERGE VERIFICATION CHECKLIST
# Run these steps after every merge, especially complex ones.
# -----------------------------------------------------------

# 1. Show the full diff of what the merge brought in
echo "=== Full merge diff ==="
git diff HEAD~1

# 2. List only the files that were conflicted and resolved
echo "=== Files that had conflicts ==="
git log --oneline --name-only -1 --diff-filter=M | head -20

# 3. Check for accidental marker leftovers in the entire repo
echo "=== Marker scan ==="
grep -r '<<<<<<<\|=======\|>>>>>>>' --include='*.py' --include='*.js' --include='*.ts' --include='*.yaml' --include='*.json' . && echo "WARNING: markers found!" || echo "Clean — no markers"

# 4. Run tests, focusing on affected modules
# pytest tests/ -k "sauce or config"  (example for specific modules)

# 5. If config files changed, validate required keys
# python validate_config.py
Output
# Sample output:
=== Full merge diff ===
diff --git a/deployment.yaml b/deployment.yaml
index abc123..def456 100644
--- a/deployment.yaml
+++ b/deployment.yaml
@@ -10,6 +10,7 @@ spec:
+ X-Security-Header: true
=== Files that had conflicts ===
sauce.py
deployment.yaml
=== Marker scan ===
Clean — no markers
Checklist Mentality
  • Verify the diff against the first parent — it's the only diff that matters.
  • Run tests on the conflicted files specifically, not just the whole suite.
  • Check for markers automatically with a script.
  • Validate config files with a schema checker.
  • Get a second set of eyes on the merge diff before pushing to a shared branch.
Production Insight
A team I worked with had a merge that compiled and passed all tests.
But the resolved conflict accidentally swapped two boolean flags in a configuration file.
The result: a staging environment that misrouted traffic for 2 hours before someone noticed.
Rule: always diff the merge commit and add config validation in CI.
Key Takeaway
A merge isn't done until you've verified the diff against the base.
Run tests on affected files and scan for leftover markers.
Automate config validation — it's the silent killer of otherwise clean merges.

Using Git Merge Tools for Complex Conflicts — VS Code, IntelliJ, and vimdiff

Manual editing works for simple conflicts, but when a file has multiple conflict regions across several functions, a three-way merge tool saves time and reduces errors. These tools show three versions simultaneously: the base (common ancestor), the current branch, and the incoming branch. You build the result by picking hunks from either side or editing directly.

VS Code has a built-in three-way merge editor that activates when you open a conflicted file. It shows four panes: base, current, incoming, and result. You can click buttons to accept current, accept incoming, or accept both. IntelliJ IDEA's merge tool works similarly with a cleaner diff view.

Vimdiff is the terminal-based fallback. It splits the terminal into vertical panes. It takes some practice but is invaluable when you're SSH'd into a server or working without a GUI.

To launch a merge tool during a conflict, run git mergetool. Git will automatically open the configured tool for each conflicted file. Configure your preferred tool once: git config --global merge.tool vscode (or intellij, vimdiff, etc.). Some tools need additional command configuration — check the documentation.

The biggest advantage of a merge tool is that you see all three versions at once. You can spot when both branches added different lines near the same region, or when one branch deleted something the other kept. Visual comparison reduces blind mispicks.

configure_merge_tool.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#!/usr/bin/env bash
# -----------------------------------------------------------
# CONFIGURE AND USE MERGE TOOLS
# -----------------------------------------------------------

# Set VS Code as the default merge tool (one-time)
git config --global merge.tool vscode
git config --global mergetool.vscode.cmd 'code --wait $MERGED'

# Set IntelliJ as the default
git config --global merge.tool intellij
git config --global mergetool.intellij.cmd 'idea merge $LOCAL $REMOTE $BASE $MERGED'

# Set vimdiff as the default (built-in, no extra config)
git config --global merge.tool vimdiff

# Launch the merge tool during a conflict
git mergetool

# If you want to skip a file, use --no-prompt or abort mid-tool
# To see allowed tools: git mergetool --tool-help

# After resolving with the tool, the result is automatically staged
# Just commit: git commit
Output
# When you run git mergetool during a conflict:
Merging:
sauce.py
Normal merge conflict for 'sauce.py':
{local}: modified file
{remote}: modified file
Hit return to start merge tool invocation:
# (VS Code opens, you resolve, save, close, then Git continues)
Learn One Merge Tool Well
Don't try to master all three. Pick the one that fits your workflow — VS Code if you use it daily, IntelliJ if you're a JetBrains person, vimdiff if you live in the terminal. Proficiency in one merge tool will pay for itself the first time you face a 300-line conflict.
Production Insight
I once watched a junior dev manually edit a 200-line conflicted file for 45 minutes and miss a conflict region.
The merge compiled but the missing region caused a null pointer exception in production.
A merge tool would have shown all conflict regions in one view — no missed hunks.
Rule: if a file has more than 3 conflict markers, use a merge tool.
Key Takeaway
Three-way merge tools show base, current, and incoming side-by-side.
Configure your tool once with git config merge.tool.
For conflicts spanning multiple functions, tools beat manual edits every time.
● Production incidentPOST-MORTEMseverity: high

The Missing Security Header: How --theirs Dropped a Prod Config

Symptom
An internal API endpoint returned sensitive data to any caller without authentication. The WAF logs showed no violation because the custom header was gone.
Assumption
The team assumed the incoming branch had the latest production config. They used git checkout --theirs deployment.yaml to resolve the conflict quickly.
Root cause
The incoming branch was stale — it lacked a security header added two days earlier on main. The --theirs flag replaced the entire file, silently dropping that header.
Fix
1. Rolled back the merge with git revert -m 1 <merge-commit>. 2. Restored the security header manually. 3. Added a pre-merge Git hook to diff critical config files against the base before finalising any conflict resolution. 4. Documented that --theirs must never be used on deployment-related files.
Key lesson
  • Never use --theirs on configuration, secrets, or lock files without a full diff review.
  • Always run git diff HEAD~1 -- <file> after a conflict resolution to see exactly what changed.
  • Set up a pre-merge hook that blocks merges if certain files (e.g., deployment.yaml) are changed by --theirs without explicit approval.
  • Automate config validation in CI — catch missing settings before they hit production.
Production debug guideSymptom → Action for the most common merge conflict scenarios4 entries
Symptom · 01
Git reports 'CONFLICT (content)' but you don't see markers in the file
Fix
Run git diff --name-only --diff-filter=U to list all conflicted files. Then open each file and search for <<<<<<< — your editor may have collapsed the markers.
Symptom · 02
You see markers but can't tell which version is the base
Fix
Install diff3 style globally: git config --global merge.conflictStyle diff3. Then re-create the merge conflict to see the ancestor block. For an existing conflicted file, run git show :1:path to see the base version stored in the index.
Symptom · 03
Merge succeeds but tests fail after the merge commit
Fix
Run git diff HEAD~1 to review all changes brought in by the merge. Pay special attention to files that were conflicted — a wrong hunk selection might have broken logic. Then git log --oneline --graph --merges to see if the merge commit is clean.
Symptom · 04
Git keeps saying 'You have not concluded your merge (MERGE_HEAD exists)' after you tried to commit
Fix
You probably ran git commit without resolving all conflicts. Run git status to see which files are still marked as conflicted. If you actually resolved them but forgot to stage, run git add <files> then commit again. If you want to abort entirely, use git merge --abort.
★ Merge Conflict Quick Debug Cheat SheetUse these commands when you hit a conflict and need to move fast without making things worse.
You just ran `git merge` and got a conflict
Immediate action
Stop. Do not commit anything. List conflicted files first.
Commands
git diff --name-only --diff-filter=U
git checkout --ours <file> OR git checkout --theirs <file> — only if you're sure one side is 100% correct
Fix now
Set diff3: git config --global merge.conflictStyle diff3, then re-create the conflict if needed.
You resolved conflicts but you're not sure you did it right+
Immediate action
Do not commit yet. Verify the resolved file contains no markers and diff against the base.
Commands
grep -n '<<<<<<<\|=======\|>>>>>>>' <file>
git diff --cached (if staged) or git diff (if unstaged)
Fix now
Run git merge --abort and start fresh if the diff looks wrong.
You accidentally committed conflict markers into the codebase+
Immediate action
Undo the merge commit immediately. Do not push.
Commands
git revert -m 1 HEAD
git reset --soft HEAD~1 (if you haven't pushed and want to keep the changes unstaged)
Fix now
Add a pre-commit hook that scans for markers: grep -r '<<<<<<<' . --include='.py' --include='.js' --include='.ts' --include='.yaml' && exit 1
You're mid-rebase and the conflicts are piling up+
Immediate action
Decide if the rebase is worth it. If not, abort now.
Commands
git rebase --abort
git rebase --continue (after resolving per-commit conflicts)
Fix now
Use git rebase --interactive to squash commits first, reducing the number of conflict-heavy replay steps.
Aspectgit mergegit rebase
Conflict frequencyOnce per merge operationOnce per replayed commit — can be multiple times
History shapePreserves branch topology with a merge commitCreates linear history, no merge commit
Safe on shared branches?Yes — non-destructive, history is additiveNo — rewrites commits, dangerous if others have the branch
Conflict resolution granularityResolve all conflicts in one sessionResolve conflicts commit-by-commit
Undo if wronggit merge --abortgit rebase --abort
Best forMerging completed feature branches to mainKeeping a solo feature branch up to date with main
Conflict markers placementAll conflicts surfaced at once in affected filesConflicts appear per-commit as each is replayed
Audit trailMerge commit shows what was merged and whenNo merge commit — cleaner log, less traceability

Key takeaways

1
A merge conflict is Git doing its job correctly
it stops when it cannot safely combine changes, rather than silently picking a winner and hiding a bug in your code.
2
Enable git config --global merge.conflictStyle diff3 immediately
without the ancestor block in your conflict markers you are always resolving conflicts with incomplete information.
3
Never use git checkout --ours . or --theirs . (dot = all files) casually
these silently discard entire file versions across your whole working tree with no undo prompt.
4
The most effective conflict prevention strategy is syncing your feature branch with main daily using git fetch && git rebase origin/main
small frequent syncs beat one giant painful merge every time.
5
After every merge, verify with git diff HEAD~1, run tests on conflicted files, and scan for leftover markers
automation prevents production incident replay.

Common mistakes to avoid

5 patterns
×

Blindly using --theirs on all conflicted files

Symptom
code appears to compile but features go missing or tests fail silently after the merge
Fix
never use git checkout --theirs . (dot = all files) without reading what you are discarding. Always diff the result with git diff HEAD~1 after a merge commit to verify both sides contributed the right code.
×

Committing conflict markers into the codebase

Symptom
CI pipeline fails with SyntaxError or teammates find raw <<<<<<< HEAD text in production source
Fix
add a pre-commit hook that scans for markers: grep -r '<<<<<<<' --include='.py' --include='.js' --include='*.ts' . && exit 1. Tools like Husky make this trivial to enforce across the team.
×

Running `git merge --continue` instead of `git commit` after resolving a standard merge conflict

Symptom
Git prints 'fatal: There is no MERGE_HEAD' or creates an unexpected commit state
Fix
for a regular merge, resolve conflicts, git add the files, then run git commit. Use --continue only for rebase and cherry-pick operations. Merge and rebase have different continuation commands because they work differently internally.
×

Not checking for conflicts before starting a long merge

Symptom
mid-merge you discover dozens of conflicts and waste hours resolving them
Fix
use git merge-base to identify overlapping changes before merging. Review the conflict-prone files with your teammate beforehand. This pre-merge inspection turns a multi-hour conflict session into a 10-minute discussion.
×

Forgetting to run tests after a conflict resolution

Symptom
the merge succeeds, tests pass on unrelated code, but the resolved file has a logic error that surfaces in production
Fix
always run the test suite after a merge, especially tests that cover the conflicted files. Also run git diff HEAD~1 to verify the merge diff is what you intended.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
Walk me through exactly what you do when you hit a merge conflict on a f...
Q02SENIOR
What is the difference between `git merge --ours` and `git checkout --ou...
Q03SENIOR
If you are in the middle of a rebase and hit conflicts on three consecut...
Q04SENIOR
How does a three-way merge tool differ from a simple text editor when re...
Q01 of 04SENIOR

Walk me through exactly what you do when you hit a merge conflict on a file that two teammates have both heavily modified. What information do you look at before deciding how to resolve it?

ANSWER
First, I list all conflicted files with git diff --name-only --diff-filter=U. Then I enable diff3 conflict style if it's not already set. For each file, I look at the ancestor version to understand what both sides changed. I check if the changes are additive (both added new lines) or conflicting (both edited the same lines). For additive conflicts, I keep both. For conflicting changes, I check the git log of both branches to understand intent. I use a three-way merge tool for complex files. After resolution, I stage the file, run git diff --cached to verify, and then commit. Finally, I run the test suite focused on the affected modules and get a code review on the merge commit.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
How do I resolve a Git merge conflict without losing anyone's changes?
02
What does it mean when Git says 'Automatic merge failed; fix conflicts and then commit the result'?
03
Is it safe to use `git merge --abort` if I change my mind mid-conflict?
04
What is the difference between `git merge` and `git rebase` in terms of conflict resolution?
05
How can I avoid merge conflicts in the first place?
🔥

That's Git. Mark it forged?

7 min read · try the examples if you haven't

Previous
Git Tags and Releases
8 / 19 · Git
Next
Git Hooks Explained