Git Rebase vs Merge — Shared Branch Rebase Cost 6 Dev Hours
Git Rebase vs Merge: After a force-pushed rebase on develop cost 6 devs 3 hours, learn the production disaster, recovery steps, and the golden rule to prevent it.
- Merge creates a merge commit with two parents — preserves parallel development history
- Rebase replays commits on top of new base — linear history with rewritten SHAs
- Interactive rebase (rebase -i): squash, reorder, reword commits before PR
- Golden Rule: never rebase a branch others have pulled
- Performance: linear history from rebase speeds up git bisect by ~30%
- Production insight: rebasing a shared branch forces teammates to hard-reset; one bad rebase costs hours
Imagine you're writing a group essay. Merge is like stapling everyone's drafts together at the end — you can see every version and who wrote what, but the final document has a messy paper trail. Rebase is like retyping your section from scratch onto the latest shared draft — the result looks seamless, as if only one person wrote it the whole time. Both get you to a finished essay, but they leave very different paper trails behind.
Merge and rebase both integrate changes from one branch into another. They solve the same problem in fundamentally different ways. Merge creates a merge commit that records when two branches came together. Rebase rewrites your commits so they appear to have started from the tip of the target branch.
The choice shapes your project's Git history. Merge preserves the full context of parallel development — you can see exactly what state main was in when your feature was built. Rebase produces a clean precise and git log more readable. Neither is universally better.
Common misconceptions: that rebase is always cleaner (it rewrites SHAs, which breaks shared branches), that merge always creates noise (merge commits are meaningful integration markers), and that the choice is personal preference (it is a team decision that affects debugging and collaboration).
How Git Merge Works — and What It Leaves Behind
When you run git merge, Git finds the most recent common ancestor of the two branches (called the merge base), then combines the changes from both branches into a brand-new commit. This new commit has two parents — one from each branch — which is why it's called a merge commit. It's the only commit in your repo that points backwards at two different lines of work.
This means your history is a faithful record of reality. If you and a colleague worked in parallel for a week, the graph shows that. You can run git log --graph and literally see two lanes of traffic merging into one. That's incredibly useful when you're trying to understand why a change was made in the context of what else was happening at the time.
The downside is noise. On a busy team where a dozen feature branches merge into main every day, your history fills up with merge commits that add structural information but no actual code change. Tools like git bisect and git log --oneline become harder to read. Some teams are fine with this — it's an honest record. Others find it distracting. That tension is the whole reason rebase exists.
- Merge commit has two parents — one from each branch
- git log --graph shows the parallel lanes and the merge point
- History is a faithful record of what happened in parallel
- Merge commits add structural information but no code change — this is the noise trade-off
How Git Rebase Works — and Why It Rewrites History
Rebase does something more radical: it replays your commits one by one on top of a new base commit. The word 'rebase' is literal — you're changing the base commit that your branch started from. The result looks as if you had branched off at the very tip of the target branch and written all your commits from there.
Here's the key thing to internalise: your original commits are not moved. Git creates brand-new commits with the same changes but different parent hashes, timestamps, and therefore different SHA-1 identifiers. The old commits still exist temporarily, but with nothing pointing to them, they'll be cleaned up by Git's garbage collector. This is not a cosmetic rename — it is a genuine rewrite.
The payoff is a perfectly linear history. When a reviewer reads your branch after a rebase, they see a clean sequence of purposeful commits with no structural noise. git log looks like a well-written changelog. git bisect works with surgical precision because every commit in the chain is meaningful. This is why many teams require rebase before merging feature branches via pull requests — you get the readability benefits of a linear history in the shared record.
Warning: Never Rebase Shared Branches
The Golden Rule of rebasing is non-negotiable: never rebase a branch that another developer has pulled from. Once a commit is shared — pushed to a remote or pulled by a teammate — its SHA must never change. Rebasing a shared branch rewrites all commits, creating diverging histories that force every collaborator to hard-reset. The most common casualty is the develop branch, where a well-intentioned cleanup costs hours of team coordination.
- Rebase creates new commits with new SHAs — old commits are orphaned
- Orphaned commits are garbage-collected after 30 days (recoverable via reflog)
- Golden Rule: never rebase a branch others have pulled
- Use --force-with-lease instead of --force when pushing rebased branches
Visual Branch History: Merge vs Rebase
Seeing the difference between merge and rebase is easier with visual diagrams. Below is a Mermaid graph showing the same initial forked history, then the result of merging, and the result of rebasing. Notice how the merge result introduces a merge commit with two parents, while the rebase result appears as a linear sequence of commits.
Before: Forked History — main has commit A, then both main and feature branches diverge. main receives commit C, feature receives commit B.
After Merge — a new merge commit D joins the two branches. Git shows both lanes of work. This is truthful parallelism but adds structural nodes.
After Rebase — feature's commit B is replayed on top of main's commit C, becoming B' with a new SHA. The history looks as if the feature was developed entirely after main's latest commit. This is easier to read but loses the fact that the feature and main commits happened concurrently.
Rebasing onto Specific Branches with --onto
The git rebase --onto flag gives you fine-grained control over where your branch is rebased. Without --onto, rebasing moves the entire branch onto the new base (e.g., git rebase main rebases all commits in the current branch that are not in main onto the tip of main). With --onto, you can rebase only a subset of commits onto a completely different base.
Common use case: you started a feature branch from the wrong point — say you branched from main when you should have branched from release/v2.0. Instead of cherry-picking each commit, you can run:
git rebase --onto release/v2.0 main feature/checkout
This takes all commits on feature/checkout that are not in main and replays them on top of release/v2.0. It's a precise surgical operation.
Another scenario: you have a chain of branches (feature-A depends on feature-B). If feature-B has already been merged and you want to rebase feature-A directly onto main, you can use --onto to skip the intermediate base.
Three Real-World Workflows — Which Approach Fits Each
Knowing the mechanics is half the battle. Knowing which to reach for in a given situation is what separates a senior engineer from someone who just memorised the commands.
Workflow 1 — Keeping your feature branch up to date: Use rebase. While you're developing in isolation, nobody else is working off your branch. Rebasing onto main daily keeps your branch current without cluttering the future merge commit with a tangle of catch-up merges inside your PR. Your pull request shows exactly your work, nothing else.
Workflow 2 — Landing a feature into main via pull request: Both strategies are used in industry. Teams that value linear history use 'Squash and Rebase' in GitHub/GitLab so the entire feature lands as one clean commit. Teams that value commit granularity and want to see the feature's internal development use a regular merge commit. Know your team's convention before you hit the merge button.
Workflow 3 — Merging a long-lived shared branch (e.g. a release branch back into main): Always use merge, never rebase. Release branches are shared by the whole team. Rebasing would rewrite commits that everyone already has locally, causing a synchronisation disaster. A merge commit here is not noise — it's a meaningful event marker that says 'release 2.4.0 was integrated on this date'.
- squash: fold a commit into the previous one — combines changes, merges messages
- fixup: fold a commit into the previous one — combines changes, discards message
- reorder: move commits up or down in the editor to change the sequence
- reword: change a commit message without changing the code
Resolving Conflicts: How Merge and Rebase Differ Under Pressure
Both commands can hit conflicts when the same lines of code were changed in both branches. But the experience of resolving those conflicts is very different — and this catches a lot of developers off guard the first time they rebase a branch with multiple commits.
With merge, you get exactly one conflict-resolution session. Git combines everything in one shot and stops if it can't. You fix the conflicts, run git add, then git merge --continue (or git commit), and you're done.
With rebase, conflicts can appear multiple times — once for each commit being replayed. If you have five commits and the second one conflicts, you'll resolve it and run git rebase --continue, then potentially hit another conflict at the fourth commit. Each resolution is isolated to the changes in that specific commit, which is actually more precise but also more repetitive. If you're not prepared for this, it feels like you've broken something when the conflict reappears.
The nuclear escape hatch is git rebase --abort, which returns your branch to exactly the state it was in before you started the rebase. There's no equivalent 'undo' once a merge commit is created — you'd need git revert or git reset, both of which are more involved.
- Merge: one conflict session for the entire merge — resolve once, done
- Rebase: one conflict session per replayed commit — resolve multiple times
- During rebase conflict: git add <file> then git rebase --continue, never git commit
- git rebase --abort: complete undo, returns to pre-rebase state — no side effects
Recovery Procedure: Upstream Branch Was Rebasing
If a teammate rebased and force-pushed a shared branch (like develop), your local branch is now based on outdated SHAs. The typical symptom is Your branch and 'origin/develop' have diverged. The wrong instinct is to merge — that creates a duplicate set of commits and a confusing history. The correct recovery is a hard reset to the rebased remote, then cherry-pick any local commits you had made on top of the old branch.
Recovery Steps: 1. Do NOT merge. Merging creates duplicate commits from the old base and the new base. 2. Fetch the latest remote state: git fetch origin 3. Align your local branch with the rebased remote: git reset --hard origin/develop. 4. If you had local commits on top of the old develop (commits you haven't pushed or that are unique to your local), recover them by finding their SHAs in git reflog and cherry-picking them: git cherry-pick <sha>. Or, if you had a feature branch based on the old develop, you can rebase that feature branch directly onto the new origin/develop: git checkout feature/your-feature && git rebase origin/develop. 5. After recovery, communicate with your team to ensure everyone is synchronized. No one should push before all have done the hard reset.
Prevention: Branch protection rules that block force-pushes to develop and main. If a rebase of a shared branch is absolutely necessary, it must be announced with a clear timeline and a plan for everyone to reset.
git pull when your local branch has diverged from the remote, Git will by default do a merge (depending on config). This creates a merge commit that includes all the old commits from the shared branch PLUS all the new rebased commits — effectively duplicating all the work. The proper fix is always a hard reset or a rebase onto the new remote state.Decision Guide: When to Use Merge vs Rebase
The choice between merge and rebase is not about personal preference — it's about branch type, team workflow, and what kind of history your project needs.
Use Merge When: - Integrating a shared branch (main, develop, release/*, hotfix). These branches have multiple contributors. Rebasing them would rewrite everyone's history. - Landing a large feature that has many commits and you want to preserve the internal development context. The merge commit acts as a bookmark. - Bringing a long-lived branch up to date with its base. Merge creates a single commit that records when the synchronization happened. - Working on a branch that multiple people are actively pushing to. Merge is the only safe way to integrate.
Use Rebase When: - Updating your private feature branch with changes from main. Your branch hasn't been pushed or others haven't pulled from it. - Clean up your branch's commit history before opening a pull request (interactive rebase). - You want a linear, easy-to-read git log that simplifies tools like git bisect and git log --oneline. - You're applying a sequence of patches from another branch (with --onto).
Philosophical Debate: Some teams advocate for rebase-only workflows because they produce a clean history. Others argue merge commits are essential for understanding the project's timeline. In practice, most mature teams use both: merge for shared branches and rebase for feature branches, with interactive rebase for PR preparation. The key is to establish a team convention and stick to it.
Rebase on develop Branch: 6 Developers Lose 3 Hours Coordinating Recovery
git rebase main on the develop branch to remove merge commit clutter.
2. The rebase rewrote all commit SHAs on develop.
3. They force-pushed: git push --force origin develop.
4. Six teammates had feature branches that were branched off the old develop (with old SHAs).
5. On their next git fetch, their local origin/develop now pointed to the rebased commits (new SHAs).
6. Their feature branches were still based on the old develop (old SHAs).
7. Git saw the branches as diverged — the old commits and new commits had different parents.
8. One developer tried to merge origin/develop into their feature branch, creating a merge commit with duplicate changes.
9. The CI pipeline built from develop and deployed the duplicate-logic code.git fetch origin && git reset --hard origin/develop to align with the rebased remote.
2. The developer who merged the diverged branches had to git reset --hard to the commit before the merge and re-branch from the rebased develop.
3. Team rule: never rebase develop, main, release/*, or any branch that others have branched from.
4. Added branch protection on GitHub to prevent force-pushes to develop and main.
5. Documented the Golden Rule in the team wiki with a link to this incident.- The Golden Rule is non-negotiable: never rebase a branch that another developer has pulled. The moment a commit is shared, its SHA must not change.
- Force-pushing a rebased shared branch costs every downstream developer time to recover. One bad rebase can cost the team hours.
- Branch protection rules on GitHub/GitLab prevent force-pushes to protected branches. Configure them for main, develop, and release branches.
- If you accidentally rebase a shared branch, announce it immediately. Coordinate the recovery before anyone merges the diverged state.
git fetch origin to get the rebased remote state.
4. Hard-reset: git reset --hard origin/main to align with the rebased remote.
5. If you had local commits on top of the old branch: cherry-pick them onto the new base: git cherry-pick <old-commit-hash>.git add <file> and git rebase --continue.
3. Do NOT run git commit during a rebase — use git rebase --continue only.
4. If too complex: git rebase --abort to return to pre-rebase state.git push --force is safe.
3. If others have pulled: do NOT force-push. Coordinate with them first.
4. Prevention: only rebase commits that have not been pushed yet.git reflog to find the commit hash before the rebase.
3. Cherry-pick the lost commit: git cherry-pick <hash>.
4. If the entire rebase went wrong: git reset --hard ORIG_HEAD to return to pre-rebase state.git commit instead of git rebase --continue during conflict resolution.
2. The extra commit is now in your rebased chain.
3. Fix: git rebase -i and squash the extra commit into the correct parent.
4. Prevention: always use git rebase --continue during rebase conflict resolution.Common mistakes to avoid
2 patternsRebasing a shared branch to clean up history
Running git commit instead of git rebase --continue during conflict resolution
Interview Questions on This Topic
Explain the Golden Rule of rebasing and why it's important.
That's Git. Mark it forged?
9 min read · try the examples if you haven't