Skip to content

Commit b03c673

Browse files
committed
feat: Accept multiple message-id forms in Superseded-by
"Superseded-by:" now accepts <msg-id>, a bare msg-id and an archive URL where the message-id is the last path segment, a la public-inbox. Also align email-address parsing across -by directives and role controls.
1 parent 16cc60b commit b03c673

4 files changed

Lines changed: 58 additions & 27 deletions

File tree

docs/bark-manual.org

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -175,9 +175,15 @@ the old report as superseded by replying to its thread with:
175175

176176
: Superseded-by: <target-message-id@example.com>
177177

178-
The angle brackets around the message-id are optional. See the
179-
[[#superseding-a-report-1][maintainer section]] for the full details on what this does and how to
180-
undo it.
178+
Three forms are accepted:
179+
180+
- =<msg-id@host>= --- the canonical RFC form.
181+
- =msg-id@host= --- a bare message-id.
182+
- An archive URL where the message-id is the last path segment, e.g.
183+
=https://list.orgmode.org/orgmode/msg-id@host/=.
184+
185+
See the [[#superseding-a-report-1][maintainer section]] for the full details on what this does
186+
and how to undo it.
181187

182188
** Voting on requests
183189

@@ -277,7 +283,9 @@ thread with:
277283

278284
: Superseded-by: <target-message-id@example.com>
279285

280-
The angle brackets around the message-id are optional. This does three
286+
The message-id can be given as =<msg-id@host>=, bare =msg-id@host=, or
287+
extracted from an archive URL where it is the last path segment (e.g.
288+
=https://list.orgmode.org/orgmode/msg-id@host/=). This does three
281289
things automatically:
282290

283291
1. Sets =:report/superseded-by-target= on the old report (a reference
@@ -546,16 +554,16 @@ prefixed with =!=. Accepts =:loose= (default) or =:strict=.
546554

547555
The rule applies uniformly to all Bark instructions:
548556

549-
| Form | Loose | Strict |
550-
|------------------+------------------------+-----------------------|
551-
| Trigger | =Closed.= / =!Closed.= | =!Closed.= |
552-
| Negative trigger | =Not acked= / =!Not acked= | =!Not acked= |
553-
| =-by= directive | =Acked-by: a@b= | =!Acked-by: a@b= |
554-
| Date directive | =Deadline: 2026-06-01= | =!Deadline: 2026-06-01= |
555-
| Topic | =Topic: event= | =!Topic: event= |
556-
| Supersede | =Superseded-by: <mid>= | =!Superseded-by: <mid>= |
557-
| Role control | =Add maintainer: a@b= | =!Add maintainer: a@b= |
558-
| Notify | =Notify: on= | =!Notify: on= |
557+
| Form | Loose | Strict |
558+
|------------------+------------------------+------------------------|
559+
| Trigger | =Closed.= / =!Closed.= | =!Closed.= |
560+
| Negative trigger | =Not acked= / =!Not acked= | =!Not acked= |
561+
| =-by= directive | =Acked-by: a@b.c= | =!Acked-by: a@b.c= |
562+
| Date directive | =Deadline: 2026-06-01= | =!Deadline: 2026-06-01= |
563+
| Topic | =Topic: event= | =!Topic: event= |
564+
| Supersede | =Superseded-by: <mid>= | =!Superseded-by: <mid>= |
565+
| Role control | =Add maintainer: a@b.c= | =!Add maintainer: a@b.c= |
566+
| Notify | =Notify: on= | =!Notify: on= |
559567

560568
Set globally or per source (per-source wins):
561569

@@ -884,9 +892,6 @@ Emails that don't match any source are not stored.
884892
| =:periods= | Time-windowed overrides for maintainers/commands (see [[#source-periods][Source periods]]) |
885893

886894
** Source periods
887-
:PROPERTIES:
888-
:CUSTOM_ID: source-periods
889-
:END:
890895

891896
When a source's vocabulary or maintainer list has evolved over time,
892897
declare the history inline with =:periods= --- a vector of time-windowed

src/bark/commands.clj

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -39,15 +39,18 @@
3939
")(?:" trailing-punct (when-not strict-punct? "|\\s") "|$)")))
4040

4141
(defn- directive-pattern [strict-syntax? {:keys [syntax param]}]
42-
(let [qs (java.util.regex.Pattern/quote syntax)
43-
prefix (common/bang-prefix strict-syntax?)]
42+
(let [qs (java.util.regex.Pattern/quote syntax)
43+
prefix (common/bang-prefix strict-syntax?)
44+
addr "[^@<>\\s]+@[^@<>\\s]+\\.[^@<>\\s]+" ; email address (dot required)
45+
mid "[^<>\\s]+@[^<>\\s]+" ; bracketed message-id
46+
mid-path "[^/<>\\s]+@[^/<>\\s]+"] ; mid in path or bare
4447
(re-pattern
4548
(case param
46-
:email-address (str "^" prefix qs ":\\s+(?:.+<(\\S+@\\S+)>|(\\S+@\\S+))" trailing-punct "?\\s*$")
47-
:date (str "^" prefix qs ":\\s+(\\d{4}-\\d{2}-\\d{2})" trailing-punct "?\\s*$")
49+
:email-address (str "^" prefix qs ":\\s+(?:.*<(" addr ")>|(" addr "))" trailing-punct "?\\s*$")
50+
:date (str "^" prefix qs ":\\s+(\\d{4}-\\d{2}-\\d{2})" trailing-punct "?\\s*$")
4851
:date-or-duration (str "^" prefix qs ":\\s+(\\d{4}-\\d{2}-\\d{2}|\\d+[dwmy](?:\\s+\\d+[dwmy])*)" trailing-punct "?\\s*$")
49-
:word (str "^" prefix qs ":\\s+([a-zA-Z0-9_-]+)" trailing-punct "?\\s*$")
50-
:message-id (str "^" prefix qs ":\\s+<?([^<>\\s]+@[^<>\\s]+)>?" trailing-punct "?\\s*$")
52+
:word (str "^" prefix qs ":\\s+([a-zA-Z0-9_-]+)" trailing-punct "?\\s*$")
53+
:message-id (str "^" prefix qs ":\\s+(?:.*<(" mid ")>|.*/(" mid-path ")/?|(" mid-path "))" trailing-punct "?\\s*$")
5154
(str "^" prefix qs trailing-punct "?\\s*$")))))
5255

5356
(defn- compile-trigger-words [strict-syntax? action-map]
@@ -184,7 +187,7 @@
184187
:unset-topic {:action :unset-topic}
185188
:set-topic (when-let [t (nth m 1 nil)]
186189
{:action :set-topic :topic t})
187-
:set-superseded (when-let [mid (nth m 1 nil)]
190+
:set-superseded (when-let [mid (or (nth m 1 nil) (nth m 2 nil) (nth m 3 nil))]
188191
{:action :set-superseded
189192
:target-message-id (str "<" mid ">")})
190193
:unset-superseded {:action :unset-superseded})]

src/bark/roles.clj

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,10 @@
134134
(re-pattern (str "(?m)^" (common/bang-prefix strict-syntax?)
135135
"(Add maintainer|Remove maintainer):\\s+(.+)$")))
136136

137+
(def ^:private address-pattern
138+
(let [addr "[^@\\s<>]+@[^@\\s<>]+\\.[^@\\s<>]+"]
139+
(re-pattern (str "<(" addr ")>|(" addr ")"))))
140+
137141
(defn- parse-addresses
138142
"Extract email addresses from the argument to `Add maintainer:` or
139143
`Remove maintainer:`. Accepts bare addresses (`alice@example.com`)
@@ -143,8 +147,7 @@
143147
appear."
144148
[s]
145149
(when s
146-
(->> (re-seq #"<([^@\s<>]+@[^@\s<>]+\.[^@\s<>]+)>|([^@\s<>]+@[^@\s<>]+\.[^@\s<>]+)"
147-
s)
150+
(->> (re-seq address-pattern s)
148151
(keep (fn [[_ bracketed bare]] (or bracketed bare)))
149152
vec)))
150153

test/bark/digest_test.clj

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -584,6 +584,13 @@
584584
;; RFC 5322 "Display Name <addr>" format
585585
(is (= [{:action :set :attr :report/owned :email-address "x@y.com" :scope :maintainer :id :owned-by}]
586586
(commands/detect-directives :bug "Owned-by: Some User <x@y.com>\n")))
587+
;; Bracketed address without display name: angle brackets must be stripped
588+
(is (= [{:action :set :attr :report/owned :email-address "x@y.com" :scope :maintainer :id :owned-by}]
589+
(commands/detect-directives :bug "Owned-by: <x@y.com>\n")))
590+
;; Address must contain a dot in the domain part
591+
(is (= [] (commands/detect-directives :bug "Owned-by: alice@localhost\n")))
592+
;; Address must not contain stray @ characters
593+
(is (= [] (commands/detect-directives :bug "Owned-by: alice@@host.com\n")))
587594
(is (= [] (commands/detect-directives :bug "Just a normal reply.\n")))
588595
(is (nil? (commands/detect-directives :bug nil)))
589596
;; Expiry directive
@@ -785,10 +792,23 @@
785792
(is (= [{:action :set-superseded :target-message-id "<msg@example.com>" :scope :user :id :superseded-by}]
786793
(commands/detect-directives :bug "Superseded-by: <msg@example.com>\n"))))
787794

788-
(testing "detect-directives: Superseded-by without angle brackets"
795+
(testing "detect-directives: Superseded-by tolerates an URL prefix"
796+
(is (= [{:action :set-superseded :target-message-id "<msg@example.com>" :scope :user :id :superseded-by}]
797+
(commands/detect-directives :bug "Superseded-by: https://orgmode.org/list/<msg@example.com>\n"))))
798+
799+
(testing "detect-directives: Superseded-by accepts a public-inbox URL"
800+
(is (= [{:action :set-superseded :target-message-id "<msg@example.com>" :scope :user :id :superseded-by}]
801+
(commands/detect-directives :bug "Superseded-by: https://list.orgmode.org/orgmode/msg@example.com/\n")))
802+
(is (= [{:action :set-superseded :target-message-id "<msg@example.com>" :scope :user :id :superseded-by}]
803+
(commands/detect-directives :bug "Superseded-by: https://list.orgmode.org/orgmode/msg@example.com\n"))))
804+
805+
(testing "detect-directives: Superseded-by accepts a bare message-id"
789806
(is (= [{:action :set-superseded :target-message-id "<msg@example.com>" :scope :user :id :superseded-by}]
790807
(commands/detect-directives :bug "Superseded-by: msg@example.com\n"))))
791808

809+
(testing "detect-directives: Superseded-by rejects URLs where the @ segment is non-terminal"
810+
(is (= [] (commands/detect-directives :bug "Superseded-by: https://example.com/foo@bar/baz.html\n"))))
811+
792812
(testing "detect-directives: Not superseded"
793813
(is (= [{:action :unset-superseded :scope :setter-or-maintainer :id :unsuperseded}]
794814
(commands/detect-directives :bug "Not superseded\n"))))

0 commit comments

Comments
 (0)