Git Rebase vs Merge: When to Use Each and Why It Matters
- Merge preserves historical truth — use it for shared or long-lived branches where context of parallel work matters. Rebase rewrites for readability — use it on private feature branches before raising a pull request.
- The Golden Rule is non-negotiable: never rebase any branch that another developer has pulled. The moment a commit is shared, its SHA must not change.
- Interactive rebase (git rebase -i) is a pre-PR hygiene tool that lets you squash, reorder, and reword commits to tell a coherent story before your code is reviewed.
- Merge: safe on shared branches, creates merge commit, preserves commit SHAs
- Rebase: safe on private branches only, rewrites commit SHAs, produces linear history
- Interactive rebase (rebase -i): squash, reorder, reword commits before PR
- Golden Rule: never rebase a branch others have pulled
'Your branch and origin have diverged' after teammate rebased shared branch
git fetch origin (get the rebased remote state)git reset --hard origin/main (align with rebased remote)Rebase conflict at each commit — resolve, continue, conflict again
git status (see conflicted files)git add <file> && git rebase --continue (resolve and continue)git push rejected — 'non-fast-forward' after rebasing pushed commits
git log --oneline origin/main..HEAD (see your rebased commits)git log --oneline HEAD..origin/main (see remote-only commits)Lost commits during interactive rebase — dropped instead of squashed
git reflog | grep 'rebase' (find the rebase operation and pre-rebase state)git cherry-pick <hash> (recover the dropped commit)Extra merge commit in rebased chain — ran git commit instead of rebase --continue
git rebase -i HEAD~N (open interactive rebase for recent commits)squash the extra commit into its parent (change 'pick' to 'squash')Production Incident
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.Production Debug GuideSystematic recovery paths for diverged branches, rebase conflicts, and shared-branch rebase disasters.
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.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.
# io.thecodeforge — Merge Feature Branch # ───────────────────────────────────────────────────────────── # SCENARIO: You've been building a user-authentication feature. # A colleague just merged a critical security patch into main. # You need those changes in your feature branch before you can continue. # ───────────────────────────────────────────────────────────── # Step 1 — Check your current position git log --oneline --graph --all # Output before merge: # * f3a91bc (HEAD -> feature/user-auth) Add password hashing # * 9d44e02 Add login endpoint # | * c7b83af (main) SECURITY: sanitize SQL inputs in user queries # | * 4e21d09 Add rate limiting middleware # |/ # * 8b12cde Initial project scaffold # Step 2 — Switch to your feature branch (already on it, just confirming) git checkout feature/user-auth # Step 3 — Merge main INTO your feature branch # This brings the security patch into your work git merge main # Git opens your editor for a commit message, or auto-creates: # "Merge branch 'main' into feature/user-auth" # Step 4 — See what the history looks like now git log --oneline --graph --all
|\
| * c7b83af (main) SECURITY: sanitize SQL inputs in user queries
| * 4e21d09 Add rate limiting middleware
* | f3a91bc Add password hashing
* | 9d44e02 Add login endpoint
|/
* 8b12cde Initial project scaffold
- 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.
# io.thecodeforge — Rebase Feature Branch onto Main # ───────────────────────────────────────────────────────────── # Same scenario — feature branch needs the security patch from main. # This time we'll use rebase instead of merge. # ───────────────────────────────────────────────────────────── # Step 1 — Verify starting state (same branching point as before) git log --oneline --graph --all # * f3a91bc (HEAD -> feature/user-auth) Add password hashing # * 9d44e02 Add login endpoint # | * c7b83af (main) SECURITY: sanitize SQL inputs in user queries # | * 4e21d09 Add rate limiting middleware # |/ # * 8b12cde Initial project scaffold # Step 2 — Make sure your working tree is clean before rebasing git status # Should show: nothing to commit, working tree clean # Step 3 — Rebase the feature branch onto the tip of main # Git will replay 9d44e02 and f3a91bc on top of c7b83af git rebase main # Output during rebase: # Successfully rebased and updated refs/heads/feature/user-auth. # Step 4 — Look at the history now git log --oneline --graph --all # Step 5 — If you had already pushed the old commits, force-push the rebased ones # ONLY if nobody else has pulled your branch git push --force-with-lease origin feature/user-auth # --force-with-lease is safer than --force: it fails if the remote has # commits you don't know about (someone else pushed while you were rebasing).
* a1f84b9 Add login endpoint
* c7b83af (main) SECURITY: sanitize SQL inputs in user queries
* 4e21d09 Add rate limiting middleware
* 8b12cde Initial project scaffold
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'.
# io/thecodeforge — Interactive Rebase Cleanup Before PR # ───────────────────────────────────────────────────────────── # SCENARIO: You've been working on a checkout feature. # Your commit history is messy with 'WIP' and 'fix typo' commits. # Before raising a PR, you want to clean it up into meaningful commits. # ───────────────────────────────────────────────────────────── # Step 1 — See what you're working with git log --oneline # 8a3f21c Fix typo in promo code validator # 7d91b4e WIP saving progress before standup # 6c82e10 Add promo code validation logic # 5b44a01 Add cart total calculation # 4e30f99 (main) Set up checkout module # Step 2 — Start interactive rebase for the last 4 commits # The SHA here is the commit BEFORE the ones you want to edit git rebase -i 4e30f99 # Git opens your editor showing: # pick 5b44a01 Add cart total calculation # pick 6c82e10 Add promo code validation logic # pick 7d91b4e WIP saving progress before standup # pick 8a3f21c Fix typo in promo code validator # Step 3 — Edit the file to squash the WIP and typo fix into the promo commit # Change 'pick' to 'squash' (or 's') for commits you want to fold in: # pick 5b44a01 Add cart total calculation # pick 6c82e10 Add promo code validation logic # squash 7d91b4e WIP saving progress before standup # squash 8a3f21c Fix typo in promo code validator # Save and close. Git opens another editor for the combined commit message. # Write a clean message: "Add promo code validation with edge case handling" # Step 4 — Verify the clean result git log --oneline
5b44a01 Add cart total calculation
4e30f99 (main) Set up checkout module
- 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.
# io/thecodeforge — Handle Rebase Conflict # ───────────────────────────────────────────────────────────── # SCENARIO: Rebasing a feature branch that modifies the same config # file that main also modified — a conflict is guaranteed. # ───────────────────────────────────────────────────────────── # Start the rebase git rebase main # Auto-merging config/database.yml # CONFLICT (content): Merge conflict in run "git rebase --continue". # Step 1 — Open the conflicted file and look at what Git shows you cat config/database.yml # <<<<<<< HEAD (the tip of main, your new base) # pool_size: 10 # timeout: 5000 # ======= # pool_size: 25 # timeout: 3000 # >>>>>>> 3c9a11f (Update database pool size for auth service) # Step 2 — Decide what the correct value is and edit the file manually # Edit config/database.yml to the correct final state # Remove ALL conflict markers (<<<<<<<, =======, >>>>>>>) # Step 3 — Stage the resolved file (do NOT git commit here) git add config/database.yml # Step 4 — Tell rebase to continue replaying the remaining commits git rebase --continue # Git may open your editor to confirm the commit message — save and close. # Step 5 — Confirm the rebase completed cleanly git log --oneline --graph
* a1f84b9 Add login endpoint
* c7b83af (main) SECURITY: sanitize SQL inputs in user queries
* 4e21d09 Add rate limiting middleware
* 8b12cde Initial project scaffold
| Feature / Aspect | git merge | git rebase |
|---|---|---|
| History shape | Non-linear — shows parallel development as a graph | Linear — appears as a single straight chain of commits |
| Creates new commits | Yes — one merge commit with two parents | Yes — new copies of every replayed commit with new SHAs |
| Conflict resolution | One session for the entire merge | One session per replayed commit that causes a conflict |
| Safe on shared branches | Yes — always safe | No — never rebase branches others are working from |
| Best for | Integrating long-lived or shared branches | Updating private feature branches and cleaning up before PRs |
| Reversibility | Undo with git revert of the merge commit | Abort mid-process with git rebase --abort; hard to undo after |
| Commit SHAs preserved | Yes — existing commits unchanged | No — all replayed commits get new SHAs |
| Readability of git log | Can get noisy on active repos | Clean and easy to scan linearly |
| When to avoid | When you want a pristine linear history | On public/shared branches like main, develop, release/* |
| Interactive mode | Not available | git rebase -i for powerful history editing |
🎯 Key Takeaways
- Merge preserves historical truth — use it for shared or long-lived branches where context of parallel work matters. Rebase rewrites for readability — use it on private feature branches before raising a pull request.
- The Golden Rule is non-negotiable: never rebase any branch that another developer has pulled. The moment a commit is shared, its SHA must not change.
- Interactive rebase (git rebase -i) is a pre-PR hygiene tool that lets you squash, reorder, and reword commits to tell a coherent story before your code is reviewed.
- Conflict resolution behaves differently: merge gives you one conflict session; rebase gives you one session per conflicted commit being replayed. git rebase --abort is your complete undo.
- Always use --force-with-lease instead of --force when pushing rebased branches. --force blindly overwrites. --force-with-lease fails if the remote has commits you do not know about.
- The squash-vs-merge decision for PRs is a team convention. Squash-merge produces one clean commit. Regular merge preserves internal development history. Know your team's convention.
⚠ Common Mistakes to Avoid
Interview Questions on This Topic
- QWhat's the practical difference between git merge and git rebase, and how would you decide which one to use when integrating a feature branch into main at your company?
- QWhat is the Golden Rule of rebasing, and what exactly goes wrong technically if you rebase a branch that your teammates have already pulled?
- QYou've finished a feature branch with 12 commits, half of which are 'WIP' and 'fix linting' commits. Your team uses squash-merge for all PRs. Walk me through how you'd clean that up before opening the pull request.
- QExplain the difference between --force and --force-with-lease. When would you use each, and what is the risk of using --force?
- QDuring a rebase, you resolve a conflict and run git commit instead of git rebase --continue. What happens, and how do you fix it?
- QYour team lead rebased develop and force-pushed. Six developers now have diverged branches. Walk through the recovery process for each developer.
- QWhen would you use git rebase -i instead of a regular git rebase? What operations does interactive rebase support?
- QA merge commit has two parents. What does that mean for git bisect, and why might a team prefer linear history for debugging?
Frequently Asked Questions
Does git rebase delete my original commits?
Not immediately — it creates new commits with identical changes but different parent hashes, leaving the originals unreferenced. Git's garbage collector removes unreferenced commits after a default grace period of 30 days. You can recover them via git reflog within that window.
Is it safe to use git pull --rebase instead of git pull?
Yes, and many senior developers set this as their default via git config --global pull.rebase true. When you pull with rebase, your local unpushed commits are replayed on top of the fetched remote commits instead of creating a merge commit. This keeps your local history linear.
If both merge and rebase end up with the same code, why does the history shape matter?
Because history is how your team debugs the future. A linear history makes git bisect more effective at pinpointing which commit introduced a bug. A non-linear history with merge commits preserves context about what was happening in parallel — valuable when understanding why a particular architectural decision was made.
What is the difference between --force and --force-with-lease?
--force blindly overwrites the remote branch with your local state. If someone pushed while you were rebasing, their commits are silently lost. --force-with-lease checks that the remote ref matches what you expect before pushing. If the remote has commits you do not know about, it fails. Always use --force-with-lease.
When should I use merge instead of rebase?
Use merge for shared branches (main, develop, release/*), for integrating long-lived branches, and for any branch where the parallel development context matters for future debugging. Use rebase only on private feature branches that nobody else has pulled.
Developer and founder of TheCodeForge. I built this site because I was tired of tutorials that explain what to type without explaining why it works. Every article here is written to make concepts actually click.