Rebase Conflicts And Why Ours And Theirs Flip

During a rebase, HEAD is the other branch. That inversion confuses everyone once, and it can quietly delete your own commit if you resolve on autopilot.

July 25, 20265 min read3 / 6

The merge conflict ended with a warning about diverged histories producing endless merge commits. Rebase is the usual answer to that, and it handles conflicts differently enough to be worth its own post.

First, get back in sync.

An Aside: Push Rejected Because It Is Checked Out

Bash
git push

It fails, and the error is worth reading rather than skipping.

You are trying to push to trunk, and trunk is currently checked out in hello-git.

Think about what pushing would mean there. Someone has that branch checked out with a working tree full of files. Advancing the branch under them would change the code beneath a person who is possibly mid-edit.

It is hot reloading, except of an entire codebase, with no warning. Git refuses.

The workaround is to move the other repository off that branch:

Bash
cd ../hello-git && git checkout bar && cd ../remote-git git push cd ../hello-git && git checkout trunk && cd ../remote-git

You will essentially never hit this in real life, because you do not normally have two copies of the same repository on one machine, both with working trees. A hosting service faces the same constraint and solves it by keeping repositories without a checked-out working tree at all.

Make a Rebase Conflict

Two changes, in two files, one of which will clash.

Upstream, change the first line of README.md to a + 3, and append a line to the end of bar.md:

Bash
cd ../hello-git git add . git commit -m "a change from hello-git for a rebase conflict"

Locally, change the same first line to a + 4, and change the first line of bar.md:

Bash
cd ../remote-git git add . git commit -m "a change from remote for a rebase conflict"

Different regions of bar.md, same line of README.md. That combination is deliberate.

Bash
git pull --rebase

Read the output before the error. bar.md merged automatically. Different parts of the same file are not a conflict, and git handled it without asking.

README.md could not be merged. git status confirms: bar.md staged and green, README.md marked both modified.

Now Read the Markers

Bash
cat README.md
Plain text
<<<<<<< HEAD a + 3 ======= a + 4 >>>>>>> a change from remote for a rebase conflict

Stop and look at that.

HEAD is a + 3. That is the upstream change. That is not your change.

Your change, a + 4, is on the incoming side.

If you resolved this on the same instincts you built in the last post, you would keep the wrong one.

Why It Flips

Nothing inconsistent is happening. HEAD always means where you are standing, and rebase moved you.

Recall what rebase does:

  1. Check out the target branch
  2. Replay your commits on top, one at a time
  3. Move your branch to the result

You are standing on the target branch. So HEAD is the target. Your commit is the thing arriving, which makes it the incoming side.

A merge on the left where HEAD is your own branch and ours means your change, next to a rebase on the right where git checked out the target first, so HEAD is the upstream change and theirs is your own commit being replayed. ExpandA merge on the left where HEAD is your own branch and ours means your change, next to a rebase on the right where git checked out the target first, so HEAD is the upstream change and theirs is your own commit being replayed.

During a rebase, ours is theirs and theirs is ours.

Do not memorise that sentence. Memorise what rebase does, and the labels follow every time.

I mean that seriously. If you learn it as a fact, you will eventually second-guess yourself: is merge the inverted one, or rebase? Then you flip your own answer and get it backwards. Reasoning from the mechanism never does that, and it is the difference between four years of confusion and thirty seconds of thought.

Resolve It

Keep the upstream change this time, a + 3. Delete the markers and the other line.

Bash
git add README.md git status

README.md disappears from the changes. Same reason as last time: you kept the version that was already there, so there is nothing to record.

Now continue. Note that it is not git commit:

Bash
git rebase --continue

Git offers you the commit message to edit, then finishes. Your branch is one commit ahead of origin/trunk.

Try it here, where the markers are labelled the same way:

Conflicts

A rebase conflict

Rebase feature onto trunk, resolve notes.md, stage it, then run git rebase --continue.

playground feature

Type a command, or click a suggestion below. `help` lists what works.

$
trunk78ab036Rewrite notes on trunkHEAD -> featurea3feaebRewrite notes on feature5bb3488Add notesb9bcc54Add README

What You Just Did to Your Own Work

Bash
git log --oneline

There is the upstream change, and there is your commit. But there is no conflict commit.

Look at what your commit actually contains:

Bash
git log -p -1

The bar.md first-line change is there. a + 4 is not. It does not exist anywhere in the history.

You erased it. Not by accident, exactly: you replayed your commit onto a new base and then edited it during the replay to remove your own change. The result is a commit that no longer contains what you originally wrote.

And nothing records that this happened. A merge would have left a merge commit saying two versions met and one was chosen. A rebase leaves a straight line that looks like the conflict never existed.

The work is not permanently gone. It is in the reflog, and you know how to walk back to it:

Bash
git reflog git log -p <sha>

But you have to know to look, and you have to know something was lost.

This is the best argument against rebase. Not that it is dangerous, not that it is complicated. That it discards information about decisions you made, and the history it leaves is cleaner partly because it is less complete.

If You Commit Instead of Continuing

A common slip during a rebase is typing git commit out of habit. The fix:

Bash
git reset --soft HEAD~1 git rebase --continue

--soft undoes the commit while keeping the changes staged, which puts you back exactly where --continue expects you to be.

What This Sets Up

You have resolved this conflict. You are about to resolve it again. And again.

Next: when the same conflict comes back every time, and the deliberately obscure feature that fixes it.

The Essentials

  1. Push is rejected when the target branch is checked out in the receiving repository.
  2. Different regions of the same file merge cleanly. Only overlapping edits conflict.
  3. During a rebase, HEAD is the branch you rebased onto, not yours.
  4. Ours and theirs are inverted compared to a merge. Reason from what rebase does rather than memorising it.
  5. Resolving in favour of HEAD during a rebase discards your own commit's change.
  6. No conflict commit is left behind, so the history does not record that a decision was made.
  7. Finish with git rebase --continue, and if you committed by mistake, git reset --soft HEAD~1 first.

Further Reading and Watching