emacs-patch-review

Port of Thunderbird Patch Review to mu4e.

git clone git://mccd.space/emacs-patch-review

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