emacs-patch-review
Port of Thunderbird Patch Review to mu4e.
git clone git://mccd.space/emacs-patch-reviewcommit f354486785675a1e3feda6f2751ef46a3e2bea39
parent 82473af65d88e39804504b03dbf5840b8365d002
Author: Pi Agent <agent@pi.local>
Date: Mon, 3 Aug 2026 12:20:58 +0200
Take over the mu4e article buffer in place when reviewing
patch-review-mu4e-review, when invoked from a mu4e-view-mode (article)
buffer, now reuses that buffer instead of popping up a new one: the
buffer is renamed, its contents replaced with the patch body, and it
is switched to patch-review-mode. The headers-buffer path keeps the
old pop-new-buffer behaviour.
This adds an &optional IN-PLACE flag threaded through
patch-review-open-message-file and patch-review--open-text, and a test
that the same buffer object is reused and switched to review mode.
Diffstat:
3 files changed, 73 insertions(+), 22 deletions(-)
diff --git a/patch-review-mu4e.el b/patch-review-mu4e.el
@@ -26,14 +26,18 @@
;;;###autoload
(defun patch-review-mu4e-review ()
- "Review the patch message at point (mu4e view or headers buffer)."
+ "Review the patch message at point (mu4e view or headers buffer).
+From a mu4e article buffer (`mu4e-view-mode') the review takes over
+that buffer in place instead of opening a new window; from a
+headers buffer it opens a new review buffer."
(interactive nil mu4e-view-mode mu4e-headers-mode)
(unless (require 'mu4e-message nil t)
(user-error "mu4e is not available"))
- (let ((path (mu4e-message-field (mu4e-message-at-point) :path)))
+ (let* ((in-place (derived-mode-p 'mu4e-view-mode))
+ (path (mu4e-message-field (mu4e-message-at-point) :path)))
(unless (and path (file-readable-p path))
(user-error "No message file at point"))
- (patch-review-open-message-file path)))
+ (patch-review-open-message-file path nil in-place)))
(provide 'patch-review-mu4e)
;;; patch-review-mu4e.el ends here
diff --git a/patch-review.el b/patch-review.el
@@ -146,38 +146,59 @@ insertions are sent as an interleaved review reply.
;;;; Opening
;;;###autoload
-(defun patch-review-open-message-file (file &optional project)
- "Open raw patch email FILE (an .eml or format-patch file) for review."
+(defun patch-review-open-message-file (file &optional project in-place)
+ "Open raw patch email FILE (an .eml or format-patch file) for review.
+PROJECT overrides the target repository. When IN-PLACE is non-nil,
+reuse the current buffer instead of popping up a new one (see
+`patch-review--open-text')."
(interactive "fPatch message file: ")
(patch-review--open-text
(with-temp-buffer
(insert-file-contents file)
(buffer-string))
(expand-file-name file)
- project))
-
-(defun patch-review--open-text (text source-file project)
- "Open TEXT (a raw message) in a new review buffer.
-SOURCE-FILE is what `git am' runs on; PROJECT overrides the
-target repository. Return the review buffer."
+ project
+ in-place))
+
+(defun patch-review--open-text (text source-file project &optional in-place)
+ "Open TEXT (a raw message) in a review buffer.
+SOURCE-FILE is what `git am' runs on; PROJECT overrides the target
+repository. Return the review buffer.
+
+When IN-PLACE is non-nil, reuse the current buffer instead of
+popping up a new one: the buffer is renamed to `*Review: SUBJECT*',
+its contents are replaced with the message body, and it is switched
+to `patch-review-mode'. `patch-review-mu4e-review' uses this to
+take over a mu4e article buffer rather than opening a window."
(pcase-let ((`(,headers . ,body) (patch-review--split-message text)))
(unless (patch-review-patch-message-p (cdr (assq 'subject headers)) body)
(unless (y-or-n-p "This message does not look like a patch; open anyway? ")
(user-error "Aborted")))
- (let ((buf (generate-new-buffer
- (format "*Review: %s*"
+ (let ((name (format "*Review: %s*"
(or (patch-review--decode (cdr (assq 'subject headers)))
(and source-file
(file-name-nondirectory source-file))
- "patch")))))
- (with-current-buffer buf
- (insert body)
- (goto-char (point-min))
- (let ((patch-review--pending-init
- (list :headers headers :source source-file :project project)))
- (patch-review-mode)))
- (pop-to-buffer buf)
- buf)))
+ "patch")))
+ (init (list :headers headers :source source-file :project project)))
+ (if in-place
+ (let ((buf (current-buffer)))
+ (with-current-buffer buf
+ (rename-buffer name t)
+ (let ((inhibit-read-only t))
+ (erase-buffer)
+ (insert body)
+ (goto-char (point-min)))
+ (let ((patch-review--pending-init init))
+ (patch-review-mode)))
+ buf)
+ (let ((buf (generate-new-buffer name)))
+ (with-current-buffer buf
+ (insert body)
+ (goto-char (point-min))
+ (let ((patch-review--pending-init init))
+ (patch-review-mode)))
+ (pop-to-buffer buf)
+ buf)))))
(defun patch-review--split-message (text)
"Split raw message TEXT into (HEADERS . BODY).
diff --git a/tests/patch-review-test.el b/tests/patch-review-test.el
@@ -456,6 +456,32 @@ starting with PREFIX."
(should-error (patch-review-send-review) :type 'user-error)))
(when (buffer-live-p buf) (kill-buffer buf)))))
+(ert-deftest patch-review-test-open-in-place ()
+ "Opening with IN-PLACE reuses the current buffer rather than making one."
+ (let* ((eml (expand-file-name
+ "fixtures/patch1.eml"
+ (file-name-directory (or load-file-name buffer-file-name))))
+ (text (with-temp-buffer
+ (insert-file-contents eml)
+ (buffer-string)))
+ (view (get-buffer-create " *patch-review-fake-view* ")))
+ (unwind-protect
+ (let ((returned
+ (with-current-buffer view
+ (insert "stale mu4e contents")
+ (patch-review--open-text text eml nil t))))
+ (should (eq returned view))
+ (with-current-buffer view
+ (should (eq major-mode 'patch-review-mode))
+ (should (string-prefix-p "*Review:" (buffer-name)))
+ ;; The review body, not the discarded text, is now the buffer.
+ (should (string-match-p "diff --git" patch-review--pristine))
+ (should-not (string-match-p "stale mu4e contents"
+ patch-review--pristine))
+ ;; The on-disk message file is the git-am source.
+ (should (file-equal-p patch-review--source-file eml))))
+ (when (buffer-live-p view) (kill-buffer view)))))
+
;;;; End-to-end: status probe and apply
(ert-deftest patch-review-test-status-and-apply ()