How to resolve merge conflicts in Git (without panicking)
A merge conflict is not an error — it is Git asking you to decide. Learn the base/ours/theirs model, the conflict markers, and why ours and theirs swap between merge and rebase.
You run git merge main. The terminal prints something you've seen other people complain about but never had yourself:
Auto-merging src/auth.ts
CONFLICT (content): Merge conflict in src/auth.ts
Automatic merge failed; fix conflicts and then commit the result.
Now src/auth.ts has these strange lines in it — <<<<<<<, =======, >>>>>>> — wrapped around two copies of your code. git status is red. You're not sure if you broke the repo. You're not sure if running the wrong command will lose your work. So you copy the error into a search box and hope.
Take a breath. Nothing is broken. You have not lost anything. A merge conflict is not an error message in the usual sense. It is Git stopping to ask you a question it cannot answer on its own: "Two people changed the same lines. Which version do you want?"
This post explains what a conflict actually is, how to read the markers, and the exact steps to resolve one. It also covers the one thing most guides skip — ours and theirs mean opposite things in a merge versus a rebase, and getting that wrong is how people pick the wrong side and quietly lose work.
What a conflict actually is
When you merge two branches, Git does not just compare them to each other. It compares three things.
- The base — the last commit both branches shared before they split apart. Git calls this the common ancestor or merge base.
- Ours — your version of the file.
- Theirs — the other version of the file.
This is called a three-way merge. Git looks at the base, then asks: what did each side change relative to that starting point?
If only one side touched a region of the file, Git takes that change automatically. If both sides changed the same region in the same way, Git also resolves it on its own. The official docs describe these as lines "cleanly resolved because only one side changed, or cleanly resolved because both sides changed the same way."
A conflict happens only when both sides changed the same lines in different ways. Git has no way to know which one you meant, so it stops and writes both versions into the file for you to decide.
The base is the key to understanding the situation. Without it, you only see two versions fighting. With it, you can see what each side was trying to do. Maybe you added a log line and your teammate renamed the function on the same line. Neither change is wrong. They just landed in the same spot.
That is the whole concept. A conflict is a three-way situation — base, ours, theirs — that Git cannot reduce to one answer by itself.
Reading the conflict markers
Open the conflicted file and you'll see something like this:
function signIn(user) {
<<<<<<< HEAD
logger.info("sign-in attempt", user.id);
return auth.verify(user.token);
=======
return auth.validate(user.token);
>>>>>>> feature/new-auth
}
Three markers split this into two sections.
<<<<<<< HEADopens the conflict. Everything from here to the=======is your side — the version on the branch you currently have checked out. (HEADis a pointer to where you are right now.)=======is the divider between the two sides.>>>>>>> feature/new-authcloses the conflict. Everything from the=======up to it is the other side — the version coming in from the branch being merged.
The git-merge docs put it plainly: "The part before the ======= is typically your side, and the part afterwards is typically their side." (git-merge)
Your job is to delete the markers and leave the file the way you want it. That might mean keeping your side, keeping their side, keeping both, or writing something new that combines them. Git does not care which — it only cares that the markers are gone and the file makes sense.
Seeing the base too
The default markers show you ours and theirs, but not the base. That missing piece is often what makes a conflict hard to read. You can turn the base on:
git config --global merge.conflictStyle zdiff3
Now conflicts include a third section, marked with |||||||, showing the common ancestor:
<<<<<<< HEAD
logger.info("sign-in attempt", user.id);
return auth.verify(user.token);
||||||| base
return auth.verify(user.token);
=======
return auth.validate(user.token);
>>>>>>> feature/new-auth
Now the story is clear. The base called auth.verify. Your side kept verify and added a log line. Their side renamed it to auth.validate. With that in front of you, the right resolution is obvious — keep the log line and the new name:
logger.info("sign-in attempt", user.id);
return auth.validate(user.token);
The diff3 style does the same thing in an older format; zdiff3 (Git 2.35+) is a tidier version that does not repeat lines common to both sides. If you only change one Git setting after reading this post, make it this one.
Resolving a conflict, step by step
Here is the full loop, from the conflict appearing to the merge being done.
Step 1: See what conflicted
git status
On branch feature/new-auth
You have unmerged paths.
(fix conflicts and run "git commit")
(use "git merge --abort" to abort the merge)
Unmerged paths:
(use "git add <file>..." to mark resolution)
both modified: src/auth.ts
no changes added to commit (use "git add" and "git push")
The phrase to look for is both modified. Those are your conflicted files. Everything else Git already merged cleanly. If five files changed in the merge but only one says "both modified," you only have one file to fix.
Step 2: Edit each conflicted file
Open src/auth.ts. Find each block of markers. Decide what survives. Remove the <<<<<<<, =======, and >>>>>>> lines. Make sure the result is valid code.
Do this for every conflicted region in every "both modified" file. A single file can have several conflicts in it.
Step 3: Mark each file as resolved
git add src/auth.ts
git add here does not mean "stage a new change." It means "I have resolved this file." Running it tells Git the conflict in that file is settled. Run git status again to confirm nothing is left:
git status
All conflicts fixed but you are still merging.
(use "git commit" to conclude merge)
Step 4: Finish the merge
git commit
Git opens an editor with a pre-filled merge message. Save and close it. The merge is complete. You now have a merge commit that joins both branches.
That is the entire process: git status to find the files, edit them, git add each one, then git commit. Four steps, every time.
You can practise this loop against a real seeded repo in the merge conflict scenario — a live terminal where you hit a genuine conflict and resolve it with no real repo at risk.
The part most guides get wrong: ours and theirs swap in a rebase
Here is the single most useful thing in this post. Read it twice.
In a merge, ours is the branch you are on and theirs is the branch you are merging in. That matches what you'd expect.
In a rebase, the meanings are reversed. ours is the branch you are rebasing onto, and theirs is your own work being replayed. This trips up people who learned conflicts on merges and then hit their first rebase conflict.
Why does it flip? Because of how rebase works mechanically. A rebase does not merge your branch into another. It takes your commits, sets them aside, moves to the tip of the upstream branch, and then replays your commits one at a time on top of it — like running git cherry-pick for each. The git-rebase docs describe it as: "Make a list of all commits on your current branch... Check out <upstream>... Replay the commits, one by one, in order."
So during the replay, the "current" state Git is building on is the upstream branch — that becomes ours. The commit being replayed on top is the incoming change — that becomes theirs. The docs say this directly:
"when a merge conflict happens, the side reported as ours is the so-far rebased series, starting with
<upstream>, and theirs is the working branch. In other words, the sides are swapped." (git-rebase)
A concrete example
You're on feature/new-auth. You want to rebase it onto the latest main.
If you merge main into your feature branch:
git switch feature/new-auth
git merge main
In the conflict, <<<<<<< HEAD is your feature work (ours), and the block after ======= is main (theirs). Normal.
If you rebase your feature branch onto main:
git switch feature/new-auth
git rebase main
In the conflict, <<<<<<< HEAD is now main (ours), and the block after ======= is your feature work (theirs). The marker even reflects this — HEAD during a rebase points at the rebased-onto base, not your branch tip.
The content of the conflict is the same. The labels are swapped. If you blindly "keep ours" out of habit, you keep main's version in a merge but throw away your own work in a rebase. That is how people lose changes during a rebase and don't notice until later.
The fix is simple: do not pick a side by the words ours and theirs. Read the actual code in each block and choose based on what it says. The words are only safe shortcuts once you know which operation you're in.
Taking one side wholesale
Sometimes you don't want to merge the two versions by hand. You want one side, whole, and the other side gone. Git has shortcuts for that.
git checkout --ours src/auth.ts # keep our version of this file
git checkout --theirs src/auth.ts # keep their version of this file
git add src/auth.ts
The git-checkout docs define these as checking out "stage #2 (ours) or #3 (theirs) for unmerged paths." They take the entire file from one side, not line by line — so use them only when you truly want one whole version.
The swap applies here too. During a rebase, --ours and --theirs are reversed in exactly the same way as the markers. The git-checkout docs warn about this explicitly:
"Note that during
git rebaseandgit pull --rebase,oursandtheirsmay appear swapped;--oursgives the version from the branch the changes are rebased onto, while--theirsgives the version from the branch that holds your work that is being rebased." (git-checkout)
So in a rebase, if you want to keep your own work for a file, you reach for --theirs, not --ours. Counterintuitive, but the docs are clear: during rebase, "your work" is theirs.
When you want to bail out
If a conflict is bigger than you expected, or you started the wrong operation, you do not have to push through. You can undo the whole thing and return to where you were before.
For a merge:
git merge --abort
This "tries to reconstruct the pre-merge state," per the git-merge docs. Your branch goes back to exactly where it was. The merge never happened.
For a rebase:
git rebase --abort
This "abort[s] the rebase operation and reset[s] HEAD to the original branch," per the git-rebase docs. Your commits return to their original place, untouched.
One caution: both abort commands work cleanly when you committed or stashed your other changes before starting. The git-merge docs note that with uncommitted changes present, --abort "will in some cases be unable to reconstruct these changes." The habit that protects you: commit or stash before any merge or rebase.
And after a rebase — not a merge — you finish with git rebase --continue instead of git commit:
git add src/auth.ts
git rebase --continue
The rebase then moves on to replay the next commit, which may produce its own conflict. Resolve, add, continue, repeat until the rebase finishes.
Tools that help
Two more things worth knowing about, briefly.
git mergetool opens a visual three-way merge tool — base, ours, theirs in separate panes — for files you'd rather not edit by hand. It works with editors and dedicated tools like Meld, KDiff3, or your IDE's built-in resolver. Run git mergetool after a conflict and it walks you through each file. Some people swear by it; others find plain text and zdiff3 faster. Try it once and decide.
git rerere ("reuse recorded resolution") records how you resolve a conflict, then replays that same resolution automatically the next time the identical conflict appears. It's quietly powerful on long-lived branches where you rebase repeatedly and keep hitting the same conflict. We'll cover it properly in a future post — for now, just know the name exists.
Common myths
Three things people believe about conflicts that are wrong.
Myth 1: "A conflict means I did something wrong." No. A conflict is the normal result of two people editing the same lines. It is not an error or a sign of a mistake. Git is doing exactly its job — refusing to guess when it genuinely cannot know which change you want. The more a team works in parallel, the more conflicts happen. Frequent conflicts mean active collaboration, not bad practice.
Myth 2: "Git picks a winner for me if I wait." No. Git never silently chooses between two conflicting edits. When it can resolve cleanly, it does — and then there's no conflict at all. When it marks a conflict, it has stopped and handed the decision entirely to you. Nothing proceeds until you remove the markers and run git add. There is no timeout, no default, no automatic winner.
Myth 3: "Rebase conflicts work just like merge conflicts." Half true, and the wrong half hurts. The resolution loop is the same — edit, git add, continue. But ours and theirs are swapped, as the git-rebase docs state outright. If you treat a rebase conflict exactly like a merge conflict and pick "ours" by habit, you discard your own work. Always read the code in each block rather than trusting the labels.
What to read next
A conflict is one of several moments where Git feels scarier than it is. These posts cover the others:
- Reorder and edit commits with git rebase — since rebase is where the trickiest conflicts show up, this explains the operation itself in depth.
- git reflog: the undo button you didn't know you had — if a bad conflict resolution slips through, this is how you recover the lost state.
- Recover a deleted Git branch — the same recovery mindset, applied to branches.
- Choosing a Git workflow — short-lived branches produce smaller, rarer conflicts; this helps you pick a workflow that keeps them small.
- git rebase --update-refs: rebase stacked branches in one shot — where the
ours/theirsswap bites hardest, since rebasing a stack replays a lot of commits.
When you're ready to do this with your hands instead of reading about it, the Resolving Merge Conflicts lesson opens a live terminal with a real conflict waiting. Read the markers, pick the right side, and ship the merge.