Which side is ours?
The mechanics never change. Git stores three versions of a conflicted file in the index: stage 1 is the common ancestor, stage 2 is the version that was already checked out, stage 3 is the version being applied on top. --ours always means stage 2 and --theirs always means stage 3. What moves around is which of your branches lands in which slot.
| Command | --ours (stage 2, above =======) | --theirs (stage 3, below =======) |
|---|---|---|
git merge topic while on main | main, the branch you are on | topic, the branch you named |
git pull (merge) | your local branch | the upstream branch |
git rebase main while on topic | main, the branch you are replaying onto | topic, the one commit currently being replayed |
git pull --rebase | the upstream branch | your local commits |
git cherry-pick <sha> | your current HEAD | the commit being picked |
The bottom two rows are the ones that catch people out.
Rebase reverses the labels because it reverses the operation. It feels like you are pulling main into your feature branch, but Git does the opposite: it checks out main, then replays your commits on top one patch at a time. At the moment a conflict appears, the checked-out side is main and the incoming patch is yours. Run git checkout --ours config.js there, expecting to keep your own work, and you have just deleted it.
There is a habit that removes the ambiguity entirely: stop thinking in terms of ours and theirs, and read the marker labels instead. Git writes the branch name or the commit subject after >>>>>>> for exactly this reason.
Read the markers with the ancestor visible
The default conflict style shows you two endings and no history. You see what each side finished with, then infer what each side was trying to change. The three-way styles print the common ancestor between the ||||||| and ======= lines, which turns guesswork into reading.
# zdiff3 needs Git 2.35 or newer (released January 2022); check yours first
git --version
git config --global merge.conflictstyle zdiff3
# On older Git, use the original three-way style
git config --global merge.conflictstyle diff3
A conflict then looks like this:
<<<<<<< HEAD
timeout: 30,
retries: 0,
||||||| 4a1c9e2
timeout: 10,
retries: 0,
=======
timeout: 10,
retries: 3,
>>>>>>> feature/http-retries
Now the answer is obvious. Our side raised the timeout and left retries alone. Their side left the timeout alone and added retries. Neither block is correct on its own: the resolution is timeout: 30 with retries: 3, and no version of the file anywhere in the repository contains that text yet. Without the ancestor block you would have seen two plausible snippets and a coin toss.
<<<<<<< HEADto the next marker is stage 2, readable asgit show :2:pathand selectable with--ours.- Between
|||||||and=======is stage 1, the merge base. It appears only underdiff3orzdiff3. =======to>>>>>>>is stage 3, readable asgit show :3:pathand selectable with--theirs.- The text after
>>>>>>>is the branch name during a merge, or the hash and subject of the commit being replayed during a rebase.
zdiff3 differs from diff3 by hoisting lines that all three versions share out of the conflict block and into the surrounding file. The conflict gets smaller and the disagreement gets easier to see. Same information, less scrolling.
Work through it one file at a time
List every conflicted path
git statusnarrates the situation, but the short form is faster to scan and the filtered list is what you want in a script. In the short output,UUis both modified,AAis both added,UDis modified by us and deleted by them, andDUis the reverse.git status --short git diff --name-only --diff-filter=UFind out what each side was doing
Before editing anything, read the commits behind each version. The symmetric difference splits them by side:
<marks a commit on your branch and>marks one on theirs. During a rebase, substituteREBASE_HEADforMERGE_HEAD.git log --oneline --left-right HEAD...MERGE_HEAD -- src/config.js git log --merge -p -- src/config.jsEdit the file, not just the marked region
Conflict markers cover the lines Git could not reconcile. They do not cover the consequences. If their side added a required argument that your side's new call sites do not pass, fixing those call sites is part of this resolution, even though nothing near them is marked.
git diff --base -- src/config.js git diff --theirs -- src/config.jsMark the file resolved
Staging is what tells Git you are finished with that path. Git does not inspect what you staged, so the step is a promise rather than a check.
git add src/config.jsBuild and test before the commit exists
A conflicted tree is the cheapest place to discover you got it wrong, because
--abortis still available and no history has been written yet.Finish the operation
Committing without
-mkeeps the prepared message, which lists the conflicted paths. That list is genuinely useful months later when somebody bisects to your merge and wants to know where the risk was.git commit # merge git rebase --continue # rebase
Prove that nothing was dropped
This is the step people skip, and it is the one that matters. A resolution that compiles can still have discarded a colleague's bug fix without a murmur, because deleting half of a conflict block usually leaves perfectly valid code behind. Four checks catch nearly all of it.
# 1. Before committing: what does this merge do to my branch?
git diff HEAD
# 2. Before committing: what does it do to theirs?
# Their work that you dropped shows up here as a deletion.
git diff MERGE_HEAD
# 3. Anywhere: markers that survived into the tree
git grep -nE '^(<{7}|={7}|>{7})( |$)'
# 4. After committing: only the hunks that match neither parent
git show --cc HEAD
The second command is the important one. Reading git diff HEAD is natural, since it shows the change arriving in your branch, and almost nobody reads the mirror image. Yet that is precisely where a dropped change appears: as a line their branch had and the merge result does not.
The fourth command is the audit. A combined diff omits any hunk that agrees with one of the parents, so on a well-behaved merge git show --cc prints very little: only the regions where you made a decision. Every line in that output should be one you can explain. Lines you do not recognise are the definition of a resolution mistake.
One caveat about a check that looks stronger than it is. After merging origin/main, git log --oneline HEAD..origin/main prints nothing, and people read that emptiness as proof the merge is complete. It proves reachability and nothing else. Every one of those commits is now an ancestor of yours, which says exactly nothing about whether their contents survived your editing. Ancestry is bookkeeping. Content is the part you have to check yourself.
A clean merge can still be wrong
Git merges text. It has no idea what your code means, so two changes can be individually correct, textually far apart, and jointly broken. These are semantic conflicts, and they never produce a marker.
- One branch renames
getUsertofetchUser. The other adds four new call sites forgetUser. Different lines, clean merge, failing build. - Both branches add a database migration numbered
0042. Both files merge in fine, and your migration runner then picks an arbitrary order or refuses to run at all. - One branch tightens a validation rule while the other adds a fixture that violates it. Nothing conflicts. The suite fails on the merge commit and on neither parent.
- Both branches pin the same dependency at incompatible versions, one in
package.jsonand one in a workspace package.
The defence is unglamorous: run the full test suite on the merge result rather than on either branch, and do it before you push. A test that passes on both parents and fails on the merge is not flaky infrastructure. It is the merge telling you something true.
Lockfiles, binaries and delete/modify conflicts
Some conflicts should never be resolved by hand.
- Lockfiles. Resolve
package.jsonfirst, then regenerate. Recent npm versions can untangle a conflictedpackage-lock.jsonduringnpm install; if yours cannot, delete the lockfile and reinstall rather than editing the conflict. A hand-merged lockfile describes a dependency tree no resolver would ever have produced. See npm ERESOLVE for what tends to happen next. - Binary and generated files. There is no merging a PNG or a compiled asset. Pick a side with
git checkout --ours pathor--theirs, or regenerate the file from its source. On Git 2.23 and later (August 2019),git restore --ours pathdoes the same job under a clearer name. - Delete/modify conflicts. One side deleted the file, the other edited it. Choosing a side is close to meaningless here, because one of the stages does not exist. Ask instead whether the edit still needs a home: keep the file with
git add path, or accept the deletion withgit rm path.
Delete/modify is worth slowing down for. It usually means one branch moved or split a module while another kept working inside the old one, so the honest resolution is often to port the edit into the new location and then take the deletion.
Backing out
Abandoning a bad resolution is cheap. Do it early, rather than pushing through a merge you no longer understand.
git merge --abort # back to the pre-merge state
git rebase --abort
git cherry-pick --abort
# Botched one file but want to keep the rest of the work?
# This restores the conflict markers for that path alone.
git checkout -m -- src/config.js
# Already committed the merge and want it gone (unpushed branch only)
git reset --hard ORIG_HEAD
--abortrestores the pre-merge state, index included. It can refuse, or leave a mess, if you had uncommitted changes before starting. That is the argument for committing or stashing first.git checkout -m -- pathre-creates the conflicted version of one file from the stages still sitting in the index. It works right up until you finish or abort the operation.- Merge sets
ORIG_HEADto the commit you were on beforehand, sogit reset --hard ORIG_HEADundoes a merge you have not shared. If you already pushed, revert instead of resetting: see undoing a Git commit. git reflogstill holds the pre-merge position for weeks after a careless reset.
Undo a Git commit without losing work
Reset, revert, amend and the reflog, including how to undo a merge you already pushed.
Troubleshootingnpm ERESOLVE dependency conflicts
What to do after a lockfile conflict leaves the dependency tree inconsistent.
TroubleshootingNode 'Cannot find module'
A merge that moves or renames a file often surfaces here first.
Frequently asked questions
In a merge conflict, is HEAD my branch or the other one?
During git merge, HEAD is your branch: the one you were on when you ran the command, shown above the ======= line. During a rebase it is the branch you are replaying onto, so the block labelled HEAD holds the upstream code and your own commit sits below the =======. Reading the branch name after >>>>>>> is more reliable than remembering the rule.
Why does --ours mean the opposite thing during a rebase?
Because the operation is inverted. A rebase checks out the upstream branch and replays your commits on top as a series of patches, so the already-checked-out side, which is what --ours always refers to, is upstream rather than yours. Nothing is inconsistent: --ours is stage 2 of the index in both cases, and only the contents of that slot differ.
How do I keep both changes in a merge conflict?
Edit the file by hand and delete the three marker lines. There is no flag for it, because Git cannot know what combining two edits means for your code. Sometimes the answer is both lines in sequence; sometimes it is one line carrying both intentions. Turning on merge.conflictstyle zdiff3 shows the common ancestor, which usually makes the combination obvious.
How do I check I did not lose someone else's changes?
Before committing, run git diff MERGE_HEAD. That shows the merge result from the other branch's point of view, so anything of theirs you dropped appears as a deletion. After committing, git show --cc <sha> prints only the hunks matching neither parent, which is a short list on a healthy merge and should contain nothing you cannot account for.
Can I just delete the conflict markers and commit?
Git will let you, and that is the problem. Removing markers without deciding what the code should do produces a commit that compiles while silently discarding half of somebody's change. Run git diff --cached --check before committing to catch markers you missed, and run the tests against the merge result rather than trusting either parent.
How do I fix a merge conflict in package-lock.json?
Do not resolve it by hand. Resolve package.json, then let the tool regenerate the lockfile: recent npm versions handle a conflicted lockfile during npm install, and deleting the file before reinstalling works everywhere. A hand-edited lockfile can describe a tree the resolver would never produce, which then breaks on a teammate's machine or in CI.
What does 'Automatic merge failed; fix conflicts and then commit the result' mean?
Git merged everything it could and stopped on the files it could not. Your working tree now holds a mixture of merged content and marked conflicts, while the index holds all three stages of every conflicted file. Run git status for the list, resolve each path, git add it, then git commit. Nothing is lost at this point, and git merge --abort returns you to where you started.