We're sorry but this app doesn't work properly without JavaScript enabled. Please enable it to continue.

Git Rebase Conflicts: How to Resolve Them Without Fear

ThePrimeagen
ThePrimeagenEx-Netflix engineer, NeoVim ricer, and Git rebaser

Last published

Table of Contents

Full disclosure: a lot of git rebase's bad rep comes from conflicts! Fear not. It's nothing you can't handle with just a smidge of practice.

Rebasing feels scarier than merging because it rewrites Git history, which means if you're not careful, you can lose work in an unrecoverable way. But as long as you understand what's going on, rebase will make your (and your team's) Git history cleaner and easier to understand.

All the content from our Boot.dev courses are available for free here on the blog. This one is the "Rebase Conflicts" chapter of Learn Git 2. If you want to try the far more immersive version of the course, do check it out!

How Do Rebase Conflicts Happen?

Quick refresher: git rebase replays your branch's commits on top of another branch, instead of tying the branches together with a merge commit like git merge does. If that's news to you, read Git Rebase first.

A conflict happens when both branches change the same lines in the same file, and Git can't decide which version to keep. In the "real world," what happens most often is:

  1. You switch to a new branch, say fix_bug, which is a copy of main.
  2. While you're fixing the bug, someone else merges their changes into main.
  3. You fix the bug, and it so happens that you edited the same files (and lines) that the other person did.
  4. You open a Pull Request to merge (or rebase) fix_bug into main, then Git tells you there's a conflict.
  5. You resolve the conflict on your branch.
  6. You complete the Pull Request with the conflict resolved.

Say your company tracks its customer lists in a Git repo, and you're on a branch called banned where you committed a new file: customers/banned.csv, the list of banned customers. Meanwhile, someone committed a different version of the same file to main. Rebasing banned onto main stops you dead in your tracks:

git switch banned
git rebase main
# CONFLICT (add/add): Merge conflict in customers/banned.csv

Git pauses the rebase mid-flight and waits for you to sort it out. The fix works almost the same way as resolving a merge conflict, but there are a few rebase-specific gotchas that trip up nearly everyone.

Rebase Leaves You in a Detached HEAD State

The first time you hit a rebase conflict, you might notice something... odd. The HEAD pointer contains main's changes rather than banned's! That's because rebase checks out the branch you're rebasing onto, in this case main, so it can replay the changes from banned on top of it.

If you had merged main instead, HEAD would still point to banned because Git doesn't switch branches under the hood during a merge.

Run git branch in the middle of the rebase and you'll notice that you're not on a branch:

* (no branch, rebasing banned)
  banned
  main

This temporary "detached HEAD" state is normal: it exists so you can resolve the conflict before the rebase continues. If branches themselves are still fuzzy, brush up with Git Branching (or take the full Learn Git course).

Ours vs Theirs Is Backwards in a Rebase

The same git checkout --theirs and git checkout --ours commands you use with merge conflicts work during a rebase... but you kinda need to think in reverse. In a merge, --ours refers to your current branch. In a rebase, --ours refers to the branch you're rebasing onto, and --theirs represents your branch, which in reality is usually "our" changes.

Command During a merge During a rebase
git checkout --ours your branch the branch you're rebasing onto (e.g. main)
git checkout --theirs the other branch your branch (e.g. banned)

Your editor probably has UI features to help you resolve the conflict too. For example, in VS Code you'll see something like this:

"Accept incoming change" is the same as git checkout --theirs, and "Accept current change" is the same as git checkout --ours.

How Do You Resolve a Rebase Conflict?

Pick the version you want (or hand-edit the file into a combination of both), stage it with git add, and then... here's the thing: with rebase conflicts, unlike merge conflicts, you don't commit to resolve the conflict. Instead, you --continue the rebase:

git checkout --ours customers/banned.csv
git add customers/banned.csv
git rebase --continue

Git reminds you of your options every time it hits a conflict:

hint: Resolve all conflicts manually, mark them as resolved with
hint: "git add/rm <conflicted_files>", then run "git rebase --continue".
hint: You can instead skip this commit: run "git rebase --skip".
hint: To abort and get back to the state before "git rebase", run "git rebase --abort".

If a conflict makes you regret every decision that led you to this point, git rebase --abort puts everything back exactly the way it was before you started. No harm done.

Why Did My Commit Disappear After the Rebase?

Our banned branch had exactly one commit — the one that added customers/banned.csv — and we resolved the conflict by taking main's version of the file (--ours). After git rebase --continue, run git log and... that commit is just gone!?!

Well, let's think about what happened:

  1. We started a rebase of banned onto main, meaning we're rewriting the history of banned to include all the changes from main.
  2. We effectively removed all the changes from banned's commit by choosing to keep the changes from main instead.
  3. We continued the rebase, and Git realized that the commit was pointless, and because we're rewriting history anyway, it just removed it.

If our changes had been more complicated, say we had kept some of the changes from the commit and overwritten others, then Git would have kept it in the history.

And since we discarded everything from banned, the branch is now identical to main. Much ado about nothing, right? It's safe to delete with git branch -d banned. If a rewritten history ever makes you think you actually lost work, the reflog has your back: Git keeps orphaned commits around for a while before garbage collecting them.

Stop Resolving the Same Conflict Twice With git rerere

A common complaint about rebase is that there are times when you may have to manually resolve the same conflicts over and over again. This is especially prevalent when you have a long-running feature branch, or even more likely, multiple feature branches that are being rebased onto main.

The git rerere functionality is a bit of a hidden feature. The name stands for "reuse recorded resolution" and, as the name implies, it allows you to ask Git to remember how you've resolved a hunk conflict so that the next time it sees the same conflict, Git can resolve it for you automatically.

In other words, if enabled, rerere will remember how you resolved a conflict (applies to rebasing but also merging) and will automatically apply that resolution the next time it sees the same conflict. Pretty neat, right?

Enable it with a config setting:

git config set --local rerere.enabled true

Say you have a feature branch called favs that adds a new file, customers/favs.md, plus a second branch favs2 created from favs (so it contains the same commit). Meanwhile, main got its own conflicting version of customers/favs.md. Rebase favs onto main and look closely at the output:

CONFLICT (add/add): Merge conflict in customers/favs.md
Recorded preimage for 'customers/favs.md'

Notice how it's recording what we're doing as we resolve the conflict? Resolve it, stage it, and git rebase --continue like normal:

Recorded resolution for 'customers/favs.md'.
Successfully rebased and updated refs/heads/favs.

Git remembered how you resolved the conflict! Now rebase favs2 onto main. It contains the same commit as favs did, so it hits the exact same conflict... and Git resolves it for you automatically:

Resolved 'customers/favs.md' using previous resolution.

You still stage the file and --continue the rebase, but the manual editing is done for you. If rerere ever records a resolution you regret, clear its cache with rm -rf .git/rr-cache from the root of your repo.

What If You Accidentally Commit During a Rebase?

You know how with merge conflicts you commit the resolution, but with rebase conflicts you --continue the resolution?

I cannot tell you how many times I have accidentally committed the resolution of a rebase conflict... If you do, just:

git reset --soft HEAD~1

The --soft flag will keep your changes, and just undo the commit. Then you can simply --continue the rebase as normal. If the difference between soft and hard resets is fuzzy, read Git Reset before you reach for this one in anger.

Once conflicts don't scare you anymore, the next history-rewriting trick worth learning is squashing commits with interactive rebase, and Learn Git 2 covers all of it hands-on.

Frequently Asked Questions

Do you commit after resolving a rebase conflict?

No. Unlike merge conflicts, you stage the resolved files with git add and then run git rebase --continue. Committing manually is a common mistake you can undo with git reset --soft HEAD~1.

Why are ours and theirs reversed during a git rebase?

Rebase checks out the branch you're rebasing onto and replays your commits on top of it. So --ours refers to the base branch (like main) and --theirs refers to your own branch.

How do you abort a git rebase?

Run git rebase --abort. It returns your branch to the exact state it was in before the rebase started.

What is git rerere?

It stands for reuse recorded resolution. When enabled, Git records how you resolve a conflict and automatically applies the same resolution the next time it sees the identical conflict.

Why did a commit disappear after my rebase?

If your conflict resolution discards all of a commit's changes, Git drops the now-empty commit while rewriting history. If you keep even some of its changes, the commit survives.

Related Articles