Skip to content

Commit c1d32a5

Browse files
committed
fix: Tolerate cosmetic subject variation in auto-supersession
1 parent b38b740 commit c1d32a5

2 files changed

Lines changed: 141 additions & 14 deletions

File tree

src/bone/digest.clj

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -333,14 +333,19 @@
333333
(log/info "Auto-closed patch" log-msg)))
334334

335335
(defn- normalize-subject
336-
"Strip Re:/Fwd: prefixes and bracketed tags to get the base subject."
336+
"Base subject as space-joined tokens: strip Re:/Fwd: prefixes,
337+
bracketed tags and parenthesized version markers (\"(v3)\"), then keep
338+
alphanumeric tokens only, so punctuation differences don't count.
339+
Nil when nothing distinctive remains."
337340
[subject]
338-
(when subject
339-
(-> subject
340-
(str/replace #"(?i)^(\s*(Re|Fwd)\s*:\s*)+" "")
341-
(str/replace #"\[[^\]]*\]\s*" "")
342-
str/trim
343-
str/lower-case)))
341+
(some->> (some-> subject
342+
(str/replace #"(?i)^(\s*(Re|Fwd)\s*:\s*)+" "")
343+
(str/replace #"\[[^\]]*\]\s*" "")
344+
(str/replace #"(?i)\(\s*v(er|ersion)?\.?\s*\d+\s*\)" "")
345+
str/lower-case)
346+
(re-seq #"[a-z0-9]+")
347+
seq
348+
(str/join " ")))
344349

345350
(def ^:private patch-pull [:patch/filename :patch/subject])
346351

@@ -459,23 +464,34 @@
459464
"Close open patch reports (thread-adjacent, or from the same sender
460465
and tracing back to a common ancestor -- see `supersede-candidate-eids`)
461466
sharing the new patch's base subject (Re:/[TAG] stripped). Handles
462-
unnumbered re-sends, including ones posted on a different thread branch."
467+
unnumbered re-sends, including ones posted on a different thread branch.
468+
When the subject normalizes to nothing (a bare \"[PATCH]\"), matching
469+
patch identifiers on both sides stand in for it."
463470
[conn report-eid email candidate-eids]
464471
(let [new-subj (normalize-subject (:email/subject email))
465472
db (d/db conn)
466473
new-idents (patch-idents
467-
(d/pull db [{:report/patches patch-pull}] report-eid))]
468-
(when (and new-subj (seq candidate-eids))
474+
(d/pull db [{:report/patches patch-pull}] report-eid))
475+
match? (fn [r]
476+
(let [r-idents (patch-idents r)]
477+
(if new-subj
478+
(and (= new-subj
479+
(normalize-subject
480+
(get-in r [:report/email :email/subject])))
481+
;; ... but veto plainly different patches.
482+
(not (patch-idents-conflict? new-idents r-idents)))
483+
;; No subject signal: require identical idents.
484+
(and (seq new-idents) (seq r-idents)
485+
(not (patch-idents-conflict? new-idents r-idents))))))]
486+
(when (seq candidate-eids)
469487
(doseq [rid candidate-eids
470488
:when (not= rid report-eid)]
471489
(let [r (d/pull db [:report/type :report/closed :report/message-id
472490
{:report/email [:email/subject]}
473491
{:report/patches patch-pull}] rid)]
474492
(when (and (= :patch (:report/type r))
475493
(not (:report/closed r))
476-
(= new-subj (normalize-subject (get-in r [:report/email :email/subject])))
477-
;; ... but veto plainly different patches.
478-
(not (patch-idents-conflict? new-idents (patch-idents r))))
494+
(match? r))
479495
(auto-supersede-patch!
480496
conn rid report-eid email
481497
(str (:report/message-id r) " (superseded by same-subject thread patch)"))))))))

test/bone/digest_test.clj

Lines changed: 112 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
Ported from test/bone-digest-test.clj (bb version)."
88
(:require [clojure.edn :as edn]
99
[clojure.string :as str]
10-
[clojure.test :refer [deftest is testing use-fixtures]]
10+
[clojure.test :refer [are deftest is testing use-fixtures]]
1111
[datalevin.core :as d]
1212
[bone.commands :as commands]
1313
[bone.common :as common]
@@ -2097,6 +2097,117 @@
20972097
(finally
20982098
(teardown! ctx))))))
20992099

2100+
(deftest normalize-subject-widening
2101+
(testing "parenthesized version markers and punctuation don't split
2102+
otherwise-identical subjects (real corpus cases), but
2103+
distinct wording still does"
2104+
(let [norm #'digest/normalize-subject]
2105+
(are [a b] (= (norm a) (norm b))
2106+
"Re: [PATCH] (v3) New LaTeX code export option: engraved"
2107+
"[PATCH] New LaTeX code export option: engraved"
2108+
"[PATCH v1] org-agenda-clock-goto: Jump to closest entry and respect, filtering"
2109+
"[PATCH v2] org-agenda-clock-goto: Jump to closest entry and respect filtering"
2110+
"[PATCH] ox-md.el export code blocks using grave accents."
2111+
"Re: [PATCH] ox-md.el export code blocks using grave accents"
2112+
"Re: [PATCH] org-protocol: decode \"+\" in query part as space (v2)"
2113+
"[PATCH] org-protocol: decode \"+\" in query part as space")
2114+
(are [a b] (not= (norm a) (norm b))
2115+
"[PATCH] Speed up tangling" "[PATCH] Improve tangling"
2116+
;; parenthesized text that is NOT a version marker is kept
2117+
"add tests for ob-haskell (ghci)" "add tests for ob-haskell"
2118+
;; bare numbers are significant (series positions, counts)
2119+
"[PATCH] Fix part 2" "[PATCH] Fix part 3")
2120+
(is (nil? (norm "[PATCH]"))
2121+
"a subject with nothing but tags normalizes to nil, never to a matchable empty string"))))
2122+
2123+
(deftest supersede-tolerates-version-marker-and-punctuation
2124+
(testing "an unnumbered re-send whose subject differs only by a
2125+
parenthesized version marker and punctuation still
2126+
supersedes the original (real cases: '(v3) New LaTeX code
2127+
export option' / 'respect, filtering')"
2128+
(let [{:keys [conn] :as ctx} (setup-db!)]
2129+
(try
2130+
(store-and-process! conn
2131+
(mk-email {:mid "<v1@test.org>"
2132+
:subject "[PATCH] widget: fix the thing, properly"
2133+
:from "user@test.org"
2134+
:date #inst "2026-06-08T19:15:00"
2135+
:body "Initial patch.\n"})
2136+
"direct")
2137+
(store-and-process! conn
2138+
(assoc (mk-email {:mid "<v2@test.org>"
2139+
:subject "Re: [PATCH] (v2) widget: fix the thing properly"
2140+
:from "user@test.org"
2141+
:date #inst "2026-06-12T14:59:00"
2142+
:in-reply-to "<v1@test.org>"
2143+
:body "Updated version of the patch.\n"})
2144+
:email/attachments [{:attachment/filename "0001-fix.patch"}])
2145+
"direct")
2146+
(let [db (d/db conn)
2147+
v1 (get-report db "<v1@test.org>")
2148+
v2 (get-report db "<v2@test.org>")]
2149+
(is (some? v2) "v2 report was created")
2150+
(is (some? (:report/closed v1))
2151+
"v1 closed despite the (v2) marker and comma in one subject")
2152+
(is (= :superseded (:report/close-reason v1)))
2153+
(is (nil? (:report/closed v2)) "v2 itself stays open"))
2154+
(finally
2155+
(teardown! ctx))))))
2156+
2157+
(deftest supersede-bare-patch-subject-falls-back-to-idents
2158+
(testing "a bare '[PATCH]' subject normalizes to nil, so it can't
2159+
subject-match anything (no more \"\" == \"\" collisions);
2160+
instead, matching patch identifiers on both sides stand in
2161+
for the subject (real case: 87fstreec8, tecosaur's
2162+
proportional image widths patch resent as 'Re: [PATCH]')"
2163+
(let [{:keys [conn] :as ctx} (setup-db!)]
2164+
(try
2165+
(store-and-process! conn
2166+
(assoc (mk-email {:mid "<a@test.org>"
2167+
:subject "[PATCH]"
2168+
:from "user@test.org"
2169+
:date #inst "2021-05-01T00:00:00"
2170+
:body "Patch A.\n"})
2171+
:email/attachments
2172+
[(fp-attachment "0001-org-Display-proportional-image-widths.patch"
2173+
"[PATCH] org: Display proportional image widths")])
2174+
"direct")
2175+
;; B: also subject-less, direct reply, but a DIFFERENT patch.
2176+
(store-and-process! conn
2177+
(assoc (mk-email {:mid "<b@test.org>"
2178+
:subject "Re: [PATCH]"
2179+
:from "user@test.org"
2180+
:date #inst "2021-05-02T00:00:00"
2181+
:in-reply-to "<a@test.org>"
2182+
:body "Patch B, unrelated.\n"})
2183+
:email/attachments
2184+
[(fp-attachment "0001-ox-html.el-remove-CDATA-strings.patch"
2185+
"[PATCH] ox-html.el: remove CDATA strings")])
2186+
"direct")
2187+
(let [db (d/db conn)
2188+
a (get-report db "<a@test.org>")]
2189+
(is (nil? (:report/closed a))
2190+
"A stays OPEN: no subject signal and B's idents differ"))
2191+
;; C: subject-less re-send of the SAME patch -- idents stand in.
2192+
(store-and-process! conn
2193+
(assoc (mk-email {:mid "<c@test.org>"
2194+
:subject "Re: [PATCH]"
2195+
:from "user@test.org"
2196+
:date #inst "2021-05-03T00:00:00"
2197+
:in-reply-to "<a@test.org>"
2198+
:body "Patch A, updated.\n"})
2199+
:email/attachments
2200+
[(fp-attachment "0001-org-Display-proportional-image-widths.patch"
2201+
"[PATCH] org: Display proportional image widths")])
2202+
"direct")
2203+
(let [db (d/db conn)
2204+
a (get-report db "<a@test.org>")]
2205+
(is (some? (:report/closed a))
2206+
"A closed: C carries the same patch, idents replace the missing subject")
2207+
(is (= :superseded (:report/close-reason a))))
2208+
(finally
2209+
(teardown! ctx))))))
2210+
21002211
;; ---------------------------------------------------------------------------
21012212
;; report-entity :report/has-ics is scoped to announcements
21022213
;; ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)