Written: August 31, 2026
You open the pull request. Green checks everywhere — until Git throws that red wall of conflict markers and the whole afternoon tilts. You’ve felt that pinch: not “I don’t know Git,” but “I don’t know which story about this file is true.”
This guide walks through what merge conflicts actually are, how to resolve them without guessing, and how to protect the next merge with a test that names the pain. You’ll leave with a concrete workflow you can run on the next bad Monday.
What a merge conflict is really telling you
Git is not angry. Git is literal. Two branches edited overlapping lines, or one deleted what the other rewrote, and Git refuses to invent a third version of reality. That refusal is the feature.
When people say “merge hell,” they usually mean two things at once: the mechanical markers (<<<<<<<, =======, >>>>>>>) and the social mess underneath — unclear ownership, a long-lived branch, or a refactor that landed while you were still shipping a hotfix.
Recognition first: if your stomach drops when you see conflict markers, you’re not bad at version control. You’re staring at a decision that used to be invisible. The conflict just made the decision loud.
Stop guessing: read the hunk for intent
Guessing looks like this: keep whichever side looks cleaner, delete the markers, commit, hope CI catches it. Sometimes you get lucky. Sometimes you ship a silent logic bug that only shows up for one customer path.
Intent looks different. You ask three questions before you touch a key:
- What was our branch trying to accomplish in this file?
- What was theirs trying to accomplish?
- Is there a third option that keeps both intents without pasting both blocks blindly?
Open the conflicted file in a real editor — not only the GitHub web UI if the hunk is large. Scroll enough context above and below the markers to see the function, the early return, the null check you almost missed. Conflicts live in lines; bugs live in behavior.
A concrete image: you’re staring at a payment helper. Your branch added a retry. Theirs added a stricter validation. Keeping “prettier” indentation and dropping the validation is how money leaks. Keeping both poorly is how you double-charge. Intent says: retry after validation succeeds, and you write that combination deliberately.
A practical resolution workflow that survives real PRs
Here’s a sequence that stays calm under pressure. Use it whether you’re on the command line or in an IDE merge tool.
1. Update before you dig. If you’re merging main into your feature branch, fetch first. Stale bases create conflict theatre — noise that already got fixed upstream.
2. List the battlefield. Run git status. Note conflicted paths. Separate “I know this file” from “I need the author of the other change.” Don’t resolve someone else’s domain while guessing.
3. Resolve one file at a time. Finish, stage, then move on. Half-resolved trees are how you lose track of a leftover marker that breaks production builds at the worst possible hour.
4. Prefer small, named commits after resolution. If your team allows it, a commit message like resolve merge: keep retry after validation in payments tells the next reader what you chose. Future you will need that sentence.
5. Run the smallest honest check. Not “the whole suite if it takes forty minutes” — the test or script that covers the conflicted behavior. If no such test exists, that’s your next section’s problem.
There’s a catch people skip: accepting “theirs” or “ours” wholesale with git checkout --theirs feels fast and often deletes careful work. Use those flags only when you truly mean to discard an entire side of a file, not when you’re tired.
Common conflict shapes (and how not to botch them)
Both sides edited the same block. Classic markers. Merge by intent. If both changes are required, rewrite the block so the control flow is clear — don’t leave duplicated if-statements stacked like debris.
One side deleted, one side edited. Git asks: delete or keep the edited version? Check why the delete happened. A deleted dead feature and an edit that fixed a bug in that feature need a product answer, not a pretty-diff answer.
Import / include chaos. Easy to “resolve” by keeping every import. Then you get unused imports, circular imports, or the wrong package version. After the merge, run the linter or compiler — treat clean imports as part of the conflict, not a chore for later.
Generated files and lockfiles. Don’t hand-merge package-lock.json or similar if you can regenerate. Pick a strategy as a team: regenerate from the surviving package manifest, or designate one person to rebuild locks. Hand-stitched lockfiles are a special genre of pain.
Formatting-only wars. If half your conflicts are braces and trailing commas, fix the process: shared formatter, format-on-save, and format commits separated from logic commits. Merge tools shouldn’t be your autoformatter.
Then run the test that would have caught a bad merge
Resolution without verification is optimism with a commit hash. The cheapest insurance is a test that fails when the wrong side “wins.”
You don’t need a perfect suite. You need one failing case that encodes the intent you just protected. Example: if you kept the stricter validation, write a test that sends the previously sneaky payload and expects rejection. If you kept the retry, write a test that simulates a transient failure and expects a second attempt — not infinite loops.
If the conflict touched configuration, add a smoke check: boot the app, hit the health endpoint, run the migration dry-run. Configuration conflicts are famous for looking “resolved” while the app dies on cold start.
When there’s no test yet, write the smallest one before you push. That order matters. Writing it after CI is red trains you to patch symptoms. Writing it while the conflict is fresh captures the decision you just made.
Prevention beats heroics: shrink the blast radius
Some conflicts are inevitable. Many are scheduled by habit.
Short-lived branches. The longer a branch lives, the more it argues with reality. Merge or rebase onto main often. Small disagreements are easier than archaeological digs.
Own fewer hot files at once. Giant “utils” modules are conflict magnets. Split by domain. If five teams edit helpers.js, you’re not doing Git wrong — you’re designing for collisions.
Communicate refactors. A rename across the codebase while someone else edits call sites is a predictable mess. Announce refactors, land them fast, or pair. Silence is expensive.
Agree on rebase vs merge culture. Either can work. Mixing them without a team rule creates surprise histories and surprise conflicts. Document the default. Teach the exception.
CI that blocks leftover markers. A tiny check that fails if conflict markers remain in the tree has saved more than one Friday deploy. It’s undignified and effective.
When you should stop and ask a human
Not every conflict is yours to finish alone. Pause when:
- The other change touches security, money, or privacy and you don’t own that surface.
- You can’t explain why both sides exist after five minutes of reading.
- The “fix” would delete a public API without a migration plan.
- You’re resolving under fatigue and the file is critical path.
Ping the author. Share the hunk. Ask, “Did you mean to keep X when Y landed?” That message is cheaper than a rollback. Teams that treat conflict questions as normal ship cleaner than teams that treat asking as weakness.
A short field checklist you can paste into your notes
- Fetch and update the base.
- List conflicted files; assign ownership mentally.
- For each file: read intent, rewrite deliberately, remove all markers.
- Stage the file; don’t leave a half-merged tree overnight if you can help it.
- Run the behavior check that matches the decision.
- Write or update a test if none exists.
- Commit with a sentence that records the choice.
- Open the PR description with “Merge notes: …” when the conflict was non-obvious.
Print that list once. The second time you use it, it’ll feel slower than guessing. The third time, you’ll notice you’re not redoing the same broken merge twice a week.
Closing: the conflict was never the real problem
We started with that red wall of markers and the private fear that you’re “bad at Git.” The markers were never the insult. They were a flashlight on two intents that needed a referee.
Next conflict: don’t pick the prettier side. Pick the story you can defend, write the combination carefully, and run the test that would have caught a bad call. That’s how merge conflicts stop feeling like chaos and start feeling like decisions.
What’s your fastest recovery move when merge hell hits mid-sprint — and would you teach that move to a junior the same way? If you have a sharper checklist than the one above, leave it in the comments so the next engineer who hits the red wall doesn’t have to invent the process alone.