The Danger of Rebase in Git — 7 Ways It Can Go Wrong (and How to Recover)
Git rebase is one of the most useful commands in Git — and one of the easiest to get badly wrong. It gives you a clean, linear history, but it gets there by rewriting history, and rewritten history behaves in ways that surprise even experienced developers: commits show up twice, a teammate's work disappears after a push, or a conflict is resolved the wrong way with no trace of what happened. This guide walks through the seven real dangers of rebase, what causes each one, how to recover when it happens, and a checklist for rebasing safely.
Why Rebase Is Dangerous: It Copies Commits, It Doesn't Move Them
Every danger in this article comes from one fact. When you run git rebase main, Git does not pick up your commits and move them. A commit's hash is calculated from its contents, and its contents include the hash of its parent — so a commit can never be given a new parent. Instead, Git creates a brand-new copy of each of your commits on top of main, then moves your branch label to the last copy.
Rebase creates F1′ and F2′ as new commits with new hashes. The original F1 and F2 still exist, but no branch points at them any more.
On your own machine, that's harmless — the old F1 and F2 are simply forgotten. The trouble starts when someone else still has the originals, or when the copies turn out not to be quite the same as the originals. Each danger below is a variation of this picture.
Danger 1: Rebasing a Shared Branch Duplicates Commits
Alice and Bob both work on feature, which contains F1 and F2. Alice rebases it onto the latest main and force-pushes. Meanwhile, Bob has committed B1 on top of his copy of F2.
If Bob's Git is set to merge when pulling (pull.rebase=false, a common setting), running git pull sees two unrelated lines of history — Alice's F1′ and F2′ on the remote, and his own F1, F2, and B1 — and does what it always does with diverged branches: it merges them.
Bob's branch now contains both the originals and the rebased copies. If he pushes, the duplicates land on the shared branch for everyone.
Git does not treat F1 and F1′ as the same commit. They have different hashes, so to Git they are simply two commits that happen to make similar changes. The log now shows every feature change twice, any difference between an original and its copy can turn into a conflict, and once Bob pushes, the duplicates spread to everyone else on the team.
Danger 2: A Force Push Can Silently Erase a Teammate's Work
After a rebase, your local branch and the remote branch have diverged, so a normal git push is rejected. The usual response is git push --force, which tells the remote to replace its branch with yours no matter what is on it.
If Bob pushed B1 to feature while you were rebasing, your force push throws it away. The remote branch now matches your rebased history exactly, and B1 is no longer on it. Git doesn't warn you, and Bob won't find out until he next pulls.
git push --force replaced the remote branch with your rebased history. B1 still exists on Bob's machine, but it is gone from the remote.
Use --force-with-lease, and add --force-if-includes
--force-with-lease adds a safety check: it only overwrites the remote branch if it still points where your last fetch saw it. If someone pushed in the meantime, the push is rejected with stale info, so you can fetch and look before trying again.
# Dangerous: overwrites the remote branch unconditionally git push --force # Safer: refuses if the remote branch moved since your last fetch git push --force-with-lease # Safest: also refuses if you fetched new commits but never integrated them (Git 2.30+) git push --force-with-lease --force-if-includes
--force-with-lease now believes you have seen Bob's B1 — even though it never made it into your branch — and lets the push overwrite it. --force-if-includes closes that gap by also checking that the remote's latest commit was actually integrated into your local branch. Make it the default for every lease push with git config --global push.useForceIfIncludes true.Danger 3: Conflicts Are Confusing — and Their Resolutions Leave No Trace
A merge stops for conflicts at most once. A rebase can stop once per replayed commit, and you may resolve the same area of code several times over as each commit is replayed. Three things make this riskier than it looks.
"Ours" and "theirs" are swapped
During a rebase, Git builds the new history by checking out the branch you are rebasing onto and replaying your commits on top of it. So from Git's point of view, the upstream branch is "ours" and your own commit is "theirs" — the opposite of what most people expect.
| Side of the conflict | During git merge feature (on main) | During git rebase main (on feature) |
|---|---|---|
ours · <<<<<<< side · Current changes | main — the branch you are on | main, plus your commits already replayed |
theirs · >>>>>>> side · Incoming changes | feature — the branch being merged in | Your commit that is being replayed |
git checkout --ours file | Keeps your branch's version | Keeps main's version and discards your change |
LithiumGit's conflict editor labels the two sides by Git's conflict markers: Current changes is the <<<<<<< side and Incoming changes is the >>>>>>> side. During a rebase, that means Current is the branch you are rebasing onto and Incoming is your own commit. Click Accept Current Change out of habit and you keep their code and throw away yours. To help you keep your bearings, LithiumGit shows the message of the commit being replayed right above the rebase's Continue, Skip, and Abort buttons.
A bad resolution is invisible
When you resolve a conflict during a merge, the resolution is recorded in the merge commit, where reviewers can see it and git show can display it. During a rebase, your resolution is folded silently into the rewritten commit. If you accidentally delete a line while resolving, the history simply says you wrote the code that way — nothing records that a conflict ever happened.
"Skip" drops the whole commit
git rebase --skip — and the Skip button in LithiumGit, which runs it — throws away the entire commit being replayed, conflicting and non-conflicting changes alike. Use it only when the commit is genuinely no longer needed, for example because the same change already landed on main.Danger 4: Rebased Commits Were Never Tested
Each original commit was written, and hopefully tested, against the old base. The rebased copies sit on a base they have never been run against. A rebase can finish without a single conflict and still leave broken commits behind.
A typical example: someone on main renames getUser() to fetchUser() and updates every call site. Your F1 adds a new call to getUser() in a different file. The two changes touch different lines, so the rebase applies cleanly — and F1′ doesn't compile. You notice at the end, add a "fix build after rebase" commit, and push. The branch tip works, but F1′ and F2′ stay broken forever, which makes git bisect unreliable for anyone who later hunts a bug through those commits.
# Run your tests after every replayed commit; the rebase stops at the first failure git rebase --exec "npm test" main
When a test fails, the rebase pauses on that commit, so you can fix it right there with git commit --amend and run git rebase --continue — instead of patching it with an extra commit at the end.
Danger 5: Merge Commits Quietly Disappear
By default, git rebase flattens your branch. If it contains merge commits — say you merged a colleague's branch into yours last week — rebase drops those merge commits and replays everything as one straight line. The branch's structure is gone, and conflicts you already resolved in those merges can come back, this time one commit at a time.
# Keep merge commits and the branch's shape while rebasing git rebase --rebase-merges main
--rebase-merges recreates each merge rather than copying it, so a conflict you resolved by hand inside a merge commit still has to be resolved again.
Danger 6: Interactive Rebase Can Delete Commits Without Asking
Interactive rebase (git rebase -i) opens a to-do list of commits for you to reorder, squash, edit, or drop. It's a powerful cleanup tool, and it takes the list literally: delete a line and that commit is gone, with no confirmation and no warning. Similarly, a fixup that you meant to be a squash keeps the changes but silently discards that commit's message.
# git rebase -i HEAD~3 opens a to-do list like this: pick 5c6b7a8 Add login form pick 1f2e3d4 Add login form validation pick 9a8b7c6 Add password reset email # Deleting a line drops that commit. Make Git refuse instead, # so dropping a commit requires an explicit "drop" command: git config --global rebase.missingCommitsCheck error
Danger 7: Branches Built on Top Get Left Behind
Suppose feature-b was branched from feature-a so you could keep working while feature-a is in review. If you rebase feature-a onto main, feature-b still points at the original feature-a commits — the ones you just replaced. Rebase feature-b later and Git can end up replaying those old commits as well, which leads to duplicate commits or conflicts with their own copies.
# From the top branch, rebase the whole stack and move every branch in it (Git 2.38+) git checkout feature-b git rebase --update-refs main # Or make that the default for every rebase git config --global rebase.updateRefs true
How to Recover From a Bad Rebase
The good news: a rebase almost never destroys committed work on your own machine. The originals are still in your repository — you just need to know where to look.
While the rebase is still running
# Stop, and put everything back exactly as it was before the rebase started git rebase --abort
In LithiumGit, a paused rebase shows the commit being replayed with Continue, Skip, and Abort buttons in the changes view. Abort runs the same command. Continue is blocked until every conflicted file is resolved, so you can't accidentally carry on with conflict markers still in the code.
Right after it finished
Before it starts, rebase saves the commit your branch pointed at in ORIG_HEAD. If you realize straight away that something went wrong, jump back to it:
# Return the branch to where it was before the rebase git reset --hard ORIG_HEAD
ORIG_HEAD is overwritten by the next reset, merge, pull, or rebase. And reset --hard discards uncommitted changes, so check what reset actually throws away before running it.Days later: use the reflog
Your reflog records every position your branch has pointed at. A whole rebase is recorded in the branch's reflog as a single entry, so the commit just before it is the tip from before the rebase:
git reflog show feature c0c9e56 feature@{0}: rebase (finish): refs/heads/feature onto 665e498769bacc2e62b3b38c3f628d06851c3307 cc76475 feature@{1}: commit: Add login form validation a137658 feature@{2}: commit: Add login form # Look at the old tip without touching anything git log --oneline feature@{1} # Keep it safe on its own branch... git branch feature-before-rebase feature@{1} # ...or move feature back to it git reset --hard feature@{1}
Rebased-away commits are kept for about 30 days by default (gc.reflogExpireUnreachable) before Git's garbage collection may delete them. And the reflog is local: it only knows about commits that were on your machine. If a force push erased a teammate's commit, that commit lives in their clone and their reflog, not yours — so they are the one who can push it back.
When someone else rebased a branch you were working on
If you are Bob from Danger 1, don't merge the rewritten branch into your old copy. Fetch, then move only your commits onto the new history:
git fetch origin # Replay only the commits made after the old origin/feature onto the new one git rebase --onto origin/feature origin/feature@{1}
Here origin/feature@{1} means "where origin/feature pointed before that fetch" — the old F2. Everything after it, B1, is replayed onto F2′, and the old F1 and F2 are left behind. In many cases git pull --rebase works this out on its own, but check the result with git log --graph before you push.
The 7 Dangers of Rebase at a Glance
| Danger | What goes wrong | Safeguard |
|---|---|---|
| Rebasing a shared branch | Teammates end up with every commit twice | Only rebase commits nobody else has |
| Force pushing | A teammate's pushed commits vanish from the remote | --force-with-lease --force-if-includes |
| Resolving conflicts | Ours and theirs swap; resolutions leave no trace | Read the sides carefully; review with git range-diff |
| Untested copies | Intermediate commits no longer build | git rebase --exec "npm test" |
| Merge commits | The branch is flattened and merge resolutions are lost | git rebase --rebase-merges |
| Interactive rebase | A deleted line silently drops a commit | rebase.missingCommitsCheck error |
| Stacked branches | Dependent branches still point at the old commits | git rebase --update-refs |
A Safe Rebase Checklist
- Make sure the commits are yours alone — not pushed yet, or pushed only to a branch nobody else uses.
- Start from a clean working tree — commit or stash anything in progress.
- Create a backup branch, so getting back is one command rather than a reflog hunt.
- Rebase with tests if you can, so every replayed commit is checked.
- Compare old and new with
git range-diff, which pairs each original commit with its copy and shows only what changed between them — exactly where a conflict-resolution mistake shows up. - Push with a lease, never a bare
--force. - Tell your team if the branch was shared after all, before they pull.
- Delete the backup once you are happy with the result.
git status git branch backup/feature git rebase --exec "npm test" main git range-diff main backup/feature feature git push --force-with-lease --force-if-includes git branch -D backup/feature
One-time setup that makes every rebase safer
# --force-with-lease also checks that you integrated what you fetched git config --global push.useForceIfIncludes true # Refuse to silently drop a commit deleted from the interactive to-do list git config --global rebase.missingCommitsCheck error # Move stacked branches along with the commits they point at git config --global rebase.updateRefs true # Remember how you resolved a conflict and reapply it if the same conflict comes back, # for example when you abort a rebase and start it again git config --global rerere.enabled true
Frequently Asked Questions
- Is git rebase dangerous?
- Rebase is safe on commits that exist only on your machine. It becomes dangerous on commits other people already have, because rebase does not move commits — it replaces them with new copies that have new hashes. Anyone still holding the originals ends up with diverged history, duplicate commits, or, after a force push, lost work.
- Can I undo a git rebase?
- Yes. While a rebase is still in progress, git rebase --abort puts everything back the way it was before it started. Right after a rebase finishes, git reset --hard ORIG_HEAD moves the branch back. Later on, find the pre-rebase commit with git reflog show <branch> — it is usually <branch>@{1} — and reset to it. Rebased-away commits are kept for about 30 days by default.
- Why does rebasing create duplicate commits?
- Rebased commits are new commits with new hashes, even when their changes are identical. If a teammate still has the original commits and merges the rebased branch into their copy, Git keeps both versions, so every change appears twice in the history.
- What is the difference between git push --force and --force-with-lease?
- git push --force overwrites the remote branch no matter what is on it, which can delete commits other people pushed. git push --force-with-lease only overwrites the branch if it still points where your last fetch saw it, and rejects the push otherwise. Adding --force-if-includes also protects you when a background fetch updated your remote-tracking branch without you integrating the new commits.
- Why are ours and theirs swapped during a git rebase?
- Rebase works by checking out the branch you are rebasing onto and replaying your commits on top of it one by one. So during a rebase, "ours" (Current changes) is the upstream branch plus any commits already replayed, and "theirs" (Incoming changes) is your own commit being replayed — the opposite of a merge.
- What does git rebase --skip do?
- It drops the commit currently being replayed entirely, not just its conflicting parts. Only use it when that commit's changes are no longer needed, for example because the same change already exists on the branch you are rebasing onto.
- Does git rebase change the commit author or date?
- Rebase keeps each commit's original author and author date, but the rebased copies get you as the committer, the current time as the commit date, and new hashes. Commit signatures are not carried over, so signed commits have to be re-signed — with your key — using git rebase --gpg-sign.