thunderbird-patch-review
Easy-to-use patch review interface for Thunderbird
Contribute: ~marcc/thunderbird-review-plugin@lists.sr.ht
git clone git://mccd.space/thunderbird-patch-review
| Log | Files | Refs | README | LICENSE |
commit 9f6dc91cab6f837c81fe27f7a04acad9d22bc35c parent 862ef5015181b3f592de86497aa40745e3ad784e Author: Pi Agent <agent@pi.local> Date: Sat, 18 Jul 2026 13:21:43 +0200 series: strip replies out of the patch series Re:/Aw:/Fwd: replies to a patch shared the same subject tag as the\noriginal (parseSubject deliberately strips those prefixes), so they were\nswept into the series by sameSeries and rendered as if they were patches.\n\nAdd isReplySubject and skip both the anchor and every folder candidate\nthat carries a reply prefix. Real patches (including a cover letter)\nstill match. Detection of the Review button is unchanged, so opening the\nreview from a reply still finds the original patches in the folder. Diffstat:
| M | extension/modules/patch-detect.js | | | 7 | +++++++ |
| M | extension/modules/series.js | | | 12 | ++++++++++-- |
| M | tests/run.mjs | | | 14 | ++++++++++++++ |
3 files changed, 31 insertions(+), 2 deletions(-)
diff --git a/extension/modules/patch-detect.js b/extension/modules/patch-detect.js
@@ -48,6 +48,13 @@ export function parseSubject(subject) {
return { tag, title: match.groups.title.trim(), prefix, version, n, m };
}
+const REPLY_PREFIX_RE = /^(?:\s*(?:re|aw|fwd?)\s*:\s*)+/i;
+
+/** True if the subject carries a leading Re:/Aw:/Fwd: (etc.) reply prefix. */
+export function isReplySubject(subject) {
+ return REPLY_PREFIX_RE.test(subject || "");
+}
+
/** True if the text contains a unified diff. */
export function bodyLooksLikeDiff(text) {
if (!text) {
diff --git a/extension/modules/series.js b/extension/modules/series.js
@@ -1,7 +1,7 @@
// Collect a full patch series from one displayed message: find the sibling
// patches in the same folder, order them, and fetch their plain-text bodies.
-import { parseSubject, sameSeries, bodyLooksLikeDiff } from "./patch-detect.js";
+import { parseSubject, sameSeries, bodyLooksLikeDiff, isReplySubject } from "./patch-detect.js";
/** Extract the text/plain body from a messages.getFull() part tree. */
export function plainBodyFromPart(part) {
@@ -64,7 +64,12 @@ export async function collectSeries(anchor) {
const anchorInfo = parseSubject(anchor.subject);
const members = new Map(); // headerMessageId -> {header, info}
- members.set(anchor.headerMessageId, { header: anchor, info: anchorInfo });
+ // Replies (Re:/Aw:/Fwd:) to a patch are not part of the series: skip the
+ // anchor itself and every folder candidate that is a reply. The subject
+ // tag still matches through the prefix, so we filter on the prefix here.
+ if (!isReplySubject(anchor.subject)) {
+ members.set(anchor.headerMessageId, { header: anchor, info: anchorInfo });
+ }
if (anchorInfo && anchorInfo.m !== null) {
let candidates = [];
@@ -74,6 +79,9 @@ export async function collectSeries(anchor) {
console.warn("patch-review: folder query failed, reviewing single message", e);
}
for (const header of candidates) {
+ if (isReplySubject(header.subject)) {
+ continue;
+ }
const info = parseSubject(header.subject);
if (!sameSeries(anchorInfo, info)) {
continue;
diff --git a/tests/run.mjs b/tests/run.mjs
@@ -9,6 +9,7 @@ import {
bodyLooksLikeDiff,
isPatchMessage,
sameSeries,
+ isReplySubject,
} from "../extension/modules/patch-detect.js";
import { parsePatchEmail } from "../extension/modules/diff-parse.js";
import { pairRanges, computeIntraline } from "../extension/modules/intraline.js";
@@ -72,6 +73,19 @@ function check(name, cond, extra = "") {
check("isPatchMessage: plain mail", !isPatchMessage("hello", "how are you?\n"));
}
+// --- reply detection -------------------------------------------------------
+
+{
+ check("reply: Re: prefix", isReplySubject("Re: [PATCH v2 3/7] net: fix socket leak"));
+ check("reply: stacked Re:", isReplySubject("Re: Re: [PATCH] fix a bug"));
+ check("reply: Aw/Fwd", isReplySubject("Fwd: [PATCH 0/2] cover") && isReplySubject("aw:[PATCH] x"));
+ check("reply: case-insensitive and spaced", isReplySubject("re : [PATCH] x"));
+ check("reply: bare patch is not a reply", !isReplySubject("[PATCH v2 3/7] net: fix socket leak"));
+ check("reply: cover letter is not a reply", !isReplySubject("[PATCH 0/3] series intro"));
+ // Patches whose title happens to start with the letters "Re" but no colon.
+ check("reply: word 'Request' is not a reply", !isReplySubject("[PATCH] Request handling"));
+}
+
// --- diff parsing -----------------------------------------------------------
{