How to handle merge conflicts in Git: a step-by-step guide
Merge conflicts are the moment most juniors panic. The terminal yells, the file looks unreadable, and the instinct is to nuke the branch and start over. Don't. Conflicts are routine, mechanical, and the entire fix takes about three minutes once you know what you're looking at. This guide is the calm version.
What a merge conflict actually is
A merge conflict happens when two branches have changed the same lines in the same file, and Git doesn't know which version to keep. That's it. Nothing is broken, nothing is lost, Git is just asking you to make a choice it can't make on its own.
The trigger is usually one of these:
git pullon a branch where someone else also pushed changesgit merge maininto your feature branch when main has moved ongit rebase mainfrom your feature branch (same idea, different mechanism)- Cherry-picking a commit that touches changed code
Git will say something like:
Auto-merging src/api/users.py
CONFLICT (content): Merge conflict in src/api/users.py
Automatic merge failed; fix conflicts and then commit the result.
You're not in trouble. You're in a state Git calls "merging", a paused merge that's waiting for your input.
The conflict markers, explained
Open the conflicting file. You'll see something like:
def get_user(user_id):
<<<<<<< HEAD
user = db.query(User).filter(User.id == user_id).first()
if not user:
raise NotFound("User does not exist")
return user
=======
user = db.query(User).filter(User.id == user_id).one_or_none()
if user is None:
return None
return user
>>>>>>> feature/user-cleanup
Three sets of markers, three sections:
<<<<<<< HEAD...=======, the version on your current branch (the branch you're merging into).=======...>>>>>>> feature/user-cleanup, the version coming in from the other branch.
Your job: replace the entire chunk (markers and all) with what the final code should look like. That might be:
- Keep yours. Delete from
=======to>>>>>>>, and delete the<<<<<<<line. - Keep theirs. Delete from
<<<<<<<to=======, and delete the>>>>>>>line. - Combine. Write a new version that takes pieces of both. Common when both sides made independent valid changes.
That's the whole skill. Most conflicts in real codebases are 5-15 lines long and take 30 seconds to read.
The full resolution flow
Step by step, end to end:
# 1. You hit a conflict during pull / merge / rebase.
# 2. See which files conflict:
git status
# Output will list conflicted files under "Unmerged paths":
# both modified: src/api/users.py
# both modified: src/utils/format.py
Open each conflicting file in your editor. VS Code, JetBrains, and Vim plugins all have UI for this, they highlight the markers and let you click "Accept Current," "Accept Incoming," or "Accept Both." Use the UI if you have one; manually editing the markers is fine too.
# 3. Edit each file to the final correct version.
# 4. After editing, mark each file as resolved by staging it:
git add src/api/users.py
git add src/utils/format.py
# 5. Verify nothing's left unresolved:
git status
# Output should now say "All conflicts fixed but you are still merging."
# 6. Complete the operation:
git commit # if you were merging
git rebase --continue # if you were rebasing
If you were rebasing and there are more commits to apply, Git will keep stepping through them, possibly hitting more conflicts. That's normal, repeat the process for each.
Run the tests before you celebrate
This is the step every junior skips. Resolving the markers means the file compiles. It does not mean your code is correct.
You can produce a syntactically valid file from a conflict that's still semantically broken, for example, by accidentally deleting a function call that one branch added but you forgot. Always run the test suite (or at minimum the local app) after resolving conflicts and before pushing.
npm test # JS
pytest # Python
go test ./... # Go
If anything was silently lost in the merge, tests catch it. Push without testing and you've shipped a regression.
Practice the full Git workflow on real codebases
InternQuest's missions drop you into pre-built repositories with realistic conflicts and convention requirements. Branch, fix, commit, push, and resolve. Free.
Try a Git mission →Merge vs rebase: which causes conflicts where
Both can produce conflicts. The difference is when and how:
Merge (git merge main)
Combines two branches into one new commit. If conflicts occur, they happen once, in a single merge commit. You resolve them, commit, done. Easy to reason about. Adds a "merge commit" to your history.
Rebase (git rebase main)
Replays each of your branch's commits one-by-one on top of main. If commit #1 conflicts, you resolve it. Then commit #2 might also conflict, sometimes with the same code, because you're applying different historical states. You can hit conflicts multiple times in a row.
Rebase makes prettier history (linear, no merge commits) but takes longer with many commits. For a feature branch with 8 commits and significant divergence from main, you might hit 3-4 separate conflict rounds during a rebase that would have been one with merge.
Practical rule: if you're a junior on a small team, use git merge main when syncing your feature branch. Save rebasing for when you're more comfortable. Most teams accept either.
How to abort if you're in over your head
You started a merge or rebase, you're staring at 47 conflicts in 12 files, and the panic is rising. There's an exit:
# If you were merging:
git merge --abort
# If you were rebasing:
git rebase --abort
# If you cherry-picked:
git cherry-pick --abort
This puts you back to exactly where you were before. Nothing is lost. Now you can think about it, ask for help, or break the merge into smaller pieces (e.g., merge in chunks instead of all at once).
Knowing the abort exists is half of feeling confident with conflicts. You can always back out.
Strategies that prevent conflicts in the first place
Most conflicts are avoidable with three habits:
- Sync often. Pull from main into your feature branch every day or two. Small conflicts resolved often = no big conflict storm at the end. The big-bang merge after two weeks of divergence is where suffering lives.
- Keep PRs small. A 100-line PR rarely has conflicts. A 1,500-line PR has them constantly. Splitting work into smaller branches is a conflict-prevention strategy as much as a review-quality one.
- Communicate when working in the same area. If you and a teammate are both editing
users.py, mention it. Coordinate so you're not both rewriting the same function in different directions.
Less-common conflict types you'll eventually meet
Binary file conflicts
Git can't merge images, PDFs, or compiled binaries. You have to pick one version with git checkout --ours path/to/file or --theirs. The resolution is faster but less flexible, you can't combine.
Rename conflicts
You renamed a file; someone else edited it. Git often handles this gracefully but sometimes asks you to confirm. git status will explain.
Deletion conflicts
You modified a file; someone else deleted it. Git asks: do you want to keep your modifications (re-create the file) or accept the deletion (lose your changes)? Make sure you understand which before deciding.
Lockfile chaos
Both branches updated package-lock.json or poetry.lock. Don't try to manually merge these, they're machine-generated. Instead: keep one side, delete the other, and regenerate:
git checkout --theirs package-lock.json
rm package-lock.json
npm install # regenerates lock from package.json
Tools that make conflicts less painful
- VS Code's built-in merge UI. Three-pane view with click-to-accept buttons. Probably the easiest place to start.
git mergetool, opens a 3-way diff tool of your choice. Useful for complex conflicts.- JetBrains IDE merge tool, best 3-way merge UI on any platform, in my experience. If you use IntelliJ, PyCharm, or WebStorm, learn to use it for conflicts.
- GitHub's "resolve in browser", for trivial conflicts (e.g., README.md), GitHub will let you resolve them in the PR view without leaving the browser.
The mindset shift
Merge conflicts feel like a Git error. They're not, they're a question. Git is asking you the only thing it can't figure out alone: "two people changed this; what's the right answer?"
Once that frame clicks, conflicts become routine. You'll resolve dozens during a real internship without anyone making a big deal of it. The first one is scary; the hundredth takes 90 seconds.
And if you ever feel stuck, remember: git merge --abort exists. You always have a way back.
Practice Git mechanics on real broken codebases
InternQuest's missions include realistic Git scenarios, branching, conventional commits, and PR workflows graded by an automated reviewer. Practice the muscle, free.
Try a Git mission →