
We Ran git rebase on a Shared Branch and Lost Three Days of Work
It was a Thursday afternoon. One git rebase followed by git push --force wiped three days of work across four developers. Here's exactly what happened, how we recovered every commit, and the rules we put in place so it never happens again.
It was a Thursday afternoon. Developer 1 had been working on a feature for four days. Clean commits, good code, reviewed and approved. He was about to open the final PR when he ran one command.
git rebase mainThen git push --force.
By the time anyone realized what had happened, four developers had lost their local branches. Two PRs were silently broken. One developer's two days of work had simply vanished from the remote. The CI pipeline was green — because it was running against old commits.
We spent three days untangling it. Here's exactly what happened, why it happens, and how we recovered — so you don't have to learn this the same way we did.
What git rebase Actually Does to History
Most explanations of rebase start with diagrams. Let's start with the thing that matters: rebase rewrites commits. It doesn't move them. It creates brand new commits with new hashes that contain the same changes.
Before rebase, your feature branch's commits have specific SHA hashes. After you rebase onto main, those same commits become completely different hashes. Different objects in Git's database. As far as Git is concerned, those are completely different commits.
# Before rebase
git log --oneline feature/payments
# d4e5f6 Add payment validation
# a1b2c3 Add payment model
# 9g8h7i Initial setup (base commit on main)
# After git rebase main
git log --oneline feature/payments
# m1n2o3 Add payment validation ← new hash, same change
# x7y8z9 Add payment model ← new hash, same change
# 3k4l5m Latest commit on mainThis is fine when the branch only exists on your machine. The problem starts the moment that branch lives on the remote and someone else has based work on it.
How the Disaster Unfolded
Here's what our team's branch situation looked like that Thursday:
main
└── feature/payments ← Developer 1's branch, pushed to remote 4 days ago
└── feature/payments-ui ← Developer 2's branch, based off Developer 1'sDeveloper 2 had been building the UI layer on top of Developer 1's branch for two days. Her local branch pointed to Developer 1's old commits. Then Developer 1 rebased and force-pushed.
The remote feature/payments now had completely different commits. Developer 2's branch still pointed to the old ones — which now existed nowhere except her local machine and Git's reflog.
When Developer 2 ran git pull, Git saw her branch had diverged from remote. She assumed it was a normal conflict. She merged. Git dutifully merged two versions of the same code — the original and the rebased copy — creating a mess of duplicate changes, phantom conflicts, and commits that referenced parents that no longer existed on the remote.
The Error That Should Have Stopped It
The actual signal was there. When Developer 1 tried to push after rebasing, Git rejected it:
git push origin feature/payments
# error: failed to push some refs to 'origin/github.com/team/repo'
# hint: Updates were rejected because the tip of your current branch is behind
# hint: its remote counterpart.Git was saying: the remote has commits you don't have. Your histories have diverged. Stop.
Instead of reading this as a warning, Developer 1 added --force to make it go away.
git push --force origin feature/payments
# Everything up to date. ← the most dangerous success message in GitHow We Recovered
Recovery took three steps. The order matters — don't skip step one.
Step 1: Stop everyone from pulling or pushing immediately
The moment you realize a shared branch has been force-pushed, message the team. Anyone who pulls will compound the damage. Anyone who pushes will overwrite the recovery.
# Message the team immediately:
# "Do NOT pull feature/payments. Do NOT push anything to it.
# Stay on your current branch. We're recovering."Step 2: Find the lost commits with git reflog
git reflog is Git's black box recorder. It tracks every position HEAD has been at, including commits that are no longer reachable from any branch. On Developer 1's machine — the original commits still existed in reflog.
git reflog
# m1n2o3 HEAD@{0}: rebase finished: returning to refs/heads/feature/payments
# x7y8z9 HEAD@{1}: rebase: Add payment model
# d4e5f6 HEAD@{2}: commit: Add payment validation ← original commit
# a1b2c3 HEAD@{3}: commit: Add payment model ← original commit
# 9g8h7i HEAD@{4}: checkout: moving from main to feature/payments
# Create a recovery branch at the last good state
git checkout -b feature/payments-recovered d4e5f6Step 3: Restore the remote branch from the recovery point
# Force-push the recovered branch back — using --force-with-lease, not --force
git push --force-with-lease origin feature/payments-recovered:feature/payments
# Each affected developer resets their local branch
git fetch origin
git checkout feature/payments
git reset --hard origin/feature/payments
# For Developer 2's branch that was derived from the broken state:
git rebase --onto origin/feature/payments <bad-merge-commit> feature/payments-uiThe Actual Fix: Never Let This Happen Again
The root cause wasn't Developer 1 making a mistake. The root cause was that the repo allowed it.
Fix 1: Use --force-with-lease instead of --force — always
# Never — overwrites remote with no checks
git push --force origin feature/branch
# Always — checks if remote changed since your last fetch
git push --force-with-lease origin feature/branchFix 2: Enable branch protection on shared branches
In GitHub, go to Settings → Branches → Branch protection rules. For any branch that more than one developer works from, enable: Require pull request reviews, Require status checks, and — the one that prevents this entire incident — Restrict force pushes.
Fix 3: The golden rule of rebase
# The rule:
# Only rebase branches that exist ONLY on your machine.
# Once a branch is pushed and someone else might have it — merge, don't rebase.
# If you want a clean linear history, use squash merge when closing the PR.
# That gives you one clean commit on main without rewriting anyone's history.Fix 4: Enable git rerere for teams doing a lot of rebasing
# rerere = Reuse Recorded Resolution
# Caches how you resolved a conflict — auto-resolves the same conflict next time
git config --global rerere.enabled trueThe Takeaway
git rebase is not dangerous. Force-pushing a rebased branch to a remote that others are working from is dangerous. The commands are the same. The context is what changes everything.
The rule is simple: if your branch exists only locally, rebase freely. The moment it's pushed - especially if someone else might have pulled it - treat it as immutable. Merge onto it. Squash when you close the PR.
And if it's already happened: breathe, message the team to stop, open reflog, and work backwards. The commits are almost certainly still there.
Git doesn't delete things. It just stops showing them to you.
Was this article helpful?
Let me know if this was useful — it helps me write more content like this.
Related Articles
You might also enjoy these
How to Create and Publish Your First npm Package (and Use It in Angular)
This guide explains how to convert a TypeScript utility into a reusable npm package and use it in an Angular app. It covers project setup, configuration, publishing, and how to install, import, and run it in Angular, including common issues and solutions.
Environment Variables You're Leaking to the Frontend Without Knowing It
You added NEXT_PUBLIC_ to your API key 'just to test something quickly.' That was six months ago. It's still there. Here's what's actually leaking — and how to stop it.
Stay in the loop
Get articles on technology, health, and lifestyle delivered to your inbox.
No spam — unsubscribe anytime.
