From fa7da4f31d9f97a71840cbf4637e1611bb17b994 Mon Sep 17 00:00:00 2001 From: Romain Cascino Date: Tue, 23 Jun 2026 11:47:22 +0200 Subject: [PATCH] Attribute the re-applied PR for squash revert-of-revert commits --- src/extractors.test.ts | 12 ++++++++++++ src/extractors.ts | 22 +++++++++++++++++++--- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/src/extractors.test.ts b/src/extractors.test.ts index ab8fc32..98e2885 100644 --- a/src/extractors.test.ts +++ b/src/extractors.test.ts @@ -810,6 +810,18 @@ describe("extractPullRequestNumbersForCommit", () => { expect(result).toEqual([]); }); + // A squash revert-of-revert re-applies the change, so its PR is attributed like a normal + // merge; a plain (or triple) revert removes it and is skipped. Keyed on revert depth parity, + // since squash messages (`Revert "Revert "…"" (#N)`) carry the nesting in the title, not a branch. + it.each([ + ['Revert "Title" (#79504)', [], "plain squash revert (odd depth)"], + ['Revert "Revert "Title"" (#79620)', [79620], "squash revert-of-revert (even depth)"], + ['Revert "Revert "Revert "Title""" (#5)', [], "triple squash revert (odd depth)"], + ])("message %j should yield %j (%s)", (message, expected) => { + const result = extractPullRequestNumbersForCommit({ sha: "abc", message }); + expect(result).toEqual(expected); + }); + // Numbers above the GraphQL Int (32-bit) bound cannot be GitHub PR numbers // and must be filtered so they don't poison the release sync mutation. it.each([ diff --git a/src/extractors.ts b/src/extractors.ts index 2864400..1061a62 100644 --- a/src/extractors.ts +++ b/src/extractors.ts @@ -303,9 +303,10 @@ export function extractPullRequestNumbersForCommit(commit: CommitContext): numbe const rawMessage = commit.message ?? ""; - // Reverts reference the original PR, not a new one. - if (/^Revert "/i.test(rawMessage)) { - verbose(`Skipping revert commit ${commit.sha} with message: "${rawMessage}"`); + // Odd depth = a net revert; its `(#N)` is the revert's own PR, undoing prior work, so skip it. + // Even depth (revert-of-revert) re-applies the change and must attribute its PR like a normal merge. + if (leadingRevertDepth(rawMessage) % 2 === 1) { + verbose(`Skipping odd-depth revert commit ${commit.sha} with message: "${rawMessage}"`); return []; } if (getRevertBranchDepth(commit.branchName) % 2 === 1) { @@ -453,6 +454,21 @@ export function getRevertMessageDepth(message: string | null | undefined): numbe return parseRevertMessage(message).depth; } +/** + * Count leading `Revert "` wrappers. Unlike {@link getRevertMessageDepth} this does not + * require balanced quotes on the subject line, so a revert whose quoted title spans + * multiple lines (e.g. reverting a GitLab merge commit) still counts as depth 1. + */ +function leadingRevertDepth(message: string): number { + let text = message; + let depth = 0; + while (/^Revert "/i.test(text)) { + depth++; + text = text.replace(/^Revert "/i, ""); + } + return depth; +} + /** Extract identifiers being reverted. Returns [] if not an odd-depth revert. */ export function extractRevertedIssueIdentifiersForCommit(commit: CommitContext): ExtractedIdentifier[] { if (!commit) return [];