Skip to content

Commit f5a8a82

Browse files
committed
fix: Store report patches at creation time
1 parent b669140 commit f5a8a82

2 files changed

Lines changed: 93 additions & 8 deletions

File tree

src/bone/digest.clj

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,11 @@
162162
(some? (d/entid db [:report/message-id message-id])))
163163

164164
(defn report-entity
165-
"Build the entity map for a new report from email data."
165+
"Build the entity map for a new report from email data.
166+
Patches are stored here, at creation: they depend only on the email's
167+
own content, so a reply flagged :email/pending-thread? (Phase 4
168+
skipped) still surfaces them immediately instead of waiting for the
169+
TTL flush."
166170
[email-eid message-id report-info email-date email now]
167171
(let [attachments (:email/attachments email)
168172
body-text (common/email-body-text email)
@@ -172,7 +176,8 @@
172176
has-ics (and (= :announcement (:type report-info))
173177
(or (common/has-ics-attachment? attachments)
174178
(common/has-inline-ics? body-text)))
175-
has-text (boolean (some common/text-attachment? attachments))]
179+
has-text (boolean (some common/text-attachment? attachments))
180+
patches (detect/build-patch-entities email)]
176181
(into {:report/type (:type report-info) :report/email email-eid
177182
:report/message-id message-id
178183
:report/last-activity (or email-date now)}
@@ -183,7 +188,8 @@
183188
:report/topic (when (:topic report-info) email-eid)
184189
:report/topic-value (:topic report-info)
185190
:report/patch-seq (:patch-seq report-info) :report/patch-source (:patch-source report-info)
186-
:report/has-ics has-ics :report/has-text-attachments has-text})))
191+
:report/has-ics has-ics :report/has-text-attachments has-text
192+
:report/patches (when (seq patches) patches)})))
187193

188194
(defn- create-report!
189195
"Create a new report entity. Returns the entity id of the new report."
@@ -437,9 +443,10 @@
437443
(seq nearest-eids)) (conj :close-previous-version)
438444
(and (= :patch rtype) (seq nearest-eids)) (conj :close-superseded-thread)
439445
(and (= :patch rtype) (:patch-seq report-info)) (conj :manage-series)
440-
;; Store patch entities on ANY report type carrying patch content,
441-
;; not just :patch reports. A [BUG] with a .patch attachment
442-
;; gets the patch stored as metadata on the bug.
446+
;; Patches are normally stored at creation (see report-entity);
447+
;; this hook only heals reports created pending by versions that
448+
;; predate creation-time storage. It applies to ANY report type
449+
;; carrying patch content, not just :patch reports.
443450
(seq patches) (conj :store-patches)
444451
;; :auto-series stays patch-only: a synthetic series only makes
445452
;; sense for :patch reports (series tracking is patch-specific).
@@ -629,8 +636,14 @@
629636
(when (:manage-series plan)
630637
(series/manage-series! conn report-eid email report-info from-addr parent-eids))
631638
(when (:store-patches plan)
632-
(d/transact! conn [{:db/id report-eid :report/patches patches}])
633-
(log/info (count patches) "patch file(s) stored"))
639+
;; No-op for reports created since patches moved to creation time
640+
;; (report-entity): the guard prevents duplicate :report/patches
641+
;; components on retry, while still healing reports created pending
642+
;; by older versions whose Phase 4 never ran.
643+
(when (empty? (:report/patches
644+
(d/pull (d/db conn) [{:report/patches [:db/id]}] report-eid)))
645+
(d/transact! conn [{:db/id report-eid :report/patches patches}])
646+
(log/info (count patches) "patch file(s) stored")))
634647
(when (:auto-series plan)
635648
(let [series-eid (series/create-series! conn (:topic report-info) from-addr 1)]
636649
(series/add-patch-to-series! conn series-eid report-eid email)

test/bone/digest_test.clj

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1695,3 +1695,75 @@
16951695
(testing "other types are never flagged, even with an .ics attachment"
16961696
(is (false? (has-ics :bug)))
16971697
(is (false? (has-ics :patch))))))
1698+
1699+
;; ---------------------------------------------------------------------------
1700+
;; report-entity stores :report/patches at creation
1701+
;; ---------------------------------------------------------------------------
1702+
1703+
(def ^:private format-patch-text
1704+
(str "From 0123456789abcdef0123456789abcdef01234567 Mon Sep 17 00:00:00 2001\n"
1705+
"From: Alice <alice@test.org>\n"
1706+
"Date: Thu, 1 May 2026 10:00:00 +0000\n"
1707+
"Subject: [PATCH] Fix the parser\n"
1708+
"\n"
1709+
"---\n"
1710+
"diff --git a/parser.el b/parser.el\n"))
1711+
1712+
(deftest report-entity-stores-patches-at-creation
1713+
(testing "an email with a .patch attachment yields :report/patches"
1714+
(let [email {:email/attachments [{:attachment/filename "0001-fix.patch"
1715+
:attachment/content-type "text/x-diff"
1716+
:attachment/data format-patch-text}]
1717+
:email/author-address "alice@test.org"}
1718+
entity (digest/report-entity 1 "<m@test.org>" {:type :patch} nil email nil)
1719+
patches (:report/patches entity)]
1720+
(is (= 1 (count patches)))
1721+
(is (= "0001-fix.patch" (:patch/filename (first patches))))
1722+
(is (= :attachment (:patch/source (first patches))))))
1723+
(testing "an email without patch content has no :report/patches key"
1724+
(let [entity (digest/report-entity 1 "<m@test.org>" {:type :bug} nil
1725+
{:email/author-address "alice@test.org"
1726+
:email/body-text "it crashes\n"}
1727+
nil)]
1728+
(is (not (contains? entity :report/patches))))))
1729+
1730+
;; ---------------------------------------------------------------------------
1731+
;; Pending emails surface their patches at creation, without duplication
1732+
;; on the TTL-flush retry (regression: patches used to wait on Phase 4,
1733+
;; which pending emails skip)
1734+
;; ---------------------------------------------------------------------------
1735+
1736+
(deftest pending-patch-stored-at-creation
1737+
(testing "A [PATCH] reply whose parent is missing gets its patches immediately."
1738+
(let [{:keys [conn] :as ctx} (setup-db!)]
1739+
(try
1740+
(store-and-process!
1741+
conn
1742+
(assoc (mk-email {:mid "<pend-patch@test.org>"
1743+
:subject "[PATCH] Fix the parser"
1744+
:from "user@test.org"
1745+
:date #inst "2026-05-01T10:00:00"
1746+
:in-reply-to "<never-ingested@test.org>"
1747+
:body "Here is the fix.\n"})
1748+
:email/attachments [{:attachment/filename "0001-fix.patch"
1749+
:attachment/content-type "text/x-diff"
1750+
:attachment/data format-patch-text}])
1751+
"direct")
1752+
(let [db (d/db conn)
1753+
r (get-report db "<pend-patch@test.org>")]
1754+
(is (true? (pending? db "<pend-patch@test.org>"))
1755+
"Reply should be pending: its parent was never ingested")
1756+
(is (= ["0001-fix.patch"] (mapv :patch/filename (:report/patches r)))
1757+
"Patches should be stored at creation despite the pending flag"))
1758+
1759+
;; TTL-flush retries the pending email; the :store-patches hook
1760+
;; must not add duplicate patch components.
1761+
(digest/flush-stale-pending! conn source-map sources 0)
1762+
(let [db (d/db conn)
1763+
r (get-report db "<pend-patch@test.org>")]
1764+
(is (false? (pending? db "<pend-patch@test.org>"))
1765+
"Flush should clear the pending flag")
1766+
(is (= ["0001-fix.patch"] (mapv :patch/filename (:report/patches r)))
1767+
"Flush retry must not duplicate the stored patches"))
1768+
(finally
1769+
(teardown! ctx))))))

0 commit comments

Comments
 (0)