A merge conflict isn't Git breaking. It's Git asking you a question it can't answer alone. Here's how to answer it calmly, every time.
The first time I hit a merge conflict I closed the terminal and hoped it would go away. It did not. The branch sat there half-merged for two days until a senior engineer walked over, spent ninety seconds fixing it, and moved on. What looked like a wall was a five-minute task I didn't understand yet.
A conflict happens when two branches change the same lines of the same file, and Git has no way to know which version you want. Git is good at merging changes that don't overlap. Two people editing different functions in the same file? Merged automatically, no drama. But when both sides touch the same line, Git stops and hands the decision back to you. That's the whole thing. It isn't an error. It's Git refusing to guess.
When a merge or pull stops with a conflict, the first command is always the same:
$ git merge feature/pricing
Auto-merging src/checkout.js
CONFLICT (content): Merge conflict in src/checkout.js
Automatic merge failed; fix conflicts and then commit the result.
$ git status
On branch main
You have unmerged paths.
(fix conflicts and run "git commit")
Unmerged paths:
(use "git add <file>..." to mark resolution)
both modified: src/checkout.js
both modified is the label that matters. Those are the files you have to open. Everything else Git already handled. On a big merge you might have three conflicted files out of forty, so don't panic at the size of the merge, only at the count under "Unmerged paths."
Open a conflicted file and you'll find Git has stuffed both versions inline, wrapped in markers:
function taxRate(region) {
<<<<<<< HEAD
return region === 'EU' ? 0.21 : 0.08;
=======
return REGION_TAX[region] ?? 0.10;
>>>>>>> feature/pricing
}
Three markers, and each means something specific. <<<<<<< HEAD opens the version from your current branch, the one you had checked out. ======= is the divider. >>>>>>> feature/pricing closes the version coming in from the branch you're merging. So the top block is "mine," the bottom block is "theirs."
Your job is to edit this into the single correct answer and delete all three markers. Sometimes you keep the top, sometimes the bottom, and often you write something new that takes the best of both:
function taxRate(region) {
return REGION_TAX[region] ?? 0.08;
}
The markers have to go completely. A leftover ======= will get committed and break the file, and worse, it'll compile in some languages before failing somewhere confusing. I grep for <<<<<<< across the repo before I commit any conflict resolution. It has caught me more than once.
Once the file reads the way you want, you tell Git this path is resolved:
$ git add src/checkout.js
$ git status
All conflicts fixed but you are still merging.
$ git commit
For a plain merge, git commit with no arguments opens a prepared merge message and you're done. If the conflict happened during a rebase instead, the command is git rebase --continue. During a cherry-pick, it's git cherry-pick --continue. Same shape every time: fix the file, git add, then continue.
Here's the one setting that changed how I feel about conflicts. By default Git shows you "mine" and "theirs" but not the common ancestor, so you're guessing what each side was trying to change. Switch to diff3 (or zdiff3 on newer Git) and it shows all three:
$ git config --global merge.conflictStyle zdiff3
Now a conflict includes the original:
<<<<<<< HEAD
return region === 'EU' ? 0.21 : 0.08;
||||||| merged common ancestor
return 0.08;
=======
return REGION_TAX[region] ?? 0.10;
>>>>>>> feature/pricing
That middle block is the answer key. You can see the base was a flat 0.08, one side added an EU case, the other side moved to a lookup table. Now the merge is obvious: keep the table, default it to 0.08. Without the ancestor you'd have stared at two rewrites with no idea what changed from what. I set this on every machine I touch.
You don't have to edit raw markers. git mergetool launches a configured three-pane tool. VS Code is my daily driver here: open the conflicted file and it gives you "Accept Current," "Accept Incoming," and "Accept Both" buttons right above each block, plus a merge editor with a result pane. For anything mechanical it's faster than editing by hand. For anything subtle I still read the diff3 markers, because buttons make it too easy to accept a side without thinking.
Sometimes you start a merge, see the mess, and decide it's not the moment. You can back out cleanly:
$ git merge --abort
That returns you to exactly where you were before the merge started, no half-finished state. During a rebase the escape hatch is git rebase --abort. Knowing these exist is half of why conflicts stop being scary. Nothing you do mid-merge is permanent until you commit.
Conflicts are a function of how long branches drift apart. The longer two branches live without seeing each other, the more the same lines diverge. So pull from the main branch often, keep pull requests small enough to merge in a day or two, and say something in chat when you're about to refactor a file half the team touches. Most brutal conflicts I've untangled came from a two-week branch meeting a big reformatting commit. That's an organizational problem wearing a Git costume.
For repeated conflicts, Git has rerere (reuse recorded resolution). Turn it on with git config --global rerere.enabled true and Git remembers how you resolved a given conflict. Next time the same hunk collides, during a repeated rebase or a long-lived branch, it replays your resolution automatically. It quietly saves real time on gnarly rebases.
If you want a broader map of Git failure modes beyond merges, our Git troubleshooting guide covers the rest of the error messages that make people close the terminal.
Turn on zdiff3 today, on every machine, before you hit your next conflict. Seeing the common ancestor turns most conflicts from a guess into a decision. Add rerere if you rebase long branches. Then treat conflicts as what they are: Git asking a question only you can answer, with --abort always sitting there as a free undo. Once you trust the undo, the fear goes away and it's just editing.
Get the latest tutorials, guides, and insights on AI, DevOps, Cloud, and Infrastructure delivered directly to your inbox.
Our best engineer quit citing on-call. We rebuilt the whole thing: saner rotations, runbooks that actually help at 3am, and escalation that doesn't punish asking for help.
We ripped every client secret out of our CI pipelines by pointing Azure federated credentials at GitHub's OIDC issuer. Here's the exact setup and the claims that trip people up.
Explore more articles in this category
The IaC landscape fractured after the Terraform license change. This is the map to what each tool is actually best at, and how to choose without regret.
Terragrunt keeps large Terraform setups DRY and orchestrated, but small teams often pay its learning curve for little return.
A practical comparison of Kubernetes-native continuous reconciliation against the classic CLI-driven, state-file IaC model for platform teams.
Evergreen posts worth revisiting.