The three methods
GitHub calls them merge commit, squash and merge and rebase and merge (GitHub docs). Each maps onto plain git. All three produce byte-identical files — checked out, the three repositories are indistinguishable. Only the history differs.
Merge commit
git merge --no-ff feature. Every original commit survives with its own hash, and a new commit with two parents records that the two lines joined.
The only method that lets git log --first-parent main read as one line per landed branch.
Squash
git merge --squash feature, then one commit. One new commit on main, with no link to the branch at all.
The originals stay on the branch, unreferenced by main. See the trap below.
Rebase-merge
git rebase main on the branch, then a fast-forward. Every commit preserved, in order, linear — and every one of them a new object.
GitHub “always updates the committer information and creates new commit SHAs”, so the branch as reviewed no longer exists.
What each one costs
| Method | Keeps / costs, and when it is right |
|---|---|
| Merge commit | Keeps every commit and the record of the join. Costs a diamond-shaped history and every “fix a typo” commit in main forever. Right when several people work on one branch, or you need to know which change set something arrived in. |
| Squash | Keeps a linear main where every commit is one reviewed change. Costs the individual commits, and springs the trap below. Right when an agent produces branches of eleven commits called “wip”, “fix”, “fix again” — which is most agent branches. |
| Rebase-merge | Keeps each commit as its own entry with no merge commits. Costs the truth: committer dates all become the merge moment, and every reviewed hash is dead. Right when commits are curated one-per-logical-change and you want fine-grained git bisect steps. |
There is no correct answer, but there is a correct way to decide: pick the one whose failure mode you can live with, then make it the repository’s only enabled option so nobody chooses again. On GitHub that is Settings → General → Pull Requests. Leaving all three enabled means the choice is made by whoever clicks the button — which, increasingly, is not a person.
The squash trap
Squashing puts the branch’s content in main and leaves no link. So git branch --merged still calls the branch unmerged, and one more commit on it re-proposes every commit that was already squashed in — conflicting on lines that are already correct. Delete it and branch again from main.
The mechanism is reachability, not content. git branch --merged lists “branches whose tips are reachable from the specified commit” (git-branch), and --squash produces “the working tree and index state as if a real merge happened (except for the merge information)” (git-merge). That parenthesis is the whole story.
Walk it: the squash commit names the previous main commit, which names the one before. Nothing on that walk is the branch tip, and nothing anywhere names it. Its content is in main twice over; its commit is in no history at all.
| After a squash-merge | What you see |
|---|---|
git branch --merged main | The branch is not listed, though every line of its work is in main. |
git log --oneline main..feature | Still reports the squashed commits as outstanding. |
| Adding one commit and re-opening | The new PR shows the old commits again, plus the new one. |
| Merging that second PR | Conflicts, in a repository where nothing is actually in conflict. |
Telling which method a repository uses
git log --graph --oneline --decorate -20— diamonds mean merge commits; a flat line with(#123)suffixes means squash; a flat line with ordinary messages means rebase-merge.git branch --merged main— the sharper check. If branches whose work is plainly inmainare still listed as unmerged, the repository squashes, and every one of those branches is a trap if anyone picks it back up.git log --format='%h %an %ad %cn %cd' --date=short— if every committer date on a run of commits is the same instant and the author dates are spread out, the repository rebase-merges.
The practical consequence for an agent: tell it the rule that follows. If the repository squashes, tidying a branch’s history before it lands is work spent on an artifact the merge deletes seconds later — but delete-and-rebranch afterwards is mandatory. If it merge-commits, leave the history alone. Rebasing to resolve conflicts against current main is a separate and legitimate reason under any method.
Taking a landing back
| Landed by | Reverting it |
|---|---|
| Squash | Ordinary: one commit in, one commit out. git revert <sha>. |
| Merge commit | Needs git revert -m 1 <sha> to name the mainline parent. Without -m git refuses outright — “usually you cannot revert a merge because you do not know which side of the merge should be considered the mainline” (git-revert). |
| Rebase-merge | Ordinary per commit, but there are several of them, and reverting them out of order will conflict. |
The sting on the merge-commit side is worth knowing before you need it in a hurry: reverting a merge “declares that you will never want the tree changes brought in by the merge”, so re-merging that branch later brings in nothing. The fix is to revert the revert, which is as strange as it sounds and is what the docs recommend.
If a second person joins the repository
Everything above is about one branch landing. With several people, branches land while other branches are being tested — so a PR that was green against yesterday’s main can break main when it merges today. That is what a merge queue exists to prevent: it “helps increase velocity by automating pull request merges into a busy branch and ensuring the branch is never broken by incompatible changes” (GitHub docs).
| What the queue does | Detail |
|---|---|
| Builds a merge group | Changes are “grouped into a merge_group with the latest version of the base_branch as well as changes from pull requests ahead of it in the queue” — so each PR is tested against the ones landing before it, not against main as it stands now. |
| Tests on temporary branches | It “creates temporary branches with a special prefix to validate pull request changes”, e.g. main/pr-1, main/pr-2. Your CI runs against those. |
| Ejects failures | “If there are failed required status checks or conflicts with the base branch, the pull request will be removed from the queue” — the rest of the queue continues without it. |
| Still needs a method | The queue does not replace the choice above: you “select which method to use when merging queued pull requests: merge, rebase, or squash”. |
For a solo repository this is machinery you do not need. It is worth recognising because it changes what an agent should expect: with a queue, clicking merge does not merge — it enqueues, the branch is tested again in a combination that has never existed before, and the PR can come back out of the queue without anyone touching it.
- About pull request merges — GitHub DocsWhat the button does, and the settings that enable each method. Does not say a squashed branch stops counting as merged — that is a git fact.
- git-branch — `--merged`Reachability, not content. The documentary basis of the squash trap.
- git-merge — `--squash`“As if a real merge happened (except for the merge information).”
- git-revertWhy
-mis required on a merge, and what reverting a merge declares about future merges. - Managing a merge queue — GitHub DocsThe team layer: merge groups, temporary branches, and what happens to a PR that fails in the queue.