Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

35 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

patchwel — an Emacs interface to Patchwork

About

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.

License

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.

Requirements

  • 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.

Installation

Put the patchwel-*.el files somewhere on your load-path and require the package:

(add-to-list 'load-path "/path/to/patchwel")
(require 'patchwel)

Configuration

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.

patchwork-servers

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, or nil for anonymous, read-only access. Required if you want to set delegates/state.
  • :projects — list of project slugs (or ids) to sync on this server, or nil to sync every project visible to that server/token.
  • :since-format — optional; forces which since= timestamp format is sent to this server when doing an incremental sync (a key into patchwork-since-format-strings: z, naive, or date). See below; leave nil to auto-detect.
  • :user-agent — optional User-Agent string to send to this server instead of patchwork-user-agent, e.g. if a server admin has exempted a specific string for you. See below; leave nil for the default.
  • :sync-timeout — optional; seconds to wait for a single list/summary request (series/patches/comments/checks/events) to this server instead of patchwork-sync-timeout. See below; leave nil for the default.
  • :events-omit-project — optional boolean; never send project= on /events/ requests to this server at all. See below; leave nil unless 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.

Handling quirky Patchwork deployments

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.org and patches.dpdk.org both want a Z-suffixed UTC datetime, while patchwork.kernel.org 500s on any timezone marker at all and wants a naive one. patchwel tries each format in patchwork-since-format-strings (z, then naive, then a bare date) until one works, caching the winner; set a server’s :since-format to 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-agent if a server admin has exempted a specific string for you. If a response still looks like a non-JSON challenge page, patchwork-api-challenge-hook is 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 than patchwork-sync-timeout’s default (5 seconds) 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-timeout rather than the global default.
  • ~project=~ breaking ~/events/~ entirely: on patchwork.kernel.org (observed on the netdevbpf project), including project= on an /events/ request 502s outright, regardless of since= format or timeout — dropping the parameter fixes it immediately. Real Patchwork events already carry their own :project field independently of the query filter, so setting a server’s :events-omit-project makes 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 :projects is normally meant to avoid paying for.

Other options

VariableDefaultPurpose
patchwork-local-db-file~~/.cache/patchwel/patchwork.db~Path to the local SQLite cache.
patchwork-cache-ttl300 (seconds)How fresh the cache must be before a sync is skipped.
patchwork-sync-lookback-days30History pulled on a server/project’s first sync (and on forced/fallback full resyncs).
patchwork-sync-timeout5 (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-timeout15 (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-size100Results 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.

Highlighting

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:

RuleFaceCondition
patchwork-series--rule-minepatchwork-series-mine-faceAssigned to one of patchwork-my-identities (a list of usernames/emails considered “you”, matched case-insensitively as a substring).
patchwork-series--rule-stalepatchwork-series-stale-faceOlder than patchwork-series-stale-days (default 14) and has no comments yet.
patchwork-series--rule-oldpatchwork-series-old-faceOlder 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.

Usage

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.

Keybindings — series listing buffer

KeyCommandAction
RETpatchwork-series-dwimOn a group header: toggle it. On a series row: open its detail buffer.
TABpatchwork-series-dwimSame as RET.
gpatchwork-series-refreshRe-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.
lpatchwork-series-redisplayRedraw 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.
Gpatchwork-fetch-seriesFetch and cache one specific series by server + id directly, regardless of patchwork-sync-lookback-days, then open its detail buffer.
apatchwork-series-apply-at-pointApply every patch in the series at point to a git repository, working-tree only (git apply, no commits).
Rpatchwork-series-review-at-pointApply the series at point as real commits and open a browsable view of them — see Applying a series and starting a review.
spatchwork-series-set-state-at-pointBulk-set the state of every patch in the series at point — see Setting patch/series state and delegate.
dpatchwork-series-set-delegate-at-pointBulk-set the delegate of every patch in the series at point.
epatchwork-series-edit-note-at-pointEdit the note for the series at point, in a dedicated buffer — see Taking notes on a patch or series.
kpatchwork-series-purge-group-at-pointOn 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~/~ppatchwork-series-next~/-prev~Move to the next/previous series row.
N~/~Ppatchwork-series-next-server~/-prev-server~Move to the next/previous server, skipping over any remaining project groups under the current one.
fpatchwork-series-set-filterInteractively edit the state/server/project/author filter, prefilled with the current values.
Fpatchwork-series-reset-filterReset the filter back to patchwork-default-state-filter.
+patchwork-series-expand-allExpand every group.
-patchwork-series-collapse-allCollapse every group.
qquit-windowClose the buffer.

Keybindings — series detail 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.

KeyCommandAction
RETpatchwork-series-detail-toggle-at-pointExpand/collapse the patch or comment at point.
TABpatchwork-series-detail-toggle-at-pointSame as RET.
rpatchwork-series-detail-reply-at-pointReply, as mail, to the comment or patch at point — see Replying to comments and patches.
gpatchwork-series-detail-refreshRedraw this series’ detail (title, author, tags, checks, patches, comments).
apatchwork-series-detail-applyApply every patch in this series, working-tree only (git apply, no commits).
Rpatchwork-series-detail-reviewApply this series as real commits and open a browsable view of them.
spatchwork-series-detail-set-state-at-pointSet the state of the patch at point — see Setting patch/series state and delegate.
dpatchwork-series-detail-set-delegate-at-pointSet the delegate of the patch at point.
Spatchwork-series-detail-set-series-stateBulk-set the state of every patch in this series, regardless of point.
Dpatchwork-series-detail-set-series-delegateBulk-set the delegate of every patch in this series, regardless of point.
epatchwork-series-detail-edit-note-at-pointEdit the note for the patch at point, in a dedicated buffer — see Taking notes on a patch or series.
Epatchwork-series-detail-edit-series-noteEdit the note for the whole series, regardless of point.
n~/~ppatchwork-series-detail-next-patch~/-prev-patch~Move to the next/previous patch’s summary line, regardless of expand state.
N~/~Ppatchwork-series-detail-next-comment~/-prev-comment~Move to the next/previous comment’s summary line.
+patchwork-series-detail-expand-allExpand every patch and comment in this buffer.
-patchwork-series-detail-collapse-allCollapse every patch and comment in this buffer.
qquit-windowClose 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.

Setting patch/series state and delegate

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.

Taking notes on a patch or series

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.

Fetching a specific series by id

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.

Replying to comments and patches

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.

Applying a series and starting a review

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 single git am --3way call instead, so each patch becomes its own real commit with its original author, date, and commit message preserved from the mail (--3way means 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 via patchwork-review-backends (see Custom hooks) — magit’s log buffer over the applied range if magit is loaded, otherwise a log-view-mode buffer via Emacs’s built-in vc. Both give the usual n/p-between-commits, RET-to-view-diff navigation their mode already provides.

Choosing a git tree per project

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.

Applying to a dedicated branch

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.

When git am fails

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.

Pruning cached patch files

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 than patchwork-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-applicable by default, since real state names vary by deployment; set to nil to 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.

Purging a project’s cached data

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.

Custom hooks

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, via run-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 return nil. 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 after patchwork-review-series (see Applying a series and starting a review). Each function is called, via run-hook-with-args-until-success, with (REPO-DIR BEFORE-REV AFTER-REV) and should return non-nil if it opened a view, or nil to 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-timemachine doesn’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-timemachine in 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 that message-mode buffer. Since replies are composed via plain message-mail rather 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.

Background sync via cron

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>&1

The 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.el once, and again any time you change your interactive patchwork-servers et al. Unlike patchwork-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.el
        

    Defaults 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 if patchwork-servers still looks unbound/empty afterward, since the generated file would otherwise end up silently empty.

  • Or write it by hand: copy patchwork-cron-config.el.example to patchwork-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-ttl is 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 g to re-query and redraw (or l for 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).

Syncing multiple servers concurrently

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>&1

It 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/api

Driving 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 ...).

Overlapping runs

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.

Appendix: a minimal mail-sending setup

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.

~~/.authinfo.gpg~

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 original

Emacs (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).

Minimal init.el snippet

(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.

About

A patchwork procelain for conducting reviews and basic patchwork project maintenance from within emacs.

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages