emacs-patch-review
Port of Thunderbird Patch Review to mu4e.
git clone git://mccd.space/emacs-patch-review| Log | Files | Refs | README | Mail |
patch-review-reply.el (4806B)
1 ;;; patch-review-reply.el --- Format review replies -*- lexical-binding: t; -*-
2
3 ;; Copyright (C) 2026 Marc Coquand
4
5 ;; This program is free software: you can redistribute it and/or modify
6 ;; it under the terms of the GNU General Public License as published by
7 ;; the Free Software Foundation, either version 3 of the License, or
8 ;; (at your option) any later version.
9
10 ;;; Commentary:
11
12 ;; Turn draft comments into a plain-text, interleaved mailing-list
13 ;; review: the commented regions of the patch are quoted with "> " and
14 ;; each comment sits unquoted below the line it targets — kernel-style.
15 ;;
16 ;; Port of thunderbird-review-ui's modules/reply-format.js and the
17 ;; locator scheme of modules/review-store.js. Pure, no side effects.
18
19 ;;; Code:
20
21 (require 'cl-lib)
22 (require 'patch-review-parse)
23
24 (defconst patch-review-reply-general "general"
25 "Locator key for the general (whole-patch) comment.")
26
27 (defconst patch-review-reply-context-before 3
28 "Quoted lines shown above the first comment in a hunk.")
29
30 (defun patch-review-reply-locator (file-index hunk-index line-index)
31 "Build a locator string for FILE-INDEX, HUNK-INDEX and LINE-INDEX."
32 (format "%d:%d:%d" file-index hunk-index line-index))
33
34 (defun patch-review-reply-parse-locator (locator)
35 "Parse LOCATOR into (FILE-INDEX HUNK-INDEX LINE-INDEX)."
36 (mapcar #'string-to-number (split-string locator ":")))
37
38 (defun patch-review-reply-format (parsed comments)
39 "Format COMMENTS against PARSED (a `patch-review-email' struct).
40 COMMENTS is an alist (LOCATOR . TEXT); LOCATOR is
41 `patch-review-reply-general' or a \"file:hunk:line\" string.
42 Return the reply body, or \"\" when there are no comments."
43 (if (null comments)
44 ""
45 (let ((out nil)
46 (by-hunk (make-hash-table :test #'equal))
47 (general (assoc patch-review-reply-general comments)))
48 (when general
49 (push (cdr general) out)
50 (push "" out))
51 ;; Group line comments by file/hunk.
52 (dolist (entry comments)
53 (unless (equal (car entry) patch-review-reply-general)
54 (pcase-let ((`(,f ,h ,l) (patch-review-reply-parse-locator (car entry))))
55 (push (list f h l (cdr entry))
56 (gethash (format "%d:%d" f h) by-hunk)))))
57 (let ((groups nil))
58 (maphash (lambda (_k v) (push v groups)) by-hunk)
59 (setq groups
60 (sort groups
61 (lambda (a b)
62 (let ((fa (car a)) (fb (car b)))
63 (if (/= fa fb)
64 (< fa fb)
65 (< (cadr a) (cadr b)))))))
66 (let ((last-file -1))
67 (dolist (group groups)
68 (setq group (sort group (lambda (a b) (< (caddr a) (caddr b)))))
69 (pcase-let ((`(,file-index ,hunk-index ,_ ,_) (car group)))
70 (let* ((file (nth file-index (patch-review-email-files parsed)))
71 (hunk (nth hunk-index (patch-review-file-hunks file))))
72 (when hunk
73 (when (/= file-index last-file)
74 (push (format "> diff --git a/%s b/%s"
75 (patch-review-file-old-path file)
76 (patch-review-file-new-path file))
77 out)
78 (setq last-file file-index))
79 ;; Quote from a little context above the first commented
80 ;; line through the last commented line, inserting
81 ;; comments after their targets.
82 (let* ((lines (patch-review-hunk-lines hunk))
83 (first (caddr (car group)))
84 (last (caddr (car (last group))))
85 (start (max 0 (- first patch-review-reply-context-before)))
86 (pending group))
87 (when (> start 0)
88 (push "> [...]" out))
89 (cl-loop for i from start
90 while (and (<= i last) (< i (length lines)))
91 do (push (concat "> " (patch-review-line-raw
92 (nth i lines)))
93 out)
94 (while (and pending (= (caddr (car pending)) i))
95 (push "" out)
96 (push (nth 3 (car pending)) out)
97 (push "" out)
98 (setq pending (cdr pending))))
99 (push "" out))))))))
100 ;; `out' is reversed: insertion-order trailing blanks lead the list.
101 (while (and out (equal (car out) ""))
102 (setq out (cdr out)))
103 (concat (mapconcat #'identity (nreverse out) "\n") "\n"))))
104
105 (provide 'patch-review-reply)
106 ;;; patch-review-reply.el ends here