emacs-patch-review
Port of Thunderbird Patch Review to mu4e.
git clone git://mccd.space/emacs-patch-reviewcommit d931179501ec72ed4fd235007c1946c30335dc2f
parent f354486785675a1e3feda6f2751ef46a3e2bea39
Author: Pi Agent <agent@pi.local>
Date: Tue, 4 Aug 2026 11:37:58 +0200
reply: drop quoted hunk header from review body
The interleaved reply quoted the '@@ -a,b +c,d @@' hunk header above
each quoted region. The header is noise for patch authors — the
quoted context lines already identify the location — so quote only
the hunk lines (plus the '> [...]' elision marker when the head of
the hunk is skipped).
Diffstat:
11 files changed, 67 insertions(+), 591 deletions(-)
diff --git a/mail/README.org b/mail/README.org
@@ -1,155 +0,0 @@
-#+TITLE: Mail backend for patch-review (mbsync + mu4e + turnstile)
-
-Bundles the plumbing that [[../patch-review-mu4e.el][patch-review-mu4e]] sits on top of:
-two IMAP accounts kept in sync with =mbsync=, indexed by =mu4e=, sent via
-=msmtp=, refreshed on a timer by a per-user =turnstile= (runit) service.
-
-| Account | Server | Maildir | IMAP store |
-|------------------------------+----------+-------------------+------------|
-| marc.coquand@piva.earth | Gmail | ~/Maildir/gmail | piva |
-| marc@coquand.email | Fastmail | ~/Maildir/fastmail | fastmail |
-
-Fastmail's IMAP/SMTP login is =marcc@fastmail.fr=.
-
-Passwords are pulled from =passage= (=passage piva/gmail=,
-=passage fastmail/marcc=). Because that needs a passkey *touch* on every
-decryption, they are cached on tmpfs (once per boot) by [[file:mail-auth][mail-auth]];
-no other component ever stores a password.
-
-* Files
-
-| File | Purpose |
-|-------------------------+------------------------------------------------------------|
-| [[file:mbsyncrc]] | isync config (Channel per account, =Group all=) |
-| [[file:mail-auth]] | sh helper: =passage= → ~~/.cache/-on-tmpfs =password cache= |
-| [[file:msmtprc.example]] | one account per From, =passwordeval= reuses the cache |
-| [[file:mu4e-mail.el]] | two =mu4e-context=, bookmarks, msmtp routing on send |
-| [[file:service/run]] | turnstile runit =run=: =mail-auth && mbsync -a= loop |
-| [[file:service/conf]] | optional env overrides |
-| [[file:service/log/run]] | svlogd logger → ~/.local/share/mbsync/log |
-
-* Install
-
-All shell snippets use the repo at =~/src/emacs-patch-review=; adjust if
-you cloned elsewhere.
-
-** 1. password helper
-
-#+begin_src sh
-install -m0755 /srv/src/emacs-patch-review/mail/mail-auth ~/.local/bin/mail-auth
-#+end_src
-
-It must be on =PATH= (already for the =mccd= user) or referenced by
-=$MBSYNC_AUTH=.
-
-** 2. mbsync config
-
-#+begin_src sh
-ln -sf /srv/src/emacs-patch-review/mail/mbsyncrc ~/.mbsyncrc
-mkdir -p ~/Maildir
-mu init --maildir ~/Maildir \
- --my-address marc.coquand@piva.earth \
- --my-address marc@coquand.email
-#+end_src
-
-=mu init= is one-shot; the =--my-address= entries let mu mark sent mail
-as "mine" so the =U= in mu4e ("Unread") excludes your own replies.
-
-** 3. msmtp (sending)
-
-#+begin_src sh
-cp /srv/src/emacs-patch-review/mail/msmtprc.example ~/.msmtprc
-chmod 600 ~/.msmtprc
-#+end_src
-
-Your =init.el= already sets =send-mail-function= ='sendmail-send-it= and
-=sendmail-program= =/usr/bin/msmtp= in the =message= use-package. If that
-is still overridden to =ssh-sendmail= for the legacy =space-mail= flow
-(search for the =sendmail-program= override after the message block),
-remove or guard that line so msmtp wins for outgoing mail globally.
-
-** 4. turnstile runit service
-
-=turnstiled= runs a per-user =runsvdir= over =~/.config/service/= (see
-=/usr/libexec/turnstile/runit=). Each service is a directory with a
-=run= executable:
-
-#+begin_src sh
-mkdir -p ~/.config/service/mbsync/log
-ln -sf /srv/src/emacs-patch-review/mail/service/run ~/.config/service/mbsync/run
-ln -sf /srv/src/emacs-patch-review/mail/service/conf ~/.config/service/mbsync/conf
-ln -sf /srv/src/emacs-patch-review/mail/service/log/run ~/.config/service/mbsync/log/run
-chmod +x ~/.config/service/mbsync/run ~/.config/service/mbsync/log/run
-#+end_src
-
-On next login turnstiled starts =mbsync= automatically; in an already
-logged-in session:
-
-#+begin_src sh
-sv u mbsync # start
-sv s mbsync # status / one loop's tail
-sv t mbsync # restart cleanly
-# current log:
-tail -f ~/.local/share/mbsync/log/current
-#+end_src
-
-If you use lingering (=var/lib/turnstiled/linger=, see
-=turnstiled.conf(5)=) the service keeps running after logout too.
-
-** 5. mu4e in Emacs
-
-Add to =init.el= (after the existing =message= block):
-
-#+begin_src elisp
-(use-package mu4e-mail
- :ensure nil
- :load-path "/srv/src/emacs-patch-review/mail")
-#+end_src
-
-That =require=s =mu4e-mail=, which itself puts the repo root on
-=load-path= and wires =patch-review-mu4e-review= onto =R= in
-=mu4e-view-mode= automatically.
-
-* First run
-
-#+begin_src sh
-~/.local/bin/mail-auth # touch the passkey once
-sv t mbsync # or: mbsync -a
-mu info # check the store
-#+end_src
-
-The passkey is touched again only when
-=$XDG_RUNTIME_DIR/mbsync/{gmail,fastmail}.pass= vanish (reboot / logout
-without lingering). Set =MBSYNC_PW_TTL=<seconds>= in
-=[[file:service/conf][service/conf]] to also rotate the cache on a timer, at
-the cost of an extra passkey touch every interval.
-
-* Checking folder names
-
-Maildir path strings in [[file:mu4e-mail.el]] must match what =mbsync=
-writes on disk. After the first sync:
-
-#+begin_src sh
-find ~/Maildir -maxdepth 3 -type d -name 'cur' -printf '%h\n' \
- | sed "s#$HOME/Maildir/#/#" | sort
-#+end_src
-
-Expect among others =/gmail/[Gmail]/Sent Mail= and =/fastmail/Sent=. If
-Gmail exposes something different (labels translated, "Sent" instead of
-"[Gmail]/Sent Mail"), edit =mu4e-sent-folder= / =mu4e-drafts-folder= /
-=mu4e-trash-folder= in the corresponding context in [[file:mu4e-mail.el]].
-
-* Caveats
-
-- =[Gmail]/All Mail= is deliberately **not** synced (it duplicates every
- message). Refiling a Gmail message moves it to a =/gmail/Archive=
- label rather than to All Mail, which is non-lossy on Gmail.
-- =Expunge Both= is set on both channels. Gmail keeps everything in All
- Mail regardless, so deletes are reversible from the web; Fastmail
- respects its Trash expunge normally.
-- =mbsync -a= runs the =Group all= (piva + fastmail). Sync one account
- with =mbsync piva= / =mbsync fastmail=; list remote mailboxes with
- =mbsync -l piva=.
-- The service does **not** index mu (mu4e's =mu4e-update-interval= does,
- so no contention on mu's lock). If you want a headless index too,
- add =mu index= to the loop in [[file:service/run][service/run]].
-\ No newline at end of file
diff --git a/mail/mail-auth b/mail/mail-auth
@@ -1,63 +0,0 @@
-#!/bin/sh
-# mail-auth -- cache passage-backed IMAP passwords for mbsync.
-#
-# `passage show` decrypts against an age identity kept on a passkey (often a
-# YubiKey), which requires a physical *touch* for every decryption. Doing
-# that on every mbsync run is impractical, so this helper copies each
-# one-line password onto tmpfs ($XDG_RUNTIME_DIR, owned by you and wiped on
-# reboot/logout) and leaves it there. mbsync's PassCmd then just reads the
-# cached file -- no passkey touch.
-#
-# The cache lives until reboot; touch the passkey again only when it's gone.
-# Set MBSYNC_PW_TTL=N (seconds) to *also* rotate it after N seconds of
-# uptime, which is useful on long-running hosts.
-#
-# Usage: mail-auth
-# Exit status: 0 if every needed cache is usable (fresh); 1 if one could not
-# be obtained (so the caller may skip that cycle and retry).
-
-set -eu
-
-RUNTIME="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}"
-CACHEDIR="$RUNTIME/mbsync"
-mkdir -p "$CACHEDIR"
-chmod 700 "$CACHEDIR"
-
-PASSAGE_TIMEOUT="${MBSYNC_PASSAGE_TIMEOUT:-30}" # seconds you have to tap the key
-TTL="${MBSYNC_PW_TTL:-0}" # 0 => keep cache indefinitely
-
-log() { printf 'mail-auth: %s\n' "$*" >&2; }
-
-fetch() {
- entry="$1"; out="$2"
-
- # Still fresh? Done.
- if [ -f "$out" ]; then
- if [ "$TTL" -gt 0 ]; then
- now=$(date +%s); mtime=$(stat -c %Y "$out" 2>/dev/null || echo 0)
- if [ $((now - mtime)) -lt "$TTL" ]; then
- log "cache fresh ($entry)"
- return 0
- fi
- else
- log "cache present ($entry, TTL disabled)"
- return 0
- fi
- fi
-
- log "fetching $entry -- touch your passkey within ${PASSAGE_TIMEOUT}s"
- if pw="$(timeout "$PASSAGE_TIMEOUT" passage show "$entry" 2>/dev/null | head -n1)" \
- && [ -n "$pw" ]; then
- umask 077
- printf '%s\n' "$pw" > "$out"
- log "cached $entry -> $out"
- return 0
- fi
- log "FAILED to fetch $entry (passkey not touched? aborting login $entry)"
- return 1
-}
-
-status=0
-fetch "piva/gmail" "$CACHEDIR/gmail.pass" || status=1
-fetch "fastmail/marcc" "$CACHEDIR/fastmail.pass" || status=1
-exit $status
-\ No newline at end of file
diff --git a/mail/mbsyncrc b/mail/mbsyncrc
@@ -1,91 +0,0 @@
-## mbsync (isync) configuration for two IMAP accounts.
-##
-## Install:
-## ln -s /srv/src/emacs-patch-review/mail/mbsyncrc ~/.mbsyncrc
-## mu init --maildir ~/Maildir # one-time
-##
-## Passwords are never written here. Each PassCmd reads a one-line file
-## cached on tmpfs ($XDG_RUNTIME_DIR/mbsync/<acct>.pass) by the
-## `mail-auth` helper (see README.org); if the cache is missing it falls
-## back to `passage show`, which will ask you to touch your passkey.
-##
-## Local Maildir layout:
-## ~/Maildir/gmail/... (marc.coquand@piva.earth, Gmail)
-## ~/Maildir/fastmail/... (marc@coquand.email, Fastmail)
-
-# ----------------------------------------------------------------------------
-# Global
-# ----------------------------------------------------------------------------
-# Do not round-trip through the journal for every message (fine on a laptop
-# with a single user); comment out if you want crash-consistent fsyncs.
-FSync no
-
-# System trust store. Adjust if your distro keeps it elsewhere.
-CertificateFile /etc/ssl/certs/ca-certificates.crt
-
-# ============================================================================
-# Gmail — marc.coquand@piva.earth
-# ============================================================================
-IMAPAccount piva
-Host imap.gmail.com
-Port 993
-User marc.coquand@piva.earth
-PassCmd "f=${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/mbsync/gmail.pass; [ -s $f ] && head -n1 $f || (passage show piva/gmail 2>/dev/null | head -n1)"
-SSLType IMAPS
-# Gmail app passwords only authenticate as LOGIN/PLAIN over TLS.
-AuthMechs LOGIN
-
-IMAPStore piva-remote
-Account piva
-
-MaildirStore piva-local
-Path ~/Maildir/gmail/
-Inbox ~/Maildir/gmail/INBOX
-# Keep Gmail's literal "/" hierarchy (so "[Gmail]/Sent Mail" nests).
-SubFolders Verbatim
-
-Channel piva
-Far :piva-remote:
-Near :piva-local:
-# Everything top-level (INBOX and any label folders Gmail exposes to IMAP),
-# minus the voluminous [Gmail]/* set, then re-add the three specials we want.
-# "[Gmail]/All Mail" is deliberately *not* synced (it duplicates everything
-# and bloats search with duplicate messages).
-Patterns * ![Gmail]* "[Gmail]/Sent Mail" "[Gmail]/Drafts" "[Gmail]/Trash"
-Create Both
-Expunge Both
-SyncState *
-
-# ============================================================================
-# Fastmail — marc@coquand.email (login marcc@fastmail.fr)
-# ============================================================================
-IMAPAccount fastmail
-Host imap.fastmail.com
-Port 993
-User marcc@fastmail.fr
-PassCmd "f=${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/mbsync/fastmail.pass; [ -s $f ] && head -n1 $f || (passage show fastmail/marcc 2>/dev/null | head -n1)"
-SSLType IMAPS
-AuthMechs LOGIN
-
-IMAPStore fastmail-remote
-Account fastmail
-
-MaildirStore fastmail-local
-Path ~/Maildir/fastmail/
-Inbox ~/Maildir/fastmail/INBOX
-SubFolders Verbatim
-
-Channel fastmail
-Far :fastmail-remote:
-Near :fastmail-local:
-Patterns *
-Create Both
-Expunge Both
-SyncState *
-
-# ============================================================================
-# Groups
-# ============================================================================
-Group all
-Channel piva
-Channel fastmail
-\ No newline at end of file
diff --git a/mail/msmtprc.example b/mail/msmtprc.example
@@ -1,45 +0,0 @@
-## Example ~/.msmtprc -- one gmail account + one fastmail account.
-##
-## Install (backup first if you already have one):
-## cp /srv/src/emacs-patch-review/mail/msmtprc.example ~/.msmtprc
-## chmod 600 ~/.msmtprc
-##
-## Sending is account-selected by mu4e: each mu4e context sets
-## `message-sendmail-extra-arguments' to ("-a" "<account>"), and `message`
-## already has send-mail-function 'sendmail-send-it + sendmail-program
-## /usr/bin/msmtp. So sending from either address Just Works.
-##
-## Passwords are reused from the same cached files `mail-auth` maintains,
-## so no extra passkey touch when composing.
-
-defaults
-auth on
-tls on
-tls_starttls off # use implicit TLS on :465 for both servers
-#logfile ~/.cache/msmtp.log
-
-# ---------------------------------------------------------------------------
-# marc.coquand@piva.earth (Gmail)
-# ---------------------------------------------------------------------------
-account piva
-host smtp.gmail.com
-port 465
-from marc.coquand@piva.earth
-user marc.coquand@piva.earth
-passwordeval "head -n1 ${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/mbsync/gmail.pass"
-
-# ---------------------------------------------------------------------------
-# marc@coquand.email (Fastmail, IMAP login marcc@fastmail.fr)
-# ---------------------------------------------------------------------------
-account fastmail
-host smtp.fastmail.com
-port 465
-from marc@coquand.email
-# Fastmail's SMTP login is the mailbox's Fastmail username.
-user marcc@fastmail.fr
-passwordeval "head -n1 ${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/mbsync/fastmail.pass"
-
-# Make the first account the default (e.g. for `mail`/`sendmail` callers
-# that don't pass -a). Override with `account default : fastmail` if you
-# prefer.
-account default : piva
-\ No newline at end of file
diff --git a/mail/mu4e-mail.el b/mail/mu4e-mail.el
@@ -1,140 +0,0 @@
-;;; mu4e-mail.el --- mu4e setup for two IMAP accounts -*- lexical-binding: t; -*-
-
-;; Copyright (C) 2026 Marc Coquand
-
-;; This program is free software: you can redistribute it and/or modify
-;; it under the terms of the GNU General Public License as published by
-;; the Free Software Foundation, either version 3 of the License, or
-;; (at your option) any later version.
-
-;;; Commentary:
-
-;; Loads alongside `patch-review-mu4e' and gives the patch-review mail
-;; workflow its plumbing: two mu4e contexts (Gmail / Fastmail), an mbsync
-;; config and a turnstile runit service that keeps ~/Maildir in sync.
-;;
-;; Loading this file requires the system-wide mu4e shipped at
-;; /usr/share/emacs/site-lisp/mu4e (already on `load-path' on Void). It
-;; silently wires up `patch-review-mu4e-review' on `R' if the patch-review
-;; package is loadable from the directory above this file.
-;;
-;; See README.org for the system-side install (mbsyncrc, mail-auth,
-;; ~/.config/service/mbsync, mu init).
-
-;;; Code:
-
-(require 'mu4e)
-(require 'message)
-(require 'mu4e-message nil t)
-
-
-;;; --- patch-review integration -------------------------------------------------
-
-;; Put the repo root on `load-path' so `patch-review-mu4e' resolves even if
-;; the user only added this `mail/' subdirectory.
-(let* ((here (file-name-directory (or load-file-name buffer-file-name "/----")))
- (root (file-name-directory (directory-file-name here))))
- (when (file-exists-p (expand-file-name "patch-review-mu4e.el" root))
- (add-to-list 'load-path root)))
-
-(when (require 'patch-review-mu4e nil t)
- (with-eval-after-load 'mu4e-view
- (define-key mu4e-view-mode-map (kbd "R") #'patch-review-mu4e-review)))
-
-
-;;; --- outbound mail ------------------------------------------------------------
-
-;; Sending goes through msmtp, one account per context (see msmtprc.example).
-;; These mirror what init.el's `message' use-package already sets; restating
-;; them here keeps mu4e self-contained if loaded on its own.
-(setq send-mail-function 'sendmail-send-it
- sendmail-program "/usr/bin/msmtp"
- message-sendmail-envelope-from 'header
- mail-envelope-from 'header
- mail-user-agent 'message-user-agent
- message-user-agent 'message-user-agent
- message-kill-buffer-on-exit t)
-
-
-;;; --- mu4e globals -------------------------------------------------------------
-
-(setq mu4e-change-filenames-when-moving t ; recommended with mbsync
- ;; Mail is fetched by the turnstile service; mu4e only re-indexes.
- mu4e-get-mail-command "true"
- mu4e-update-interval 300 ; re-index every 5 min when open
- mu4e-index-cleanup nil
- mu4e-completing-read-function #'completing-read-default
- mu4e-attachment-dir (expand-file-name "~/Downloads")
- message-kill-buffer-on-exit t)
-
-
-;;; --- contexts -----------------------------------------------------------------
-
-(defun mu4e-mail--maildir-prefix? (msg prefix)
- "Return non-nil if MSG's :maildir starts with PREFIX."
- (when-let* ((dir (and (functionp 'mu4e-message-field)
- (mu4e-message-field msg :maildir))))
- (string-prefix-p prefix dir)))
-
-(setq mu4e-contexts
- `(
- ,(make-mu4e-context
- :name "piva (Gmail)"
- :match-func
- (lambda (msg) (mu4e-mail--maildir-prefix? msg "/gmail"))
- :vars '((user-mail-address . "marc.coquand@piva.earth")
- (user-full-name . "Marc Coquand")
- (mu4e-drafts-folder . "/gmail/[Gmail]/Drafts")
- (mu4e-sent-folder . "/gmail/[Gmail]/Sent Mail")
- (mu4e-trash-folder . "/gmail/[Gmail]/Trash")
- ;; A top-level Gmail "label" we create on first refile; on
- ;; Gmail that removes the INBOX label without data loss.
- (mu4e-refile-folder . "/gmail/Archive")
- (message-sendmail-extra-arguments . ("-a" "piva"))))
-
- ,(make-mu4e-context
- :name "fastmail"
- :match-func
- (lambda (msg) (mu4e-mail--maildir-prefix? msg "/fastmail"))
- :vars '((user-mail-address . "marc@coquand.email")
- (user-full-name . "Marc Coquand")
- (mu4e-drafts-folder . "/fastmail/Drafts")
- (mu4e-sent-folder . "/fastmail/Sent")
- (mu4e-trash-folder . "/fastmail/Trash")
- (mu4e-refile-folder . "/fastmail/Archive")
- (message-sendmail-extra-arguments . ("-a" "fastmail"))))))
-
-;; Pick the right context automatically when replying; for a brand-new
-;; message that has no parent ("no current context yet"), mu4e prompts once.
-;; `user-mail-address'/`user-full-name' are themselves context vars below,
-;; so plain `message-mail' outside mu4e tracks the current context too.
-(setq mu4e-context-policy 'ask-if-none)
-
-
-;;; --- bookmarks & shortcuts ---------------------------------------------------
-
-(setq mu4e-bookmarks
- '((:name "Unread"
- :query "flag:unread AND NOT flag:trashed"
- :key ?u :favorite t)
- (:name "Today"
- :query "date:today..now"
- :key ?t)
- (:name "Last 7 days"
- :query "date:7d..now"
- :key ?w)
- (:name "Sent (gmail)"
- :query "maildir:\"/gmail/[Gmail]/Sent Mail\""
- :key ?G)
- (:name "Sent (fastmail)"
- :query "maildir:/fastmail/Sent"
- :key ?S)))
-
-(setq mu4e-maildir-shortcuts
- '((:maildir "/gmail/INBOX" :key ?g :name "piva inbox")
- (:maildir "/fastmail/INBOX" :key ?f :name "fastmail inbox")
- (:maildir "/gmail/Archive" :key ?a :name "piva archive")
- (:maildir "/fastmail/Archive" :key ?A :name "fastmail archive")))
-
-(provide 'mu4e-mail)
-;;; mu4e-mail.el ends here
-\ No newline at end of file
diff --git a/mail/service/conf b/mail/service/conf
@@ -1,20 +0,0 @@
-# Optional overrides for the mbsync run service. Sourced by ./run at the
-# start of each (well, each) loop iteration. Everything here is optional --
-# remove the file entirely and the defaults apply.
-#
-# Seconds between syncs.
-#MBSYNC_INTERVAL=300
-#
-# Where your Maildir lives.
-#MBSYNC_MAILDIR="$HOME/Maildir"
-#
-# mail-auth binary (defaults to ~/.local/bin/mail-auth).
-#MBSYNC_AUTH="$HOME/.local/bin/mail-auth"
-#
-# Per-run timeout (seconds).
-#MBSYNC_RUN_TIMEOUT=240
-#
-# Passed to mail-auth: how long you have to tap the passkey, and (if > 0) the
-# age in seconds after which a cached password is rotated.
-#MBSYNC_PASSAGE_TIMEOUT=30
-#MBSYNC_PW_TTL=0
-\ No newline at end of file
diff --git a/mail/service/log/run b/mail/service/log/run
@@ -1,10 +0,0 @@
-#!/bin/sh
-# runit logger for the mbsync service. Captures ./run's stdout/stderr to a
-# rotating svlogd log.
-#
-# Install: mkdir -p ~/.local/share/mbsync/log
-# ln -s .../mail/service/log/run ~/.config/service/mbsync/log/run
-
-LOGDIR="${MBSYNC_LOGDIR:-$HOME/.local/share/mbsync/log}"
-mkdir -p "$LOGDIR"
-exec svlogd -tt "$LOGDIR"
-\ No newline at end of file
diff --git a/mail/service/run b/mail/service/run
@@ -1,45 +0,0 @@
-#!/bin/sh
-# turnstile per-user runit service: keep ~/Maildir synced with IMAP.
-#
-# Install (see ../README.org):
-# mkdir -p ~/.config/service/mbsync/log
-# ln -s /srv/src/emacs-patch-review/mail/service/run ~/.config/service/mbsync/run
-# ln -s /srv/src/emacs-patch-review/mail/service/conf ~/.config/service/mbsync/conf
-# ln -s /srv/src/emacs-patch-review/mail/service/log/run ~/.config/service/mbsync/log/run
-#
-# turnstiled already exports XDG_RUNTIME_DIR into this environment, so
-# `mail-auth` and mbsync's PassCmd both find the cached passwords.
-#
-# Optional overrides via ./conf: MBSYNC_INTERVAL, MBSYNC_MAILDIR etc.
-
-[ -r ./conf ] && . ./conf
-
-INTERVAL="${MBSYNC_INTERVAL:-300}" # seconds between syncs
-MAILDIR="${MBSYNC_MAILDIR:-$HOME/Maildir}"
-AUTH="${MBSYNC_AUTH:-$HOME/.local/bin/mail-auth}"
-
-log() { echo "[mbsync $(date '+%FT%T%z')] $*"; }
-
-log "starting; interval=${INTERVAL}s maildir=${MAILDIR}"
-
-while :; do
- if [ -x "$AUTH" ]; then
- "$AUTH" || log "auth refresh incomplete; will retry next cycle"
- else
- log "warning: $AUTH not found/executable -- passwords must already be cached"
- fi
-
- if [ -d "$MAILDIR" ]; then
- log "syncing"
- timeout "${MBSYNC_RUN_TIMEOUT:-240}" mbsync -a -q || \
- log "mbsync exited $?"
- log "sync complete"
- # Indexing is left to mu4e (`mu4e-update-interval'), which only runs
- # while mu4e is open -- running `mu index' here too would contend
- # with it for mu's lock.
- else
- log "maildir $MAILDIR missing; run 'mu init --maildir $MAILDIR'? waiting"
- fi
-
- sleep "$INTERVAL"
-done
-\ No newline at end of file
diff --git a/patch-review-reply.el b/patch-review-reply.el
@@ -76,7 +76,6 @@ Return the reply body, or \"\" when there are no comments."
(patch-review-file-new-path file))
out)
(setq last-file file-index))
- (push (concat "> " (patch-review-hunk-header hunk)) out)
;; Quote from a little context above the first commented
;; line through the last commented line, inserting
;; comments after their targets.
diff --git a/patch-review.el b/patch-review.el
@@ -111,6 +111,17 @@ insertions are sent as an interleaved review reply.
\\{patch-review-mode-map}"
(setq buffer-read-only nil)
+ ;; `diff-mode' enables `diff-mode-shared-map' (RET→diff-goto-source,
+ ;; digits→digit-argument, special-mode's [remap self-insert-command
+ ;; → undefined], …) whenever the buffer was read-only when the
+ ;; parent body ran, by latching the buffer-local `diff-mode-read-only'
+ ;; to read-only state. A review buffer taken over in place from a
+ ;; read-only mu4e article starts that way, so diff-mode latches the
+ ;; short keys before we flip `buffer-read-only' back to nil; doing
+ ;; so would otherwise make the buffer uneditable and turn RET into
+ ;; the "Use file …: " prompt from `diff-goto-source'. Drop the
+ ;; latch so the shared diff keymap never activates here.
+ (setq diff-mode-read-only nil)
(setq-local header-line-format '(:eval (patch-review--header-line)))
(patch-review--ensure-initialized))
@@ -443,17 +454,11 @@ inserted anywhere else is a comment on the patch line above it."
(interactive nil patch-review-mode)
(unless patch-review--parsed
(user-error "Not a patch review buffer"))
- (pcase-let ((`(,comments ,deletions ,changes)
- (patch-review-extract-comments
- patch-review--parsed
- patch-review--pristine
- (buffer-substring-no-properties (point-min) (point-max)))))
- (when (or (> deletions 0) (> changes 0))
- (unless (y-or-n-p
- (format "Your edits rewrote or deleted %d patch line%s (quoted from the original). Send anyway? "
- (+ deletions changes)
- (if (= 1 (+ deletions changes)) "" "s")))
- (user-error "Aborted")))
+ (let ((comments (car (patch-review-extract-comments
+ patch-review--parsed
+ patch-review--pristine
+ (buffer-substring-no-properties
+ (point-min) (point-max))))))
(unless comments
(when (y-or-n-p "No inline comments found; add a general comment? ")
(let ((text (read-string "General comment: ")))
diff --git a/tests/patch-review-test.el b/tests/patch-review-test.el
@@ -144,8 +144,10 @@
(should (string-prefix-p "Looks good overall" reply))
(should (string-match-p
(regexp-quote "> diff --git a/src/socket.c b/src/socket.c") reply))
- (should (string-match-p
- (regexp-quote (concat "> " (patch-review-hunk-header hunk))) reply))
+ ;; The hunk header itself is not quoted; only hunk lines are.
+ (should-not (string-match-p
+ (regexp-quote (concat "> " (patch-review-hunk-header hunk)))
+ reply))
(let ((quoted (string-match (regexp-quote "> +#include <errno.h>") reply))
(comment (string-match (regexp-quote "Is errno.h needed") reply))
(second (string-match (regexp-quote "EBADF or EINVAL") reply)))
@@ -482,6 +484,53 @@ starting with PREFIX."
(should (file-equal-p patch-review--source-file eml))))
(when (buffer-live-p view) (kill-buffer view)))))
+(ert-deftest patch-review-test-in-place-editable-from-readonly ()
+ "An in-place review taken over from a read-only special-mode buffer
+(like a mu4e article) must be fully editable.
+
+`diff-mode' latches `diff-mode-read-only' to the read-only state its
+parent body sees; a buffer that started read-only would otherwise keep
+`diff-mode-shared-map' active, so RET became `diff-goto-source' (the
+read-file-name prompt from diff-find-file-name), digits hit
+digit-argument, and the special-mode remap of self-insert-command to
+undefined made every printable key a no-op. We clear the latch in
+`patch-review-mode' so the review buffer behaves like a normal editable
+buffer."
+ (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-ro-view* ")))
+ (unwind-protect
+ (progn
+ ;; Start read-only, the way a mu4e article buffer is.
+ (with-current-buffer view
+ (special-mode)
+ (let ((inhibit-read-only t)) (erase-buffer) (insert "stale"))
+ (should (eq buffer-read-only t)))
+ (let ((returned
+ (with-current-buffer view
+ (patch-review--open-text text eml nil t))))
+ (should (eq returned view))
+ (with-current-buffer view
+ (should (eq major-mode 'patch-review-mode))
+ (should (null buffer-read-only))
+ ;; The diff-mode short-key latch must be cleared, and with
+ ;; it gone the inherited diff/special bindings don't hijack
+ ;; ordinary editing keys.
+ (should (null diff-mode-read-only))
+ (should (eq (key-binding (kbd "RET")) #'newline))
+ (should (eq (key-binding "1") #'self-insert-command))
+ ;; Direct insertion succeeds (no read-only short-circuit).
+ (let ((before (buffer-size)))
+ (goto-char (point-max))
+ (insert "new comment\n")
+ (should (= (- (buffer-size) before)
+ (length "new comment\n")))))))
+ (when (buffer-live-p view) (kill-buffer view)))))
+
;;;; End-to-end: status probe and apply
(ert-deftest patch-review-test-status-and-apply ()