When Not to Rebase in Git
The traditional rule is “never rebase public history”. It is a good default and the right thing to learn first.
It is also not quite the rule. The actual question is narrower and more useful: is anyone depending on these commits? A branch you pushed as a backup that nobody has pulled is public by the strict definition and entirely safe to rebase. A branch three colleagues have based work on is not, even if it never left your team.
What actually goes wrong
Section titled “What actually goes wrong”Rebasing replaces every commit in the range with a new object. If someone else has the originals:
Their branch and yours share no history. Git sees two unrelated lines of development that happen to have the same name.
Their git pull makes it worse. Pull fetches your rewritten branch and merges it with their old copy,
producing a history containing both versions of every commit. The changes appear twice, often with
conflicts.
Work based on your commits is orphaned. A branch created from your commits still points at objects that are no longer on your branch.
Review comments lose their anchors. Comments attached to lines of specific commits detach when those commits cease to exist.
None of this destroys data. All of it costs other people time, and all of it is avoidable.
The clear cases
Section titled “The clear cases”Shared branches with active collaborators
Section titled “Shared branches with active collaborators”If two or more people are committing to a branch, nobody should rebase it. There is no version of this that ends well; use merge.
main, and any long-lived integration branch
Section titled “main, and any long-lived integration branch”Rewriting a mainline means every clone in existence is inconsistent with it. Everyone must reset — and anyone who pulls instead will reintroduce the old commits.
This is an incident, not a cleanup. For anything already on main, the tool is git revert.
Release and maintenance branches
Section titled “Release and maintenance branches”Released history is a record of what was shipped. Rewriting it means the commits corresponding to a tagged release no longer exist, which breaks the connection between tags and history.
If a release branch has a problem, fix it forward: commit a fix and cut a new patch release.
Anything a tag points to
Section titled “Anything a tag points to”A tag names a specific commit. Rebase past it and the tag points at an object no longer reachable from the branch. Tags are meant to be permanent markers; rewriting underneath them defeats the purpose.
Audit-sensitive contexts
Section titled “Audit-sensitive contexts”Where you must be able to demonstrate what changed, when, and who approved it, a history that can be rewritten undermines the claim. Some organisations disable force pushes on protected branches specifically for this reason.
Signed commits interact here too: rebasing produces new commits, and the signatures do not carry across unless the rebase re-signs them. Signed Commits covers this.
When the team has not agreed
Section titled “When the team has not agreed”If half the team rebases and half merges, the result is confusion and a history nobody can read. This is a policy question, not a technical one — but rebasing a branch on a team that does not expect it is a practical mistake regardless of the merits.
The judgement cases
Section titled “The judgement cases”These are where the strict rule is too blunt.
Pushed, but nobody has pulled it
Section titled “Pushed, but nobody has pulled it”A branch pushed for backup, or opened as a draft nobody has looked at, is technically public. Rebasing it
is fine — the only copy that matters is yours and the remote’s, and --force-with-lease handles the
remote.
The risk is being wrong about whether anyone has it. On a small team, ask. On a large one, assume someone has.
Under review
Section titled “Under review”Rebasing during review invalidates comment anchors and forces reviewers to re-orient. It also re-runs CI from scratch.
The convention that works: add commits during review, reshape before or after. If a reviewer explicitly asks you to clean up the history before merge, do it after approval.
Someone has branched from your branch
Section titled “Someone has branched from your branch”Stacked branches are the case people forget. Rebasing the parent orphans the child’s base, and the child’s owner must re-parent it:
git rebase --onto feature-a-new feature-a-old feature-bThat is a manageable operation — provided they know it happened. If you must rebase a branch someone has
stacked on, tell them, and consider using --update-refs to move their local branch for them if it is in
your repository.
Long-lived branches with heavy conflicts
Section titled “Long-lived branches with heavy conflicts”Technically safe if the branch is private, but rebasing replays commits one at a time, so a genuine overlap can mean resolving the same conflict repeatedly.
A merge resolves it once. On a branch with twenty commits and a real conflict, merging is often simply the better tool even where rebasing is permitted.
Force pushing, honestly
Section titled “Force pushing, honestly”Rebasing a pushed branch requires a force push, and the safety of that operation is often overstated.
git push --force-with-leaseThe lease compares the remote’s position against your remote-tracking ref — your local record of where the remote was at your last fetch. If they differ, the push is refused.
Many teams disable force pushes on protected branches entirely, which is a reasonable default: it makes the rule structural rather than something people have to remember at the wrong moment.
Making the rule structural
Section titled “Making the rule structural”Relying on everyone remembering the rule at the wrong moment does not scale. Three controls make it enforceable.
Disable force pushes on protected branches. Hosting platforms can reject any non-fast-forward push to
named branches. This makes rewriting main or a release branch impossible rather than merely discouraged,
and it is the single most valuable setting here.
Require linear history if that is what you want. This rejects merge commits, which pushes every integration towards squash or rebase at merge time — done by the platform, on the server, without anyone force-pushing anything.
Protect tags. Some platforms allow rules preventing tags from being moved or deleted. A tag that can be repointed is not the permanent marker a release process assumes it is.
Locally, a pre-push hook can refuse force pushes to specific branches as a backstop:
#!/bin/sh# .githooks/pre-push — refuse force pushes to protected brancheswhile read -r _local_ref _local_sha remote_ref _remote_sha; do case "$remote_ref" in refs/heads/main|refs/heads/release/*) if [ "$GIT_PUSH_OPTION_COUNT" ]; then :; fi echo "pre-push: refusing to push directly to $remote_ref" >&2 exit 1 ;; esacdoneWhat to do instead
Section titled “What to do instead”| Situation | Instead of rebasing |
|---|---|
| Branch is shared | git merge main into the branch |
Change is on main | git revert |
Want a linear main | Squash or rebase at merge time, via the platform |
| Want to tidy commits, review in progress | Wait until after approval |
| Want to tidy a shared branch | Rebase a copy, review it, then replace deliberately with everyone informed |
| Branch has many conflicts | Merge — resolve once |
| Fixing a mistake in released history | Fix forward with a new commit and a new tag |
The pattern throughout: rewriting is for history only you have; everything else moves forward.
Recovering when it has already happened
Section titled “Recovering when it has already happened”If a branch you have was rebased out from under you, and you have no local commits on it:
git fetch origingit reset --hard origin/feature-branchThis discards your local copy in favour of the rewritten one. Safe only if you have nothing unpushed.
If you do have local commits on the old base, re-parent them instead of resetting:
git fetch origingit rebase --onto origin/feature-branch <old-branch-tip> HEADRead as: replay my commits — those after <old-branch-tip> — onto the new version of the branch. Find the
old tip in your reflog:
git reflog show feature-branchCommon mistakes
Section titled “Common mistakes”Treating “never rebase public history” as absolute. It is a good default; the real test is whether anyone depends on the commits.
Assuming --force-with-lease is always safe. Fetching first defeats it. Add --force-if-includes.
Rebasing a branch with a stacked child without telling its owner.
Rebasing during review and expecting comments to survive.
Force-pushing to main to “clean up”. Everyone must re-clone; the cost is enormous relative to a tidier
log.
Pulling after someone force-pushed. Reset or re-parent instead.
Rebasing to remove a secret. Rotate the credential. Rewriting is cleanup, and it does not reach clones, forks or logs.
Rebasing a long branch with many conflicts because rebasing is “cleaner”. Merging resolves once.
Mental Model
Section titled “Mental Model”Rebasing is editing a document only you have a copy of.
While that is true, edit freely. The moment someone else has a copy, editing yours does not change theirs — it just means the two no longer match, and reconciling them is now their problem.
Reverting is publishing a correction. Everyone gets it automatically, nobody’s copy breaks, and the record shows both the original and the correction.
What You Learned
Section titled “What You Learned”- The real test is whether anyone depends on the commits, not whether they have been pushed.
- Never rewrite
main, release branches, maintenance branches, or anything a tag points at. - Rebasing during review invalidates comment anchors and re-runs CI.
- Stacked branches break when their parent is rebased;
--ontore-parents them. --force-with-leasechecks your local record of the remote, so fetching first defeats it.--force-if-includesadds the stronger check.- If someone force-pushes a branch you have, reset or re-parent — do not pull.
- For anything already shared,
git revertis the correct tool.
Try It Yourself
Section titled “Try It Yourself”Simulate the shared-branch problem safely, using two clones of a local repository.
-
Create a “server” and two clones:
Terminal window git init --bare ~/tmp/origin.gitgit clone ~/tmp/origin.git alice && git clone ~/tmp/origin.git bob -
Alice creates a branch and pushes it:
Terminal window cd aliceecho one > f.txt && git add . && git commit -m "First"git push -u origin maingit switch -c shared && echo two >> f.txt && git commit -am "Second"git push -u origin shared -
Bob pulls it:
Terminal window cd ../bob && git fetch && git switch sharedgit log --oneline -
Alice rebases and force-pushes:
Terminal window cd ../alicegit commit --amend -m "Second (reworded)"git push --force-with-lease -
Bob pulls — the wrong move. Predict the result first.
Terminal window cd ../bob && git pullgit log --oneline -
Observe that Bob’s history now contains both versions of the commit.
-
Recover properly:
Terminal window git reset --hard origin/sharedgit log --oneline
Step 6 is the whole lesson made concrete. Seeing the duplicated commit once makes the rule stick far better than reading about it.
Next Cluster
Section titled “Next Cluster”That completes rebasing. The final cluster covers the Git features that make everyday work faster — worktrees, sparse checkout, partial clone, hooks, configuration and signing.