23 Sept 2025

Saving Patches Within Mutt

For any project that uses an email-based patch workflow, I prefer to use mutt as my email client for patch review. Mutt is a highly configurable, cmdline-driven email program. I can start mutt, open a folder, or switch folders, jump to a message by number, jump to a message by searching subject lines for a string or name, search within a message for a string, scroll through the email list or a message using vi keystrokes, view a patch using a colouring scheme that appeals to me, and, if I want to reply, mutt can open up my regular vi for email composition with the email to which I'm replying already quoted... all from the keyboard. It is fast and efficient.

Sometimes, however, simply looking at a patch isn't enough. Sometimes I will look at a patch and think to myself: "does that really solve the bug?" or: "does this patch apply cleanly?" or: "I'm pretty sure that doesn't even compile". In these cases the only solution is to download the patch and give it a try.

Depending on the project's workflow, or even your particular workflow on a given day, there may be several ways to download patches:

  • for projects that use an email workflow, if the project has a lore server, you can use b4 to fetch patches given the lore server's URL of the patch or patch series
  • for projects that use an email workflow, if the project uses a patchwork server, you can use git-pw to fetch patches with the patchwork instance's PATCH-ID, or a patch series using patchwork's SERIES-ID
  • if the project regularly pushes to-be-reviewed patches into specifically designated branches of a git repository (likely not fast-forwardable), you can fetch the patches in the designated branch and and use "git format-patch ..." to create them
  • if a project uses github (publicly or privately) you can surf to a specific commit, then modify the URL by hand to add the string ".patch" on the end, and your browser will display that commit as a plain-text patch which you can then download; similarly you can append ".diff" to get a unified diff instead
  • if I am already using mutt to review patches, and there are patches I want to download, I simply download them via mutt

Mutt comes with built-in abilities to download emails. Whether you have a specific email open in pager mode, or simply have an email highlighted in index mode, press the pipe character (|) and mutt will ask you (in the command area): "Pipe to command: ". Enter something such as "cat > /path/to/file" and the email will be saved to file /path/to/file.

That works, an you can "git am ..." the resulting file, but what you have saved is an email (headers and all), not a patch. Although "git am ..." handles email well, if that email is made up of various MIME segments, they can introduce funny artifacts into your patch comments (for example). Also, if you want to download sets of patches, or a several series' of patches, you will need to organize them yourself to remember which patch is which.

One feature that I have added is an enhanced way of saving patches. One that is easy to use, that creates actual patches from emails, that handles MIME, and organizes my patches so it is easy to find (and apply) them afterwards. Here is how I've integrated a fancy way of saving patches from mutt which can then be easily "git am ..."'ed to a repository, or processed with some sort of patch review tool.

Mutt has a concept of tagging emails. Whether in index mode or pager mode, you can tag an individual email using "t", tag based on a pattern using "SHIFT-t", or tag an entire thread using "ESC-t". Simply tag whichever emails you want to save, then press "(p" and the patches will be saved and organized. Mutt can be expanded with macros, so I have created a macro which will invoke a script for each tagged email. Mutt will call the script for each tagged email, and will pipe the email to the script's STDIN.

Somewhere in my ~/.mutt/muttrc, or included into it, I have the following line:

macro index,pager (p "<enter-command>unset wait_key<enter><tag-prefix><pipe-message>mutt-save-patch<enter><enter-command>set wait_key<enter>" "output git patches"

Then I need to create the mutt-save-patch program/script somewhere in mutt's PATH. My script makes use of two utilities: formail (from procmail) and ripMIME. formail is a filter for manipulating emails, and ripMIME is a tool for extracting MIME attachments. The script starts by saving off the email into a temporary file, it then uses the email's various metadata (from the header) to create a filename and a directory into which to save the email. Email threads will be saved to the same directory. It then converts the email into a patch, includes only the patch-specific components of the email, and saves it off as a patch in a file, one file per patch.

One small annoyance I have encountered is the Yocto email server. I have noticed that when developers send patches to the various Yocto mailing lists, the Yocto email server re-formats the email as a set of MIME attachments: one that is empty, one that contains the patch itself (the body of the email), then a third one that contains a mailing-list-specific footer. If the Yocto email server did not do this, then the patch would be easier to extract. Adding these MIME sections forced me to add ripMIME to the script. Fortunately applying ripMIME to an email that does not have MIME sections simply returns the body as-is, so introducing this step does not overly complicate the script. It is possible that handling MIME attachments could get more complicated when used with other projects, so this part of the script might need more work in the future.

Here's a copy of my current script (tweaks and updates occur from time-to-time):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#!/bin/bash

# make sure the directories ~/.mutt/tmp and ~/.mutt/patches exist before
# running this script

# this script requires the following programs be installed/available on
# the $PATH:
#       - formail (part of procmail)
#       - ripmime (https://github.com/inflex/ripMIME install by hand)

TMPFILE=$(mktemp --tmpdir="$HOME/.mutt/tmp" patch.XXXXXX)
TMPDIR=$(mktemp --tmpdir="$HOME/.mutt/tmp" --directory)

cat > $TMPFILE
MESSAGEID=$(cat $TMPFILE | formail -c -xMessage-ID: | sed -e 's/^[[:space:]]*//' | cut -d'<' -f2 | cut -d'@' -f1)
INREPLYTO=$(cat $TMPFILE | formail -c -xIn-Reply-To: | head -n1 | sed -e 's/^[[:space:]]*//' | cut -d'<' -f2 | cut -d'@' -f1)
PATCHNAME=$(cat $TMPFILE | formail -c -xSubject: | sed -e 's/^[[:space:]]*//' | tr "'" "." | sed -e '{ s@[*()" \t]@_@g; s@[/:]@-@g; s@^ \+@@; s@\.\.@.@g; s@-_@_@g; s@__@_@g; s@\.$@@; }' | cut -c 1-70).patch

DIRNAME=$MESSAGEID
if [ -n "$INREPLYTO" ]; then
        DIRNAME=$INREPLYTO
fi
mkdir -p $HOME/.mutt/patches/$DIRNAME

# NOTE: it's fine if both MESSAGEID and INREPLYTO are empty
#       in that case the patch will simply go in $HOME/.mutt/patches
formail -X From: -X Subject: -X Date: -X Message-ID: < $TMPFILE | formail > $HOME/.mutt/patches/$DIRNAME/$PATCHNAME
ripmime -i $TMPFILE -d $TMPDIR
for MIME in $(ls $TMPDIR); do
        cat $TMPDIR/$MIME | grep "^diff" > /dev/null 2>&1
        if [ $? -eq 0 ]; then 
                cat $TMPDIR/$MIME >> $HOME/.mutt/patches/$DIRNAME/$PATCHNAME
        fi
done

# cleanup
rm -f $TMPFILE
rm -fr $TMPDIR

For example, let's say I'm doing some review of some patches on oe-core in mutt. In the following screenshot I have identified a couple of patches that I want to download. I have tagged them, and you can identify which ones have been tagged by looking in the column between the status and the date/time. The asterisk identifies the emails which I have tagged:


With the emails I want to save tagged, I simply invoke the macro with "(p". On disk, this results in the following directories and files being created:

.mutt/patches
├── 20250922113920.2693840-1-alex.kanavin
│   ├── [OE-core]_[PATCH_1-2]_which_update_2.21_->_2.23,_build_with_meson.patch
│   └── [OE-core]_[PATCH_2-2]_cwautomacros_delete_the_recipe.patch
├── 20250922142031.3625684-1-ross.burton
│   ├── [OE-core]_[PATCH_1-4]_libdnf_don.t_depend_on_libcheck.patch
│   ├── [OE-core]_[PATCH_2-4]_libdnf_remove_obsolete_gobject-introspection_sup.patch
│   ├── [OE-core]_[PATCH_3-4]_libdnf_remove_non-functional_gtk-doc_support.patch
│   └── [OE-core]_[PATCH_4-4]_libdnf_remove_obsolete_path_path.patch
├── 20250923005234.2952070-1-Randy.MacLeod
│   └── [OE-core]_[PATCH_v3]_gawk_disable_persistent_memory_allocator_due_to_l.patch
└── 20250923125857.340991-1-hongxu.jia
    └── [OE-core]_[PATCH_v2]_perf_fix_reproducibility_issue_occasionally.patch

4 directories, 8 files

An example of which is:

$ cat .mutt/patches/20250922142031.3625684-1-ross.burton/\[OE-core\]_\[PATCH_2-4\]_libdnf_remove_obsolete_gobject-introspection_sup.patch 
From ross.burton=arm.com@lists.openembedded.org  Tue Sep 23 11:55:08 2025
From: "Ross Burton via lists.openembedded.org" <ross.burton=arm.com@lists.openembedded.org>
Subject: [OE-core] [PATCH 2/4] libdnf: remove obsolete gobject-introspection support
Date: Mon, 22 Sep 2025 15:20:29 +0100
Message-ID: <20250922142031.3625684-2-ross.burton@arm.com>

The intention to remove G-I support was stated in [1] and the last few
pieces removed in [2], which were part of 0.15.0.

[1] libdnf a4abd42a ("Move libcheck dependency to tests/")
[2] libdnf e2f2862b ("[swdb]: C++ implementation with SWIG bindings.")

Signed-off-by: Ross Burton <ross.burton@arm.com>
---
 meta/recipes-devtools/libdnf/libdnf_0.74.0.bb | 6 +-----
 1 file changed, 1 insertion(+), 5 deletions(-)

diff --git a/meta/recipes-devtools/libdnf/libdnf_0.74.0.bb b/meta/recipes-devtools/libdnf/libdnf_0.74.0.bb
index 0dce8dc183b..aa9e18e763f 100644
--- a/meta/recipes-devtools/libdnf/libdnf_0.74.0.bb
+++ b/meta/recipes-devtools/libdnf/libdnf_0.74.0.bb
@@ -19,16 +19,12 @@ UPSTREAM_CHECK_GITTAGREGEX = "(?P<pver>(?!4\.90)\d+(\.\d+)+)"
 
 DEPENDS = "glib-2.0 libsolv librepo rpm gtk-doc libmodulemd json-c swig-native util-linux"
 
-inherit gtk-doc gobject-introspection cmake pkgconfig setuptools3-base
+inherit gtk-doc cmake pkgconfig setuptools3-base
 
 EXTRA_OECMAKE = " -DPYTHON_INSTALL_DIR=${PYTHON_SITEPACKAGES_DIR} -DWITH_MAN=OFF -DPYTHON_DESIRED=3 \
-                  ${@bb.utils.contains('GI_DATA_ENABLED', 'True', '-DWITH_GIR=ON', '-DWITH_GIR=OFF', d)} \
                   -DWITH_TESTS=OFF \
                   -DWITH_ZCHUNK=OFF \
                   -DWITH_HTML=OFF \
                 "
-EXTRA_OECMAKE:append:class-native = " -DWITH_GIR=OFF"
-EXTRA_OECMAKE:append:class-nativesdk = " -DWITH_GIR=OFF"
-
 BBCLASSEXTEND = "native nativesdk"
 SKIP_RECIPE[libdnf] ?= "${@bb.utils.contains('PACKAGE_CLASSES', 'package_rpm', '', 'Does not build without package_rpm in PACKAGE_CLASSES due disabled rpm support in libsolv', d)}"
-- 
2.43.0


No comments:

Post a Comment