|
| 1 | +# What a squash merge lands |
| 2 | + |
| 3 | +A squash merge writes **one** commit to main, and its message is composed from |
| 4 | +two surfaces: the **squash-message subject** (a text box in the merge dialog, |
| 5 | +seeded with the PR title) and the **branch's commit messages**. The PR *body* is |
| 6 | +not an input to it. Both halves below were measured against the nine PRs this pod |
| 7 | +landed between 09:08:43Z and 09:19:20Z on 2026-09-30, by recomputing the expected |
| 8 | +message from the PR title and the branch commits and comparing it to what main |
| 9 | +carries, character for character. |
| 10 | + |
| 11 | +## The composition |
| 12 | + |
| 13 | +| PR shape | landed subject | landed body | |
| 14 | +|---|---|---| |
| 15 | +| **1 commit** | `<subject box>` + ` (#N)` | that commit's message **minus its first line** | |
| 16 | +| **2+ commits** | `<subject box>` + ` (#N)` | per commit, in order: `* <full commit subject>`, a blank line, then that commit's message body | |
| 17 | + |
| 18 | +The subject box is seeded with the **PR title**, so the default is |
| 19 | +`<PR title> (#N)` — true in 8 of the 9. The body matched this composition |
| 20 | +**exactly in 9 of 9**, single-commit and 8-commit PRs included. |
| 21 | + |
| 22 | +| PR | commits | landed subject | landed body | |
| 23 | +|---|---|---|---| |
| 24 | +| #2015 | 1 | title + `(#2015)` | the commit body | |
| 25 | +| #2017 | 3 | title + `(#2017)` | 3 bullets + bodies | |
| 26 | +| #2024 | 2 | title + `(#2024)` | 2 bullets + bodies | |
| 27 | +| #2031 | 1 | **the COMMIT subject** + `(#2031)` | the commit body | |
| 28 | +| #2043 | 8 | title + `(#2043)` | 8 bullets + bodies | |
| 29 | +| #2045 | 3 | title + `(#2045)` | 3 bullets + bodies | |
| 30 | +| #2046 | 1 | title + `(#2046)` | the commit body | |
| 31 | +| #2048 | 1 | title + `(#2048)` | the commit body | |
| 32 | +| #2049 | 2 | title + `(#2049)` | 2 bullets + bodies | |
| 33 | + |
| 34 | +## #2031 is the one that shows the box is editable |
| 35 | + |
| 36 | +Its PR title was `… the concurrency discriminator is the run, not the SHA |
| 37 | +(TASK-204 late delta)`. The landed subject is `… the concurrency |
| 38 | +discriminator is the run, not the SHA (#2031)` — the commit's own subject, with |
| 39 | +GitHub's ` (#N)` still appended. So the presser can overwrite the subject box, |
| 40 | +and ` (#N)` is added to whatever sits in it. One in nine — and the practical |
| 41 | +reading runs the other way from the usual one: **do not assume the PR title is |
| 42 | +what landed.** Read the merge commit. |
| 43 | + |
| 44 | +## Why the body is the surface that bites |
| 45 | + |
| 46 | +A claim that must not land is fixed on **both** surfaces — the PR title (the |
| 47 | +subject) **and every commit message that carries it** (the body). Nothing else |
| 48 | +reaches either: a PR *body* edit is not an input to the composition at all, and a |
| 49 | +PR comment reaches neither. |
| 50 | + |
| 51 | +**Amending the tip does not amend earlier commits.** Measured on #2049, whose |
| 52 | +branch carried two commits with the tip amended to retract an over-claim: main's |
| 53 | +merge message today carries both texts, side by side — |
| 54 | + |
| 55 | +``` |
| 56 | +line 13: `split('@media (prefers-reduced-motion')[1]` is 865 lines / 33,883 characters |
| 57 | +line 58: right; calling it "fiction about the code" was the wrong reason, and is retracted. |
| 58 | +``` |
| 59 | + |
| 60 | +— the retracted framing in commit 1's body and its retraction in commit 2's, in |
| 61 | +one message. `git log -1 --format=%B <merge>` is the only reader that shows |
| 62 | +this. The PR page shows the current branch state, which is what the author last |
| 63 | +saw; main shows what landed. |
| 64 | + |
| 65 | +## Checking what landed — and what will land |
| 66 | + |
| 67 | +```bash |
| 68 | +PR=2049 |
| 69 | +M=$(git log --format=%H --grep="(#$PR)\$" -1 origin/main) # the merge commit |
| 70 | +git log -1 --format=%s "$M" # the landed subject |
| 71 | +git log -1 --format=%B "$M" # the landed body — the surface main reads |
| 72 | +gh pr view "$PR" --json title -q .title # the seeded subject |
| 73 | +gh api repos/Team-Commonly/commonly/commits/<oid> -q .commit.message # lossless branch message |
| 74 | +``` |
| 75 | + |
| 76 | +Two instrument notes, both measured. `gh pr view <N> --json commits -q |
| 77 | +'.commits[] | .messageHeadline'` is **lossy**: across the nine PRs' 22 commits, |
| 78 | +17 headlines ended in `…` and every one of those measured exactly 70 characters |
| 79 | +(69 plus the ellipsis), while the longest intact one is 72 — so the cut lands |
| 80 | +somewhere between, and the projection counts commits faithfully while quoting |
| 81 | +them wrongly. Read the commit object above for the text itself; the branch |
| 82 | +commits survive the branch deletion inside the PR record. The second note is the |
| 83 | +one that matters here: this is the same class as the PR page, a summary that is |
| 84 | +right about the shape and wrong about the words. And the composition is a fact about the |
| 85 | +**message** only: whether what landed is what was reviewed is a separate |
| 86 | +measurement — the patch-id comparison in rule 32 of |
| 87 | +`docs/development/review-checklist.md`, and the tree/origin check in rule 49. |
| 88 | + |
| 89 | +## Habits this buys |
| 90 | + |
| 91 | +- **Before a press:** the PR title is one of two surfaces; the commit list is the |
| 92 | + other. Read both. A commit-message fix moves the head and so costs a re-stamp |
| 93 | + (rule 44); a title edit does not. |
| 94 | +- **After a merge:** `git log -1 --format=%B <merge>` answers "what does main |
| 95 | + say"; the PR page answers "what did the branch say last". |
| 96 | +- **A landed-and-wrong message cannot be unlanded.** A follow-up commit puts a |
| 97 | + correction next to it, which is a different artifact from a fix — the remedy |
| 98 | + that works is the one taken before the press. |
| 99 | +- **Never cite a PR title as a landing claim.** #2031 is the counterexample that |
| 100 | + cost nothing; the same mechanism with a wrong title is the one that does. |
0 commit comments