Curious Pandas

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.

15 min+40 XP for finishing

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.

Visual guideRebasing a branch someone built on
Rebasing a branch someone built onYou push F1 and F2. Sam fetches them and builds G on top. You rebase, which makes new copies F1' and F2', and force-push them over the originals. When Sam pulls, git merges his old F1 and F2 with your new copies, and his history shows each of your commits twice.you push F1, F2to originoriginhas F1, F2Sam builds Gon top of F2you rebasenew F1', F2'force-pushold ids replacedSam pullsold + new copiespushfetchrewritepush -fpullRebasing a branch someone built onYou push F1 and F2. Sam fetches them and builds G on top. You rebase, which makes new copies F1' and F2', and force-push them over the originals. When Sam pulls, git merges his old F1 and F2 with your new copies, and his history shows each of your commits twice.you push F1, F2to originoriginhas F1, F2Sam builds Gon top of F2you rebasenew F1', F2'force-pushold ids replacedSam pullsold + new copiespushfetchrewritepush -fpull
  1. You push your branch. Now it exists outside your repository.
  2. Sam fetches it and builds G on top of your F2. His work depends on your commit ids.
  3. You rebase onto the new main. F1 and F2 become F1' and F2': similar, but different commits.
  4. A normal push is refused, so you force-push. Origin now has the new copies; the old ids are gone from it.
  5. 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.

Integrate the search brancha model of git; checked against real git in the tests

The commit graph
on mainyour branchSam's work
main, newest first
    release-please reads
        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
        Check yourself+10 XP

        You rebase your branch onto the latest main. What happens to your original commits?

        Check yourself+10 XP

        A teammate has built on your pushed branch. Which integration leaves them nothing to untangle?

        Check yourself+10 XP

        Your team squash-merges and uses release-please. A pull request with three commits is titled 'Update search'. What does release-please see?

        Do it for real · your machine+50 XP bonus

        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 --oneline

        Sam’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.

        Self-reported. We can't see your machine, and we don't want to. This is a bonus, never required.

        Sources

        Finished reading?

        Missions and quizzes above earn their own XP. This marks the lesson done.