Merge, rebase or squash
Three ways a pull request lands on main, and what each does to history. Learn the one rule that keeps rebase safe, why squash makes the PR title matter, and integrate a branch a teammate built on without hurting them.
Your branch is reviewed and green. Now it has to land on main, and GitHub offers three buttons that end with the same files but different histories:
- Create a merge commit: “all commits from the feature branch are added to the base branch in a merge commit”, using
--no-ff. - Squash and merge: “the pull request’s commits are squashed into a single commit”.
- Rebase and merge: “all commits from the topic branch (or head branch) are added onto the base branch individually without a merge commit” - and GitHub “creates new commit SHAs” to do it.
Merge: keep everything, join the lines
A merge commit has two parents: the tip of main and the tip of your branch. Every commit you made keeps its id, and the history shows that a branch existed. That is why git-flow’s author recommends --no-ff: it “avoids losing information about the historical existence of a feature branch”. The cost is a history with forks and joins in it - the same merge you met in Conflicts are honesty.
Rebase: replay your work on top
Rebase takes your commits and replays them, one by one, on top of the new main. Pro Git is precise about what that means: “you’re abandoning existing commits and creating new ones that are similar but different”. Same changes, new commits, new ids. Afterwards main can fast-forward and history is one straight line. Pro Git also points out that the final snapshot “is the same snapshot - it’s only the history that is different”.
Abandoning commits is harmless when nobody else has them. It is not harmless when they do.
- You push your branch. Now it exists outside your repository.
- Sam fetches it and builds G on top of your F2. His work depends on your commit ids.
- You rebase onto the new main. F1 and F2 become F1' and F2': similar, but different commits.
- A normal push is refused, so you force-push. Origin now has the new copies; the old ids are gone from it.
- Sam pulls. Git merges his old F1 and F2 with your new copies - and his log shows each of your commits twice.
Pro Git’s rule, in bold in the original:
Do not rebase commits that exist outside your repository and that people may have based work on.
And the practical version: “rebase local changes before pushing to clean up your work, but never rebase anything that you’ve pushed somewhere.” This site’s tests replay exactly the story above with the real git binary: after the force-push and Sam’s pull, git log shows both copies of each commit.
Squash: one commit, and the title is the message
Squash turns the whole pull request into one new commit on main. Your individual commits never reach main at all - which keeps main tidy, and has two consequences.
First, the commit message. GitHub’s default for a squash is “the commit title and message” when the pull request has one commit, and “the pull request title and list of commits” when it has two or more. With more than one commit, the pull request title becomes the commit title on main. If your team uses Conventional Commits and release-please, that title is what decides the next version - feat(search): filter results by price makes a minor release; Update search makes nothing.
Second, teammates. Anyone who built on your branch still has your original commits. main only has the squashed copy, so git sees those commits as not yet on main, and their next pull request offers them all over again.
Try it
Two situations. On your own unpushed branch, anything goes. On a branch Sam has built on, see which method leaves him nothing to untangle.
| Method | What main gets | Rewrites commits? | Safe on a branch others built on? |
|---|---|---|---|
| Merge commit | your commits + one merge commit | no | yes |
| Rebase and merge | new copies of your commits, in a line | yes | no - duplicates for them |
| Squash and merge | one new commit (title = PR title, for 2+ commits) | effectively: the originals never reach main | no - their branch re-offers your commits |
You rebase your branch onto the latest main. What happens to your original commits?
Rebase creates new commits that are similar but different. Your old ones still exist in any clone that had them - which is exactly why rebasing pushed work causes trouble.
A teammate has built on your pushed branch. Which integration leaves them nothing to untangle?
Only a merge commit keeps your commits' ids, so their work still sits on commits main contains. Rebase gives them duplicates; squash leaves them holding commits main never received.
Your team squash-merges and uses release-please. A pull request with three commits is titled 'Update search'. What does release-please see?
With two or more commits, GitHub's default squash message uses the pull request title as the commit title. 'Update search' has no conventional type, so release-please counts nothing.
Everything above ran in your browser. Now try it in a real terminal: any Linux machine, a Mac's Terminal app, or Windows with WSL. Nothing to install beyond that.
Reproduce the rebase story on your own disk - no account needed, a bare repository is a fine remote:
git init --bare -b main origin.git && git clone -q origin.git you && cd you
git switch -c main && git commit --allow-empty -qm "base" && git push -q origin main
git switch -c feature && git commit --allow-empty -qm "F1" && git commit --allow-empty -qm "F2"
git push -q origin feature && cd ..
git clone -q -b feature origin.git sam && cd sam && git commit --allow-empty -qm "G" && cd ..
cd you && git switch main && git commit --allow-empty -qm "C" && git push -q origin main
git switch feature && git rebase -q main && git push -q --force origin feature && cd ..
cd sam && git pull -q --no-rebase --no-edit origin feature && git log --onelineSam’s log shows F1 and F2 twice. Then do it again with git merge --no-ff feature on main instead of the rebase, and compare.
Sources
Missions and quizzes above earn their own XP. This marks the lesson done.