patchwel is an Emacs package for working with Patchwork, the patch tracking and review tool used by many mailing-list-based projects (the Linux kernel, QEMU, Open vSwitch, DPDK, and others). Its goal is to bring patch review and mailing-list patch management into Emacs directly: browsing series and their review status, delegating and tracking state, and applying patches to a local git tree, without leaving the editor for a browser tab.
patchwel talks to one or more Patchwork servers’ REST APIs, caches
what it fetches in a local SQLite database, and presents it as a
groupable, filterable listing buffer. Beyond browsing, it covers the
day-to-day review loop end to end: replying to comments and patches as
mail, applying a series as real git commits on its own branch and
opening a browsable log of them (via magit or vc), and tracking a
patch’s CI checks and state through to a final decision.
patchwel is free software, licensed under the GNU General Public License, version 3 (GPLv3) or (at your option) any later version. See https://www.gnu.org/licenses/gpl-3.0.en.html for the full license text.
- Emacs 29.1 or later (patchwel uses the built-in
sqlite.el, which requires Emacs to have been built with SQLite support). - Network access to whichever Patchwork server(s) you configure.
Put the patchwel-*.el files somewhere on your load-path and
require the package:
(add-to-list 'load-path "/path/to/patchwel")
(require 'patchwel)All configuration lives under the patchwork customize group
(M-x customize-group RET patchwork RET), or can be set directly with
setq in your init file.
The central setting: a list of Patchwork servers to sync and browse.
Each entry is a plist with :url, :token, and :projects:
(setq patchwork-servers
'((:url "https://patchwork.ozlabs.org/api" :token nil
:projects ("openvswitch"))
(:url "https://patchwork.kernel.org/api" :token nil
:projects ("netdevbpf"))
(:url "https://patches.dpdk.org/api" :token "your-api-token"
:projects nil))):url— base API URL for the server, no trailing slash.:token— API token for this server, ornilfor anonymous, read-only access. Required if you want to set delegates/state.:projects— list of project slugs (or ids) to sync on this server, ornilto sync every project visible to that server/token.:since-format— optional; forces whichsince=timestamp format is sent to this server when doing an incremental sync (a key intopatchwork-since-format-strings:z,naive, ordate). See below; leavenilto auto-detect.:user-agent— optional User-Agent string to send to this server instead ofpatchwork-user-agent, e.g. if a server admin has exempted a specific string for you. See below; leavenilfor the default.:sync-timeout— optional; seconds to wait for a single list/summary request (series/patches/comments/checks/events) to this server instead ofpatchwork-sync-timeout. See below; leavenilfor the default.:events-omit-project— optional boolean; never sendproject=on/events/requests to this server at all. See below; leavenilunless you’ve actually hit the specific bug it works around.
For any server that hosts many unrelated projects (patchwork.ozlabs.org
and patchwork.kernel.org both host hundreds), set :projects
explicitly rather than leaving it nil — otherwise every sync walks
the server’s entire, mostly-irrelevant event/series history instead of
just the projects you care about.
Different real-world Patchwork instances don’t all behave alike, so a couple of things are auto-detected but can be pinned down explicitly per server if you already know what a given deployment needs:
- ~since=~ format: incremental syncs ask for events/series changed
since=a timestamp, but deployments disagree on the format —patchwork.ozlabs.organdpatches.dpdk.orgboth want a Z-suffixed UTC datetime, whilepatchwork.kernel.org500s on any timezone marker at all and wants a naive one. patchwel tries each format inpatchwork-since-format-strings(z, thennaive, then a baredate) until one works, caching the winner; set a server’s:since-formatto skip straight to the one you already know works, avoiding paying for a failed guess on every forced/first sync. - User-Agent: Emacs’s own default User-Agent reads as automated
traffic, and at least one Patchwork deployment fronted by an
anti-bot system (e.g. Anubis) has been observed serving a
JavaScript challenge page instead of a real API response for it.
patchwel identifies as
patchwork-user-agent(a plain curl-style string,"curl/8.7.1"by default — deliberately never a real browser’s User-Agent) instead; override per server with:user-agentif a server admin has exempted a specific string for you. If a response still looks like a non-JSON challenge page,patchwork-api-challenge-hookis given a chance to supply a substitute response before giving up (see Custom hooks below) — Emacs cannot solve such a challenge itself, but a separate module could plug in here. - Slow endpoints on a busy project: an incremental sync’s
/events/query can be genuinely slower thanpatchwork-sync-timeout’s default (5seconds) on a high-traffic project, rather than just occasionally slow — and a timeout there is not treated as “this server has no events API” (only a real 404 is), so it just fails that sync cycle outright and retries the exact same cutoff next time, indefinitely, until it stops timing out. If*Messages*keeps showing “Patchwork: failed to sync …: Patchwork API request timed out” for one server/project and a forced resync (C-u g) doesn’t help either, raise that server’s:sync-timeoutrather than the global default. - ~project=~ breaking ~/events/~ entirely: on
patchwork.kernel.org(observed on thenetdevbpfproject), includingproject=on an/events/request 502s outright, regardless ofsince=format or timeout — dropping the parameter fixes it immediately. Real Patchwork events already carry their own:projectfield independently of the query filter, so setting a server’s:events-omit-projectmakes incremental syncs fetch every project’s events together and dispatch them to the right one locally (using that field), no extra requests needed. Leave it unset otherwise — on a server hosting many unrelated projects, enabling it means every incremental sync fetches the entire server’s event history instead of just the project(s) you configured, exactly what:projectsis normally meant to avoid paying for.
| Variable | Default | Purpose |
|---|---|---|
patchwork-local-db-file | ~~/.cache/patchwel/patchwork.db~ | Path to the local SQLite cache. |
patchwork-cache-ttl | 300 (seconds) | How fresh the cache must be before a sync is skipped. |
patchwork-sync-lookback-days | 30 | History pulled on a server/project’s first sync (and on forced/fallback full resyncs). |
patchwork-sync-timeout | 5 (seconds) | Per-HTTP-request timeout for list/summary requests (series/patches/comments/checks/events); a slow/unreachable server is skipped rather than blocking the whole sync. |
patchwork-detail-fetch-timeout | 15 (seconds) | Per-request timeout for a single patch/series/cover’s full detail or mbox — slower/larger than the list requests above, so it gets a longer timeout of its own. |
patchwork-page-size | 100 | Results requested per page from the API. |
patchwork-series-buffer-name | *patchwork-series* | Name of the main listing buffer. |
patchwork-tag-names | ("ack" "review" "test" "fixes") | Trailer tags counted from comment text (Acked-by/Reviewed-by/Tested-by/Fixes). |
patchwork-default-state-filter | ("new" "assigned" "under-review") | States shown in the listing buffer until you change the filter at runtime. Set to nil to show every state. Patchwork’s actual state names vary by deployment, so adjust this to match yours. |
The listing buffer highlights a whole series row using
patchwork-series-highlight-rules, a list of functions each called
with a series plist, returning a face or nil; the first to return
non-nil wins. Built in, in order:
| Rule | Face | Condition |
|---|---|---|
patchwork-series--rule-mine | patchwork-series-mine-face | Assigned to one of patchwork-my-identities (a list of usernames/emails considered “you”, matched case-insensitively as a substring). |
patchwork-series--rule-stale | patchwork-series-stale-face | Older than patchwork-series-stale-days (default 14) and has no comments yet. |
patchwork-series--rule-old | patchwork-series-old-face | Older than patchwork-series-old-days (default 30), regardless of comments. |
Add your own rule by pushing a function onto (the front of)
patchwork-series-highlight-rules — anything that takes a series
plist and returns a face symbol or nil works, e.g. one that checks
(plist-get series :state). The title and submitted-date columns
also each get their own face (patchwork-series-title-face~/
~patchwork-series-date-face) independent of the row-highlight rules.
In a series detail buffer: a patch’s summary line uses
patchwork-patch-line-face to stand out from its comments; an
expanded comment’s quoted lines (>-prefixed, any nesting depth) use
patchwork-comment-quote-face and its actual reply text uses
patchwork-comment-reply-face; a CI check’s line uses
patchwork-check-success-face~/-warning-face~/~-fail-face~ depending
on its state.
M-x patchwork-show-series syncs every configured server/project
(subject to patchwork-cache-ttl) and opens the main listing buffer.
Series are grouped under collapsible [+]~/~[-] headers, one per
server/project pair, each showing title, author, submitted date,
comment count, Ack/Review/Test/Fixes tag counts, CI check
Success/Warning/Failure counts, assignee, and state.
patchwel doesn’t bind any global key of its own, so entering it is
just another M-x away — but if you’d rather have a dedicated chord,
bind whatever you like to it:
(global-set-key (kbd "C-c p") #'patchwork-show-series)With a prefix argument (C-u C-c p here), it shows whatever is
already cached without syncing first — handy if something else (e.g.
a cron job) is already keeping the cache warm.
| Key | Command | Action |
|---|---|---|
RET | patchwork-series-dwim | On a group header: toggle it. On a series row: open its detail buffer. |
TAB | patchwork-series-dwim | Same as RET. |
g | patchwork-series-refresh | Re-sync. With a prefix argument (C-u g), force a full resync (bypassing both the TTL and incremental/events-based sync) rather than the usual incremental sync. |
l | patchwork-series-redisplay | Redraw from whatever is already cached — never contacts any server, unlike g (which still checks the TTL). Use this when a crontab (see Background sync via cron) is already keeping the cache fresh and you just want the buffer to reflect that. |
G | patchwork-fetch-series | Fetch and cache one specific series by server + id directly, regardless of patchwork-sync-lookback-days, then open its detail buffer. |
a | patchwork-series-apply-at-point | Apply every patch in the series at point to a git repository, working-tree only (git apply, no commits). |
R | patchwork-series-review-at-point | Apply the series at point as real commits and open a browsable view of them — see Applying a series and starting a review. |
s | patchwork-series-set-state-at-point | Bulk-set the state of every patch in the series at point — see Setting patch/series state and delegate. |
d | patchwork-series-set-delegate-at-point | Bulk-set the delegate of every patch in the series at point. |
e | patchwork-series-edit-note-at-point | Edit the note for the series at point, in a dedicated buffer — see Taking notes on a patch or series. |
k | patchwork-series-purge-group-at-point | On a group header: purge every cached series/patch/comment/check/queued change for that server+project, after confirming — see Purging a project’s cached data. |
n~/~p | patchwork-series-next~/-prev~ | Move to the next/previous series row. |
N~/~P | patchwork-series-next-server~/-prev-server~ | Move to the next/previous server, skipping over any remaining project groups under the current one. |
f | patchwork-series-set-filter | Interactively edit the state/server/project/author filter, prefilled with the current values. |
F | patchwork-series-reset-filter | Reset the filter back to patchwork-default-state-filter. |
+ | patchwork-series-expand-all | Expand every group. |
- | patchwork-series-collapse-all | Collapse every group. |
q | quit-window | Close the buffer. |
A series detail buffer lists its title/author/tags/check counts (its web URL too, as a clickable button, when the server reports one), then each patch (its commit message and diff, collapsed by default) and each patch’s comments and CI checks.
| Key | Command | Action |
|---|---|---|
RET | patchwork-series-detail-toggle-at-point | Expand/collapse the patch or comment at point. |
TAB | patchwork-series-detail-toggle-at-point | Same as RET. |
r | patchwork-series-detail-reply-at-point | Reply, as mail, to the comment or patch at point — see Replying to comments and patches. |
g | patchwork-series-detail-refresh | Redraw this series’ detail (title, author, tags, checks, patches, comments). |
a | patchwork-series-detail-apply | Apply every patch in this series, working-tree only (git apply, no commits). |
R | patchwork-series-detail-review | Apply this series as real commits and open a browsable view of them. |
s | patchwork-series-detail-set-state-at-point | Set the state of the patch at point — see Setting patch/series state and delegate. |
d | patchwork-series-detail-set-delegate-at-point | Set the delegate of the patch at point. |
S | patchwork-series-detail-set-series-state | Bulk-set the state of every patch in this series, regardless of point. |
D | patchwork-series-detail-set-series-delegate | Bulk-set the delegate of every patch in this series, regardless of point. |
e | patchwork-series-detail-edit-note-at-point | Edit the note for the patch at point, in a dedicated buffer — see Taking notes on a patch or series. |
E | patchwork-series-detail-edit-series-note | Edit the note for the whole series, regardless of point. |
n~/~p | patchwork-series-detail-next-patch~/-prev-patch~ | Move to the next/previous patch’s summary line, regardless of expand state. |
N~/~P | patchwork-series-detail-next-comment~/-prev-comment~ | Move to the next/previous comment’s summary line. |
+ | patchwork-series-detail-expand-all | Expand every patch and comment in this buffer. |
- | patchwork-series-detail-collapse-all | Collapse every patch and comment in this buffer. |
q | quit-window | Close the buffer. |
A CI check’s target_url, when it has one, is rendered as a clickable
button (RET/mouse-2) that opens it with browse-url rather than as
plain text.
s~/~d set the state/delegate of a single patch (the patch at point
in a detail buffer, or every patch in the series at point in the
listing buffer); S~/~D in a detail buffer bulk-set the whole series
regardless of point. Patchwork has no native per-series state or
delegate — the series-wide commands just apply the same value to
every patch currently in that series, one at a time, and a single
patch’s failure is reported but doesn’t stop the rest from being
attempted.
Setting either field is offline-tolerant: the change is attempted immediately, and if the server can’t be reached right now (a timeout, a 5xx, or any other network-level failure), it’s queued locally instead of failing outright. A queued change is retried automatically the next time a sync against that server succeeds. If the patch’s real state/delegate turns out to have changed server-side in the meantime (someone else touched it before the queued change was ever applied), the queued change is discarded and reported rather than applied — it is not silently forced through, and it is not kept around for later review.
Wherever a patch or series’ state/delegate is already shown, a queued
change shows alongside the current value. The series detail buffer
shows the full form, e.g. State: new (pending: rejected), on both
the series-wide header lines and each patch’s own summary line; the
main listing buffer’s columns are fixed-width and shared across every
row, so there’s only room for a compact trailing * marker instead —
open the series’ detail buffer to see what’s actually queued.
Note: the locally cached delegate is a resolved display name, but Patchwork’s write API expects a delegate user id or username (which varies by deployment) — there’s no name-to-id lookup here, so type whatever your server’s API actually expects when prompted.
e edits the note for the series at point in the listing buffer, or
the patch at point in a detail buffer; E in a detail buffer edits the
note for the whole series shown there, regardless of point. Each opens
a dedicated buffer, pre-filled with the existing note if any —
C-c C-c saves and closes it, C-c C-k discards any changes and
closes it without saving. Saving blank (or whitespace-only) content
deletes the note; there’s no separate delete command.
A note is local to patchwel, not sent to the Patchwork server or tied to a mail draft — it’s for tracking your own review state (e.g. “still waiting on CI”, “asked for X, not yet addressed”) right next to the series/patch it’s about, distinct from composing an actual reply (see Replying to comments and patches).
Wherever a patch or series already renders, a note shows alongside it:
the listing buffer’s Nt column shows a compact * marker when a
series has a note (there isn’t room there for the text itself); the
series detail buffer shows the full text in a Notes: block for the
series, and a ~ [note]~ marker on any patch with one, in full under a
--- Note --- block once that patch is expanded.
Note: a note is tied to the current cached patch/series id, which is new every version of a series (v1, v2, v3, …) — it does not carry forward automatically when a new version arrives. This was a deliberate scope decision, not an oversight: an old version can already have aged out of the local cache by the time a new one shows up, which would make any such carry-over unreliable exactly when it would matter most.
G (patchwork-fetch-series) prompts for a server and a series id,
then fetches and caches just that series — its patches, comments, and
checks — regardless of patchwork-sync-lookback-days or which project
it belongs to, and opens its detail buffer. Useful for a series you
already know the id of (e.g. found by browsing the Patchwork web UI
directly) that’s too old, or otherwise outside the range, for a
routine sync to ever reach on its own.
r in a series detail buffer (patchwork-series-detail-reply-at-point)
composes a wide-reply message-mode buffer for whatever is at point:
- On a comment: To is the comment author’s email; Cc is the
original To/Cc addresses minus the new To and minus
user-mail-address; Subject gets a “Re: ” prefix unless already present; In-Reply-To is the comment’s Message-ID; References is the comment’s own References chain plus its Message-ID appended. The comment’s body is quoted one level deeper than it already was. - On a patch: same wide-reply treatment, replying to the patch submission itself (its mail headers are fetched live if not already cached, since the listing/sync’s list view doesn’t carry them).
This assumes message-mode is already configured to send mail the way
you normally would from Emacs (e.g. smtpmail or similar) — patchwel
just composes the buffer. If you don’t already have that set up, see
the Gnus manual (message-mode itself is documented there, under
“Message”), or this quick-start Gnus setup guide — or
the appendix below for a minimal example that gets a Gmail account
able to send via smtpmail and ~~/.authinfo.gpg~.
Note: replies compose through gnus-msg-mail instead of plain
message-mail whenever Gnus is actually running in the current
session (gnus-alive-p) — per its own docstring, gnus-msg-mail is
“like message-mail, but with Gnus paraphernalia, particularly the
Gcc: header for archiving purposes”, since a Gcc header alone does
nothing without going through the setup (gnus-setup-message) that
actually processes it at send time. Falls back to plain message-mail
when Gnus isn’t running, exactly as before. If you’re not running
Gnus at all (a plain smtpmail setup, say) and still want sent replies
archived somewhere, or Gnus’s own posting-style matching still isn’t
picking up the way you expect, see patchwork-mail-compose-hook under
Custom hooks to add whatever Gcc/Fcc header your setup needs by hand.
The same gap applies to Gnus’s “replied” mark on the original
article: replying from a Gnus summary buffer registers a send action
that marks the source article once the reply is actually sent, but a
patchwel-composed reply never goes through that summary-buffer flow,
so the original never gets marked. Unverified idea, not tested
against a live Gnus/backend setup – since the Message-ID being
replied to is already sitting in the composed buffer’s In-Reply-To
header, a similar send action can be registered by hand in
patchwork-mail-compose-hook:
(add-hook 'patchwork-mail-compose-hook
(lambda ()
(let ((msgid (message-fetch-field "In-Reply-To")))
(when msgid
(message-add-action
(lambda ()
(save-window-excursion
(when (gnus-summary-refer-article msgid)
(gnus-summary-mark-article-as-replied msgid))))
'send)))))gnus-summary-refer-article locates which group/article a Message-ID
belongs to (via the registry if gnus-registry-mode is on, or
gnus-refer-article-methods otherwise); gnus-summary-mark-article-as-replied
is the same primitive Gnus’s own reply flow uses to set the mark. Try
it and confirm the “replied” mark actually shows up in your summary
buffer afterward – some Gnus versions may want a numeric article
number there instead of the Message-ID string.
Beyond the a~/~R keybindings above, applying is also available as
M-x commands (each downloads a patch’s mbox from Patchwork on demand
rather than requiring it to already be cached locally):
patchwork-apply-patch— apply a single patch to a git repository’s working tree (git apply; no commit is created).patchwork-apply-series— apply every patch in a series this way, in order.patchwork-undo-patch— reverse-apply a previously-applied patch.patchwork-apply-series-as-commits— apply every patch in a series via a singlegit am --3waycall instead, so each patch becomes its own real commit with its original author, date, and commit message preserved from the mail (--3waymeans a failing patch falls back to a real merge with conflict markers, rather than simply refusing to apply). Returns the before/after HEAD commit hashes, which is what lets the result be browsed afterwards.patchwork-review-series(R) — ties the above together: applies a series as commits, then opens a browsable view of them viapatchwork-review-backends(see Custom hooks) — magit’s log buffer over the applied range if magit is loaded, otherwise alog-view-modebuffer via Emacs’s built-invc. Both give the usual n/p-between-commits, RET-to-view-diff navigation their mode already provides.
patchwork-project-git-trees is an alist mapping
(SERVER-URL . PROJECT-SLUG) to a git repository directory, since
different projects usually need different trees. It’s populated on
demand: the first time you apply a series for a given project, you’re
prompted for a directory and offered to save it here for next time;
edit the variable directly (or via M-x customize-variable) to fix a
wrong path or set one up ahead of time.
By default a series is applied to whatever is currently checked out.
To instead have it create and check out its own branch every time,
configure patchwork-project-branch-strategies (same
(SERVER-URL . PROJECT-SLUG) alist shape, falling back to
patchwork-default-branch-strategy if a project has no entry) with a
string of the form "BASE:TARGET-TEMPLATE":
(setq patchwork-project-branch-strategies
'((("https://patchwork.ozlabs.org/api" . "openvswitch")
. "upstream/main:review_%i")))This means: every time a series from that project is applied, create
and check out a new branch named by substituting the series id for
%i (review_514346 for series 514346), based on the tip of
upstream/main. Applying errors out rather than reusing or resetting
an already-existing target branch, so applying the same series twice
under a strategy is a mistake that surfaces instead of silently
overwriting whatever was already there.
If git am --3way stops (a conflict, or anything else), the
repository is left mid-~am~ and a *patchwork-am: <repo>* buffer opens
automatically showing its output, the patch email it stopped on, and
each conflicted file as a button (RET/mouse-2) to open directly — no
need to drop to a shell to see what happened. From there:
M-x patchwork-git-am-status— show the same view again on demand (e.g. coming back to it later, from a fresh Emacs session).M-x patchwork-git-am-continue— after resolving conflicts by hand, continue.M-x patchwork-git-am-skip— skip the current patch.M-x patchwork-git-am-abort— give up and restore the original branch.
Each prompts for the repository directory and refreshes the status buffer with the result.
Every patch mbox patchwork-git-download-patch downloads (for either
kind of apply, or a review) is cached in patchwork-git-temp-dir
(~~/.cache/patchwel/patchwork-patches~) — nothing deletes them on its
own, so the directory only grows. Two ways to reclaim that space:
M-x patchwork-git-prune-patches— deletes cached files older thanpatchwork-patch-cache-max-age-days(default 30). With a prefix argument (C-u M-x patchwork-git-prune-patches), deletes every cached file regardless of age. Prompts for confirmation, then reports how many files and how much disk space were freed.- Automatically, during sync:
patchwork-prune-on-terminal-states(a list of state strings —accepted,rejected,deferred,changes-requested,superseded,not-applicableby default, since real state names vary by deployment; set tonilto disable) drives pruning a patch’s cached mbox the moment a sync observes it transition from some other state into one of these — review/apply work is presumably done by then. Re-syncing a patch already in a terminal state is a no-op, not a repeated deletion attempt.
If a project ever gets polled by mistake (e.g. a misconfigured
patchwork-servers entry or a bad sync run) and you want to remove
just its cached series/patches/comments/checks without touching any
other project or server’s data, use
M-x patchwork-db-purge-project. It prompts for a server and a
project slug (completing against whatever’s actually cached for that
server), reports how many series and patches would be removed, asks
for confirmation, then deletes them along with any queued pending
change, any notes on those patches/series, and the project’s own row.
Scoped by the cached series’ own project slug, so it also cleans up
correctly even if the project was queried directly and never got its
own row in the local projects table.
From the listing buffer, k on a server+project group header
(patchwork-series-purge-group-at-point) does the same thing without
leaving the buffer — it prompts for confirmation naming the server and
project, then purges and redraws.
Three extension points let you plug in your own behavior without modifying patchwel itself:
patchwork-api-challenge-hook— run when a Patchwork server’s response looks like an anti-bot challenge page rather than the expected JSON (see Handling quirky Patchwork deployments). Each function is called, viarun-hook-with-args-until-success, with(SERVER URL BUFFER), where BUFFER holds the complete raw HTTP response that looked like a challenge; the first function to return a substitute buffer (same raw “status line, headers, blank line, body” shape) wins, and it’s processed instead. A function that can’t handle a given response should returnnil. Empty by default — Emacs cannot solve such a challenge on its own, so this exists purely so a dedicated module could plug in later (e.g. one that solves an Anubis proof-of-work challenge and re-issues the request).patchwork-review-backends— tried, in order, to open a browsable view of a series’ just-applied commits afterpatchwork-review-series(see Applying a series and starting a review). Each function is called, viarun-hook-with-args-until-success, with(REPO-DIR BEFORE-REV AFTER-REV)and should return non-nil if it opened a view, ornilto let the next backend try. Defaults to(patchwork-review-backend-magit patchwork-review-backend-vc); add your own function to the front of the list to prefer a different review tool (e.g.code-review.el,forge).git-timemachinedoesn’t fit this hook, since it walks one file’s revision history rather than a range of commits across a whole tree — but it still works as normal on any file touched by a just-applied series (M-x git-timemachinein that file’s buffer).patchwork-mail-compose-hook— run with no arguments once a reply (see Replying to comments and patches) is fully composed, in thatmessage-modebuffer. Since replies are composed via plainmessage-mailrather than through Gnus’s own summary-reply commands, Gnus-specific context such a buffer would normally have (gnus-newsgroup-name,message-reply-headers) is never set, so any sent-mail archiving that depends on it may not fire. Use this hook to insert whatever your setup needs, e.g. a Gcc header matching a monthly-rotating archive group:(add-hook 'patchwork-mail-compose-hook (lambda () (message-add-header (format-time-string "Gcc: nnfolder+archive:sent.%Y-%m"))))
Empty by default.
The interactive listing buffer only syncs when you ask it to (opening
it, or g~/~C-u g). For a fresher cache without having to remember to
refresh, patchwork-cron-sync.el is a small, self-contained script you
can drive from a crontab with a headless Emacs, independent of any
interactive session:
*/5 * * * * /usr/bin/flock -n ~/.cache/patchwel/cron-sync.lock /usr/bin/emacs --batch -l /path/to/patchwel/patchwork-cron-sync.el >> ~/.cache/patchwel/sync.log 2>&1The flock -n prefix stops two ticks from ever running at once: if a
previous sync is still going (e.g. a slow network made it take longer
than the 5-minute interval), the next tick just skips instead of
starting a second, overlapping process — see “Overlapping runs” below.
It needs ~~/.cache/patchwel/~ to already exist, which it will after the
very first ordinary use of patchwel (or of
patchwork-generate-cron-config.el); create it by hand first if this
is the very first thing you’re setting up.
Before using it, set up patchwork-cron-config.el — the file
patchwork-cron-sync.el loads for its actual patchwork-servers (and
any other patchwork-* settings). It’s deliberately not tracked by
git (see .gitignore): patchwork-cron-sync.el itself contains only
functional logic and no configuration of your own, so `git pull` can
always update it freely without ever conflicting with your settings —
editing patchwork-cron-sync.el directly is exactly the mistake that
guarantees a conflict the moment that file changes upstream.
patchwork-cron-sync.el deliberately does not load your normal
init.el either — init files often assume a display, are slow to
load, or do things that only make sense interactively.
Two ways to set that file up:
- Generate it from your real interactive config (recommended, keeps
the two in sync automatically instead of by hand): run
patchwork-generate-cron-config.elonce, and again any time you change your interactivepatchwork-serverset al. Unlikepatchwork-cron-sync.el, this script’s whole job is to load your init file — that’s a cost worth paying for a one-off you run by hand, not a five-minute cron tick.emacs --batch -l /path/to/patchwel/patchwork-generate-cron-config.el -- ~/.emacs.d/init.elDefaults to `user-init-file` if you omit the path. If your patchwel setup is loaded lazily by a package manager (e.g.
:defer t), a plain batch load of your init file may never actually trigger it — the script warns ifpatchwork-serversstill looks unbound/empty afterward, since the generated file would otherwise end up silently empty. - Or write it by hand: copy
patchwork-cron-config.el.exampletopatchwork-cron-config.el(same directory) and edit that copy directly — simplest if your init file can’t run headless at all, at the cost of having to update it yourself when your interactive config changes.
Either way, point the crontab entry at wherever you’ve actually put
the patchwel source directory and patchwork-cron-sync.el. If you’d
rather keep the generated/hand-written config somewhere else entirely
(e.g. a private dotfiles repo), point $PATCHWORK_CRON_CONFIG at it —
both patchwork-cron-sync.el and patchwork-generate-cron-config.el
read/write that same location when it’s set.
Notes on how this interacts with an interactive session:
- Running the cron job more often than
patchwork-cache-ttlis harmless — a sync that finds the cache still fresh does no network work at all, so the cron interval just bounds worst-case staleness, not request volume. - The local SQLite database is opened with WAL journaling and a busy timeout, so the cron process and an interactive Emacs session can read and write it concurrently without “database is locked” errors.
- The interactive buffer won’t redraw on its own just because the
cache changed underneath it — you still need
gto re-query and redraw (orlfor a display-only redraw that never touches the network itself, if cron alone is what you want keeping the cache fresh — see the keybinding table above).
The plain crontab entry above syncs every configured server serially,
one after another, in a single Emacs process. A sync’s wall-clock time
is dominated by serialized network round-trips (one HTTP request has
to finish before the next starts), not by Emacs Lisp itself, so with
several servers configured this can take meaningfully longer than
syncing just one. patchwork-cron-sync-parallel.sh drives
patchwork-cron-sync.el once per configured server instead, all
running as separate concurrent processes:
*/5 * * * * /path/to/patchwel/patchwork-cron-sync-parallel.sh >> ~/.cache/patchwel/sync.log 2>&1It reads the server list from patchwork-cron-sync.el itself (via a
--list-servers mode that just prints each configured :url and
exits — no sync, no db access), which in turn comes from your own
patchwork-cron-config.el same as the plain single-process case above
— nothing extra to configure. The local db’s WAL journaling and busy
timeout (already there for cron-vs-interactive concurrency) is exactly
what also makes it safe for these several processes to write to it at
once. Each server gets its own flock lock file
(~~/.cache/patchwel/cron-sync-<server>.lock~) internally, so if one
server’s sync is still running when the next tick fires, only that
server’s sync is skipped (logged to stderr, so it shows up in
sync.log) — the others still run on schedule.
patchwork-cron-sync.el itself still works standalone with a specific
server URL as its argument (everything after Emacs’s own -- marker),
if you want to drive the per-server parallelism some other way than
the provided wrapper script:
emacs --batch -l /path/to/patchwel/patchwork-cron-sync.el -- https://patchwork.ozlabs.org/apiDriving it this way yourself bypasses patchwork-cron-sync-parallel.sh’s
own per-server locking — add your own if you need it (e.g.
flock -n ~/.cache/patchwel/cron-sync-myserver.lock emacs --batch ...).
Nothing here corrupts the local cache if two sync processes ever do
end up running at once regardless — every write is an idempotent
upsert or delete, and the db’s WAL journaling plus busy timeout already
serialize concurrent writers safely. What overlap actually costs,
without a lock: duplicate network requests to the Patchwork server for
the same window, and a sync that hits enough write contention to
exceed the busy timeout just fails that tick’s sync outright (logged,
then self-heals next tick) rather than merging cleanly with the
still-running one. flock -n, as set up above, avoids all of that by
making an overlapping tick a no-op (it exits immediately, logging
nothing beyond flock itself declining to run) instead of a second,
competing process.
This isn’t part of patchwel — it’s a minimal, self-contained example
of getting message-mode able to actually send mail via SMTP, which is
what patchwork-mail-reply-to-comment~/-to-patch~ compose into. Skip
this if you already send mail from Emacs. For real documentation
rather than this cut-down example, see the Gnus manual or this
quick-start Gnus setup guide.
Store SMTP credentials encrypted rather than in plain text in
init.el — Emacs’s auth-source reads this file (via GPG) when
smtpmail needs a password:
machine smtp.gmail.com login you@gmail.com password APP-PASSWORD port 587
For Gmail specifically, APP-PASSWORD needs to be an App Password
(Google Account → Security → 2-Step Verification → App passwords),
not your regular account password, if 2-Step Verification is enabled
(as it is by default for most accounts) — Google’s SMTP no longer
accepts plain account passwords for third-party clients otherwise.
Encrypt the file once with:
gpg --encrypt --recipient you@example.com ~/.authinfo # writes ~/.authinfo.gpg
rm ~/.authinfo # remove the plaintext originalEmacs (via epa-file, loaded by default) will prompt for your GPG
passphrase the first time something reads the file in a session (or
use gpg-agent to cache it).
(setq user-mail-address "you@gmail.com"
user-full-name "Your Name")
(setq send-mail-function #'smtpmail-send-it
message-send-mail-function #'smtpmail-send-it
smtpmail-smtp-server "smtp.gmail.com"
smtpmail-smtp-service 587
smtpmail-stream-type 'starttls
smtpmail-smtp-user "you@gmail.com")With this in place, any message-mode buffer — including the ones
patchwork-mail-reply-to-comment~/-to-patch~ compose — sends with
C-c C-c.