What git rebase Actually Does to a Shared Branch
Everyone has heard the rule: never rebase a shared branch. Fewer people have watched what actually breaks when you do. I ran the whole thing in two throwaway clones to see the damage with my own eyes, hashes and all.
The setup
Two clones of one repo. A branch called feature has one commit. A teammate has pulled it and added a commit of their own on top. Meanwhile main moved forward with a version bump.
Before the rebase, the branch looked like this:
ba88a11 feature: add retry helper
ce2a214 base commit
The teammate's copy had one more commit sitting on ba88a11:
8e44ceb teammate: fix comment typo
ba88a11 feature: add retry helper
ce2a214 base commit
The rebase
I checked out feature and ran git rebase origin/main. One commit to replay, clean apply, no conflict. Then I looked at the log:
cdd32d5 feature: add retry helper
6fecb31 main: bump version
ce2a214 base commit
Same message, same code, different commit. ba88a11 became cdd32d5.
That is the whole mechanism, and it is not magic. A commit's hash covers its content, its author, its timestamp, and its parent. Rebase changes the parent, from ce2a214 to 6fecb31, so the hash changes even though nothing else did. The old commit is not edited. It is abandoned, and a new commit with the same message takes its place. Git does not rewrite history. It writes new history and leaves the old commits lying around until something prunes them.
The force push
A normal push after this fails, because the remote still points at ba88a11 and the new history does not contain it. My first attempt failed for a second reason too: the remote had moved since my last fetch, so --force-with-lease refused until I fetched first. That refusal is the lease doing its job. After a fetch, the force push went through:
+ 8e44ceb...cdd32d5 feature -> feature (forced update)
The branch on the remote now points at the rewritten commit. So far, so tidy.
What the teammate sees
The teammate's clone still has the old history. Their feature still points at 8e44ceb, built on ba88a11. Nothing they did is wrong, and nothing in their repo tells them the branch moved under them until they fetch.
If they pull with a merge, it gets ugly. I tried it. The merge conflicted on a file both versions of the commit added, and after resolving, the history held both copies of the same work:
e9e0df0 merge remote feature after rebase
8e44ceb teammate: fix comment typo
cdd32d5 feature: add retry helper
ba88a11 feature: add retry helper
6fecb31 main: bump version
ce2a214 base commit
feature: add retry helper appears twice. Same message, same change, two hashes. git log now shows the work twice, git blame gets confusing, and anyone bisecting through this stretch hits the same change in two places.
The clean recovery is a rebase on their side: git rebase origin/feature. Their commit gets replayed on top of the rewritten branch and gets a new hash of its own, 99c3b26, while Git skips the old ba88a11 with a warning that it was already applied. That works, but it only works because the teammate knew to rebase instead of pulling, and because their change applied cleanly. With three teammates, or one who already pushed more work on top of the old commit, the cleanup multiplies.
When rebase is fine
Rebase is a great tool on a branch only you have touched. Your commits, your mess, your rewrite. The moment someone else has based work on your branch, the branch is shared, and rewriting it means rewriting their work too. They have to notice, they have to recover, and the recovery has to go right.
The rule people quote is really a question: who else is standing on this branch? If the answer is nobody, rebase away. If the answer is anyone at all, merge main into the branch instead. The history gets a merge commit and stays honest about what happened. A slightly messier graph beats a teammate's afternoon lost to untangling duplicate commits.
Comments
Join the discussionNo comments yet. Be the first to comment.
