From 28da3fb0d342d50c835d982ffde4a81c80411d12 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Wed, 30 Sep 2026 03:57:12 -0700 Subject: [PATCH] =?UTF-8?q?docs(runbooks):=20what=20a=20squash=20merge=20l?= =?UTF-8?q?ands=20=E2=80=94=20a=20one-commit=20(non-merge)=20PR=20takes=20?= =?UTF-8?q?its=20subject=20from=20the=20commit,=20two=20or=20more=20take?= =?UTF-8?q?=20the=20PR=20title=20(TASK-223)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Written by sprint-impl, a Commonly agent. Pod thread: https://commonly.me/v2/pods/6a692a1be833c668acdb84cf (message 75968) Two surfaces compose a squash message and a repository setting decides which one supplies the subject. `squash_merge_commit_title: COMMIT_OR_PR_TITLE` means one non-merge commit takes the commit's subject and two or more take the PR title; `squash_merge_commit_message: COMMIT_MESSAGES` means the body is always the branch's commit messages. Both read from the repo, so the default composition is decidable before a press. The PR body is not an input. The count is of NON-MERGE commits, and merge commits are excluded from both the count and the body bullets (#1965 7+1 -> 7, #1901 6+1 -> 6, #1905 5+1 -> 5, #1906 4+1 -> 4). THE SUBJECT CAN BE OVERRIDDEN, and that is measured rather than assumed. Three landings took a subject the setting does not predict, all merged 2026-09-08, in an older window of 194 merged PRs (#1534-#1749). #1645 matches NEITHER the PR title nor the commit's subject (0 rename events); #1623 landed the PR TITLE and #1644 landed commit 1's subject, each BYTE-EXACT, where the count predicts the other candidate. A stale prefill - the dialog seeding its subject field when it RENDERS, so a box opened earlier is pressed carrying an older subject; INFERRED, never read, since no API field records the client or the box - accounts for #1644 (commit 1 at 20:24:15Z, commit 2 at 20:29:26Z, the press at 20:52:58Z, so a box opened in that window carried commit 1's subject) and for NEITHER of the other two unless that inference holds: #1623's branch held one non-merge commit for its whole life with 0 force-push events, so no render of it could have offered the title if the dialog prefills by the count the landings follow. Five branches in the same window carrying main-merges landed the commit's subject where counting every commit would predict the title (#1678 1+1, #1647 1+2, #1574 1+1, #1558 1+2, #1539 1+1) - which is what makes #1623 an override and not a conforming multi-commit landing. Zero overrides in the 297 merged PRs measured below (#1750-#2053, contiguity-checked). So an override is detectable — a landed subject the setting and the count do not predict — and a census is a claim about its own window, not about the practice. The "the setting was different then" reading is pre-empted by the same window: 33 of its 96 discriminating landings took the commit's subject, 32 of them on a branch holding one non-merge commit, which a PR-title setting cannot produce without an override each. Census, 297 merged PRs, restricted to the discriminating set — the 82 PRs whose title and first commit subject differ, the only set where the two candidates can be told apart: one non-merge commit landed the commit's subject 33 of 33; two or more landed the PR title 49 of 49. No exceptions. Six of the nine merges of 2026-09-30 had title == first commit subject and could not discriminate at all; the three that could (#2024, #2031, #2049) follow the setting. Eight corrections this revision carries: four of them mine, two sprint-review's and two ux-lead's, plus an addition at (5) that the later ones correct. The first draft read #2031's landed subject (the commit's, not the title's) as a presser overwriting the box and generalised from it; with the setting read, #2031 is the one-commit default. The second reported "50 of 51" with #1964 as an unexplained residual — a classifier artifact, since `gh pr view --json commits` counts merge commits, and #1964's branch is 1 non-merge + 1 merge and landed in the one-commit shape with no bullets. The third: the commit message said an editable box "has never been seen being edited", which is a claim about the whole history made from a 297-PR window, and ux-lead refuted it with #1644 and #1645. The fourth, also ux-lead's: this revision claimed only #1645 needed an edit, treating a stale prefill as an explanation for #1623 - whose one non-merge commit never grew, so no prefill following the landings' count could have offered its title - and mislabelled the census window #1696-#2053 when the fetch, capped at 300 rows sorted by UPDATED, spanned #1750-#2053 and missed three merged rows inside it (#1752, #1753, #1765, since read and conforming). Both fixed; every claim the doc makes now names the window it was measured in. (5) An addition rather than a correction: `git log --no-merges -1 ` means "newest non-merge commit REACHABLE", so on a merge-tipped branch it hands back another PR's commit on main (#1623 -> "...(#1626)", #1574 -> "...(#1576)"); the recipe now ranges from the merge base. (6) A correction to (5) itself, and it is sprint-review's: the control half of (5) was wrong. #1964's tip IS a merge (3e7ce46a, parents 3a658321 own + ff8de0fa merged-in), so merge-tip is NECESSARY AND NOT SUFFICIENT for the trap. What spares #1964 is DATE ORDER, by 40 seconds - its own commit at 11:17:28Z against the merged-in 11:16:48Z - while #1623 loses the same race by 30 minutes (own a9c6cb28 at 06:55:46Z against main's 9f759062, #1626, at 07:25:57Z), which is why #1623's unranged read returns the SEO guide. #1964 is therefore a NEAR MISS, not a clean control, and the clean control is a tip that is not a merge, where the branch's own commit is the newest thing reachable: #2051, #2053, #1752, #1753, #1765, #1645, #2031, one parent each and origin/main an ancestor of none of those tips, each reading its own commit unranged. The doc carries the corrected condition in both the recipe and the habits bullet. (7) Also sprint-review's, and again about (5): the doc measured the trap "on the seven merge-tipped branches in this population, six returned a different PR's commit". That population - every merge-carrying row of #1534-#1749 holding one non-merge commit - contains 22 such branches, all 22 merge-tipped, and 21 of them return a different commit, so 6-of-7 understated the hazard 3.5x; "this population" also had no stated boundary in the doc. The one quiet row is #1651, and it is quiet for the same reason #1964 survives: its own 1270011e (05:26:06-07:00) is newer than the tip it merged in, f3ae3798 (00:49:19-07:00), so the date-ordered walk never leaves the branch. Coincident candidates do not make a row quiet - 15 of the 16 rows in that population whose title equals their first commit subject are among the 21 that fire. The doc now states the window, the ratio, and #1651's measured reason. (8) Both of ux-lead's. (a) The contiguity split was wrong: two of the range's 304 numbers are ISSUES, not PRs (#1821 closed, #1959 open), so the split is 302 pull requests - 297 merged, 3 closed unmerged (#1784, #1903, #1967), 2 open (#1751, #1768) - and 2 issues, checked one at a time with the `pull_request` key on `gh api .../issues/N`, not "297 merged, 3 unmerged, 4 open" of 304 PR numbers. (b) The doc and this message stated the prefill as measured while the PR ask called it an inference, and the ask was the honest one: no prefill was ever read, because the measured rule is about what LANDS. #1623's exclusion is therefore conditional on the dialog prefilling by the same count, and if it counts merges instead then #1623 needed no edit and the merge-carrying landings that took a one-commit-predicted subject are the ones to explain. Every surface now says inferred; ux-lead notes their own 75d82be0 wording was where the overclaim entered. Also recorded because the doc quotes them: `git log --grep="(#N)$"` resolves the WRONG commit when a later commit on main also names that PR (#1677 returned de5fc4d83e, actual f8ad3bde6a; #1877 8235be3d2f vs e3d95501a1), so the recipe uses the PR's mergeCommit; `gh pr view --json commits` truncates headlines (17 of 22 in the nine-PR set, each to 69 characters plus an ellipsis, longest intact 72); and amending the tip does not amend earlier commits — #2049's merge message carries commit 1's retracted framing at line 13 and its retraction at line 58. Scope: this is the message, not the tree. Fidelity of what landed to what was reviewed stays rule 32's patch-id comparison; the tree/origin check is rule 49. --- docs/runbooks/what-a-squash-lands.md | 301 +++++++++++++++++++++++++++ 1 file changed, 301 insertions(+) create mode 100644 docs/runbooks/what-a-squash-lands.md diff --git a/docs/runbooks/what-a-squash-lands.md b/docs/runbooks/what-a-squash-lands.md new file mode 100644 index 000000000..1766430d5 --- /dev/null +++ b/docs/runbooks/what-a-squash-lands.md @@ -0,0 +1,301 @@ +# What a squash merge lands + +A squash merge writes **one** commit to main. Its message is composed from two +surfaces — the branch's **commit messages** and the **PR title** — and *which one +supplies the subject is decided by the repository setting, on the count of +non-merge commits*. The PR *body* is not an input at all. + +The setting is readable, and reading it makes the composition decidable without +waiting for a merge: + +```bash +gh api repos/Team-Commonly/commonly \ + -q '{squash_title: .squash_merge_commit_title, squash_message: .squash_merge_commit_message}' +# {"squash_title":"COMMIT_OR_PR_TITLE","squash_message":"COMMIT_MESSAGES"} +``` + +`COMMIT_OR_PR_TITLE` means **one commit → that commit's subject; two or more → +the PR title**. `COMMIT_MESSAGES` means the body is the branch's commit +messages, always. Both counts skip **merge commits**, which matters on any branch +that merged main into itself — see below. + +This is the **default** composition, not a law. The presser can set the subject +at the press — GitHub's merge dialog exposes the box, and `gh pr merge -t/--subject` +sets it from the CLI — and an override is detectable: it is a landed subject that +the setting and the non-merge count do not predict. Three are +measured [below](#an-override-exists--and-it-is-measurable). + +## The composition + +| PR shape (the default) | landed subject | landed body | +|---|---|---| +| **1 non-merge commit** | the **commit's** subject + ` (#N)` | that commit's message **minus its first line** | +| **2+ non-merge commits** | the **PR title** + ` (#N)` | per commit, in order: `* `, a blank line, then that commit's message body | + +A merge commit on the branch is **excluded from both**: it is not counted when +choosing the subject and it gets no `* ` bullet in the body. Measured on four +branches that merged main into themselves — #1965 (7 non-merge + 1 merge → **7** +bullets), #1901 (6 + 1 → 6), #1905 (5 + 1 → 5), #1906 (4 + 1 → 4) — and on +**#1964**, whose branch carries **1 non-merge commit + 1 merge commit** and whose +landed message is in the **one-commit** shape, the commit's subject with no +bullets at all. + +## What the census shows + +Measured over **the 297 merged PRs numbered #1750–#2053** (`gh api +…/pulls?state=closed`, each merge commit read with `git log -1 --format=%B`). That +range is contiguity-checked: of its 304 numbers, **302 are pull requests** — 297 +merged, 3 closed unmerged (#1784, #1903, #1967) and 2 open (#1751, #1768) — and +**2 are issues** (#1821 closed, #1959 open), each checked with the `pull_request` +key on `gh api …/issues/N`. The original fetch, capped at 300 rows *sorted by +updated*, missed three of the merged ones inside it (#1752, #1753, #1765); each +was read afterwards and all three are conforming. Restricted to the +**discriminating** set — +the 82 PRs whose PR title and first commit subject differ, which is the only set +where the two candidates for the subject can be told apart: + +| non-merge commits | landed the **commit's** subject | landed the **PR title** | +|---|---|---| +| **1** | **33 of 33** | 0 | +| **2+** | 0 | **49 of 49** | + +The classifier is the count of commits with fewer than two parents; `gh pr view + --json commits` counts merge commits too and will call a one-commit branch +"2 commits". That single mis-classification produced the only apparent exception +in an earlier draft of this census (**#1964**, above) and with it a residual +"unexplained" case that was really a classifier artifact. + +Across the nine PRs this pod landed between 09:08:43Z and 09:19:20Z on +2026-09-30, **six had `title == first commit subject`**, so they cannot +distinguish the two surfaces and say nothing about which one won. The three that +discriminate are all the rule: + +| PR | non-merge commits | landed subject | +|---|---|---| +| #2024 | 2 | the PR title + ` (#2024)` | +| #2049 | 2 | the PR title + ` (#2049)` | +| #2031 | **1** | **the commit's** subject + ` (#2031)` | + +The body shape is a second, independent classifier — a multi-commit landing emits +one line-initial `* ` per non-merge commit — and it agrees: 0 exceptions across +this set. It is the weaker of the two, because a bulleted line inside a +*one-commit* PR's commit body would look multi; in this population it does not +happen (0 of the 33 one-commit landings contain a line-initial `* `), but when +the answer matters, count the non-merge commits. + +Within that window **no landing needs an override**: the 82 discriminating rows +all take the surface the count predicts, and the other 215 rows, where the two +candidates are identical, all landed that shared text — which an override would +have displaced. That window is why this census is a claim about 297 landings and +not about the practice; the next section measures an older window where +overrides do occur. + +## #2031 was the one-commit default, not an overwrite + +#2031 has one commit, and its PR title differs from that commit's subject +(`… (TASK-204 late delta)`). Its landed subject is the **commit's** own subject +with ` (#N)` appended. An earlier draft of this doc read that as the presser +overwriting the subject box, and inferred a general rule ("the box is editable, +so the title is not what landed") from a single case that the setting already +explains. That inference was the error, not the observation: **under this setting a +one-commit PR takes its subject from the commit, so #2031 is the default in +action** — *default*, not "always": #1623 below is a one-non-merge-commit branch +that landed its PR title. + +## An override exists — and it is measurable + +The subject at the press is editable — the merge dialog's box, or +`gh pr merge -t/--subject` — and it **has been used**. +Measured in an older window — **194 merged PRs, #1534–#1749**, against **zero** +in the 297 above — three landings took a subject the setting does not predict, +all three merged on **2026-09-08**: + +| PR | non-merge (+ merge) commits | landed subject | how it relates to the two candidates | +|---|---|---|---| +| #1645 | 1 (+0) | matches **neither** | equals **neither** the PR title nor the commit's subject; 0 rename events | +| #1623 | 1 (+3) | the **PR title**, byte-exact | equals a candidate; the count predicts the commit's subject | +| #1644 | 2 (+1) | **commit 1's** subject, byte-exact | equals a candidate; the count predicts the title | + +All three fail the prediction. What separates them is whether a **stale prefill** +could account for the landing instead of an edit: a merge dialog seeding its +subject field when it *renders*, so that a box opened while the branch held fewer +commits is pressed carrying an older subject. **That mechanism is inferred, never +read** — no API field records which client pressed or what the box held — and +every bullet below inherits the hedge. + +- **#1623 cannot be explained that way — on a premise that is inferred, not + read.** Its branch held exactly **one non-merge commit for its whole life** — + three main-merges followed it — and the timeline records **0 force-push + events**. The measured rule is about what *lands*: a branch with one non-merge + commit lands the **commit's subject**. If the dialog prefills by that same + count, no render of it could have offered the title, and it landed the title, + byte-exact. **No prefill has ever been read**: if the dialog counts merges, + #1623 needed no edit, and the five merge-carrying landings below become the ones + to explain. +- **#1645 cannot be explained that way either**: one commit, no merges, and a + landing matching *neither* candidate. +- **#1644 can**: commit 1 landed 20:24:15Z, commit 2 20:29:26Z, and the press + 20:52:58Z, so a dialog rendered inside that five-minute window prefilled commit + 1's subject and was pressed stale. Available, and unverifiable from the API — + which is why this row is evidence the *prediction* can fail, not evidence of an + edit. + +That the rule counts **non-merge** commits even when merges are present is what +makes #1623 an override rather than a conforming multi-commit landing, and it is +measured directly in the same window: **five** branches carrying main-merges landed +the **commit's** subject where counting every commit would have predicted the title +— #1678 (1 non-merge + 1 merge), #1647 (1 + 2), #1574 (1 + 1), #1558 (1 + 2), +#1539 (1 + 1). #1647 is the shape #1623 has, landing the other way. + +The alternative reading — that the setting was something else on 2026-09-08 — +does not survive the same window: **33 of its 96 discriminating landings took the +commit's subject, 32 of them on a branch holding one non-merge commit** (the 33rd +is #1644 above), and a PR-title setting produces none of those without an override +each. The setting was `COMMIT_OR_PR_TITLE` there too, which is what makes the +three rows above overrides rather than a different rule. The other 98 rows of that +window have `title == first commit subject` and cannot discriminate at all. + +So an override is detectable: **a landed subject the setting and the non-merge +count do not predict** — matching neither candidate (#1645), or matching the +candidate the count rules out (#1623, #1644). Where a stale prefill is unavailable +the box is what remains: for #1645 outright, and for #1623 on the inferred premise +above. The census +above supports a claim about *its own window*, not about the practice: 0 +overrides in the 297 landings of #1750–#2053, 3 in the 194 of #1534–#1749. A +landed subject is never evidence about the PR title either way, and the merge +commit is the only reader that settles what landed. + +## Why the commit messages are the surface that always bites + +The body is always the branch's commit messages, and for a one-commit PR the +subject is a commit message too. So a claim that must not land is fixed in the +**commit message** first, and only then on the PR title — and specifically: + +- **1 non-merge commit:** the commit message supplies **both** the subject and + the body. A wrong PR title changes nothing that lands; a wrong commit message + changes everything that lands. +- **2+ non-merge commits:** the **title** supplies the subject, the **commit + messages** supply the body. Fixing the title does not fix a body claim, and + amending the tip does not fix an earlier commit's text. + +Nothing else reaches either surface: a PR *body* edit is not an input to the +composition at all, and a PR comment reaches neither. + +**Amending the tip does not amend earlier commits.** Measured on #2049, whose +branch carried two commits with the tip amended to retract an over-claim: main's +merge message today carries both texts, side by side — + +``` +line 13: `split('@media (prefers-reduced-motion')[1]` is 865 lines / 33,883 characters +line 58: right; calling it "fiction about the code" was the wrong reason, and is retracted. +``` + +— the retracted framing in commit 1's body and its retraction in commit 2's, in +one message. `git log -1 --format=%B ` is the only reader that shows +this. The PR page shows the current branch state, which is what the author last +saw; main shows what landed. + +## Checking what landed — and what will land + +```bash +PR=2049 +M=$(gh pr view "$PR" --json mergeCommit -q .mergeCommit.oid) # the merge commit, by identity +git log -1 --format=%s "$M" # the landed subject +git log -1 --format=%B "$M" # the landed body — the surface main reads +gh pr view "$PR" --json title -q .title # the subject for a 2+-commit PR +gh api repos/Team-Commonly/commonly/commits/ -q .commit.message # lossless branch message +``` + +Resolve the merge commit from the PR, never by grepping a message for `(#N)`. +Measured: `git log --format=%H --grep="(#1677)\$" -1 origin/main` returns +`de5fc4d83e`, while #1677's merge commit is `f8ad3bde6a` — both end with +`(#1677)`, because a *later* commit on main also names that PR, and there are two +such commits. The same happens for #1877 (`8235be3d2f` returned, +`e3d95501a1` actual). A grep for a string that other messages legitimately +contain fails by returning a plausible wrong commit, which is worse than +returning nothing. + +To predict the subject before the press, count the branch's **non-merge** +commits, because that count — not the title — picks the surface: + +```bash +gh pr view "$PR" --json commits -q '.commits[].oid' | while read oid; do + gh api repos/Team-Commonly/commonly/commits/$oid -q '.parents|length' +done | sort | uniq -c # parents == 1 is a non-merge commit; 2 is a merge and does not count +``` + +Reading the branch's **own** commit is a separate instrument, and it needs a +range. `git log --no-merges -1 ` means "the newest non-merge commit +*reachable* from ref", walking *through* merge commits into everything they merged +in — and once main is reachable that way, the read can return main's commit +instead of the branch's. In the **22 merge-carrying rows of the #1534–#1749 +window** — one non-merge commit each, and a merge tip on every one of them — +**21 returned a *different* commit**: #1623 → `feat(seo): add AI agent stop +conditions guide (#1626)`, #1574 → `… (#1576)`, #1647 → `… (#1651)`, #1558, #1539 +and #1678 likewise, and fifteen more. The one quiet row is **#1651**, quiet for +the same reason #1964 survives below: its own `1270011e` (05:26:06-07:00) is +*newer* than the tip it merged in, `f3ae3798` (00:49:19-07:00), so the date-ordered +walk never leaves the branch. Coincident candidates are not what makes a row +quiet — 15 of the 16 rows here whose title equals their first commit subject are +among the 21 that fire. Range it from the merge base, or read each commit object +through the API: + +```bash +git log --no-merges --format=%s "$(git merge-base origin/main "$REF")..$REF" +``` + +A merge tip is **necessary but not sufficient**, and **#1964** — from the 297-row +window, not the 22 above — shows why. Its tip `3e7ce46a` *is* a merge — parents +are its own `3a658321` (11:17:28Z) and the merged-in `ff8de0fa` (11:16:48Z) — and +the unranged read still returns #1964's own commit, because the walk is +**date-ordered** and that commit is **40 seconds newer**. #1623 has the same shape +and loses the same race by **30 minutes**: its own `a9c6cb28` at 06:55:46Z against +main's `9f759062` (#1626) at 07:25:57Z, which is the whole reason the unranged +read hands back the SEO guide. So **#1964 is a near miss, not a control** — which +commit a merge tip yields is decided by timestamps, and a 40-second margin is not +something a reader can check. The clean control is a tip that is **not** a merge, +with the branch's own commit the newest thing reachable: measured on #2051, #2053, +#1752, #1753, #1765, #1645 and #2031, one parent each, `origin/main` an ancestor +of **none** of those tips, and the unranged read returning that same commit in all +seven. + +One instrument note, measured. `gh pr view --json commits -q +'.commits[] | .messageHeadline'` is **lossy**: across the nine PRs' 22 commits, +17 headlines ended in `…` and every one of those measured exactly 70 characters +(69 plus the ellipsis), while the longest intact one is 72 — so the cut lands +somewhere between, and the projection counts commits faithfully while quoting +them wrongly. Read the commit object above for the text itself; the branch +commits survive the branch deletion inside the PR record. And the composition is +a fact about the **message** only: whether what landed is what was reviewed is a +separate measurement — the patch-id comparison in rule 32 of +`docs/development/review-checklist.md`, and the tree/origin check in rule 49. + +## Habits this buys + +- **Never read a branch commit without a range.** `git log --no-merges -1 + ` can hand you another PR's commit whenever the tip is a merge **and + something it merged in is newer than the branch's own commit** — it means + "newest non-merge commit reachable", ordered by date, and main is reachable. A + merge tip is necessary for that and not sufficient: #1964 survives the same + shape by 40 seconds that #1623 loses by 30 minutes, and of the 22 merge-tipped + rows in one window, 21 returned a different commit. A wrong commit reads exactly + like a right one — range from `git merge-base`. +- **Before a press, count the non-merge commits.** One: the **commit message is + the landed message** — subject and body — so fix it in the commit and do not + rely on the PR title. Two or more: the **title is the subject** and the commit + messages are the body, so a subject fix is a title edit and a text fix is a + commit amend. A commit-message fix moves the head and so costs a re-stamp + (rule 44); a title edit does not. +- **A credit line must be in the commit message.** It lands in the body in every + shape, and in the subject too for a one-commit PR; a PR-body line lands + nowhere. +- **After a merge:** `git log -1 --format=%B ` answers "what does main + say"; the PR page answers "what did the branch say last". +- **A landed-and-wrong message cannot be unlanded.** A follow-up commit puts a + correction next to it, which is a different artifact from a fix — the remedy + that works is the one taken before the press. +- **Never cite a PR title as a landing claim.** It is the subject for 2+ commits + and irrelevant for one commit; the merge commit is the only reader that knows. +- **A landing that matches neither candidate is an override**, not a rule you + have not found yet: check the non-merge count and the title's rename history + before concluding anything about the mechanism.