From: Adrian Ratiu <adrian.ratiu@collabora.com>
To: git@vger.kernel.org
Cc: Jeff King <peff@peff.net>,
Emily Shaffer <emilyshaffer@google.com>,
Junio C Hamano <gitster@pobox.com>,
Patrick Steinhardt <ps@pks.im>,
Josh Steadmon <steadmon@google.com>,
Kristoffer Haugsbakk <kristofferhaugsbakk@fastmail.com>,
"brian m . carlson" <sandals@crustytoothpaste.net>,
Adrian Ratiu <adrian.ratiu@collabora.com>
Subject: [PATCH v2 08/10] hook: add per-event jobs config
Date: Sun, 22 Feb 2026 02:29:02 +0200 [thread overview]
Message-ID: <20260222002904.1879356-9-adrian.ratiu@collabora.com> (raw)
In-Reply-To: <20260222002904.1879356-1-adrian.ratiu@collabora.com>
Add a hook.<event>.jobs count config that allows users to override the
global hook.jobs setting for specific hook events.
This allows finer-grained control over parallelism on a per-event basis.
For example, to run `post-receive` hooks with up to 4 parallel jobs
while keeping other events at their global default:
[hook]
post-receive.jobs = 4
Signed-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>
---
Documentation/config/hook.adoc | 19 +++++++++++
hook.c | 47 +++++++++++++++++++++++----
hook.h | 1 +
t/t1800-hook.sh | 59 ++++++++++++++++++++++++++++++++++
4 files changed, 120 insertions(+), 6 deletions(-)
diff --git a/Documentation/config/hook.adoc b/Documentation/config/hook.adoc
index 8894088bda..6ad23ac71d 100644
--- a/Documentation/config/hook.adoc
+++ b/Documentation/config/hook.adoc
@@ -33,9 +33,28 @@ hook.<name>.parallel::
found in the hooks directory do not need to, and run in parallel when
the effective job count is greater than 1. See linkgit:git-hook[1].
+hook.<event>.jobs::
+ Specifies how many hooks can be run simultaneously for the `<event>`
+ hook event (e.g. `hook.post-receive.jobs = 4`). Overrides `hook.jobs`
+ for this specific event. The same parallelism restrictions apply: this
+ setting has no effect unless all configured hooks for the event have
+ `hook.<friendly-name>.parallel` set to `true`. Must be a positive int,
+ zero is rejected with a warning. See linkgit:git-hook[1].
++
+Note on naming: although this key resembles `hook.<friendly-name>.*`
+(a per-hook setting), `<event>` must be the event name, not a hook
+friendly name. The key component is stored literally and looked up by
+event name at runtime with no translation between the two namespaces.
+A key like `hook.my-hook.jobs` is stored under `"my-hook"` but the
+lookup at runtime uses the event name (e.g. `"post-receive"`), so
+`hook.my-hook.jobs` is silently ignored even when `my-hook` is
+registered for that event. Use `hook.post-receive.jobs` or any other
+valid event name when setting `hook.<event>.jobs`.
+
hook.jobs::
Specifies how many hooks can be run simultaneously during parallelized
hook execution. If unspecified, defaults to 1 (serial execution).
+ Can be overridden on a per-event basis with `hook.<event>.jobs`.
Some hooks always run sequentially regardless of this setting because
git knows they cannot safely be parallelized: `applypatch-msg`,
`pre-commit`, `prepare-commit-msg`, `commit-msg`, `post-commit`,
diff --git a/hook.c b/hook.c
index 6214276e3d..013e41a8d6 100644
--- a/hook.c
+++ b/hook.c
@@ -133,6 +133,8 @@ struct hook_config_cache_entry {
* event_hooks: event-name to list of friendly-names map.
* disabled_hooks: set of friendly-names with hook.name.enabled = false.
* parallel_hooks: friendly-name to parallel flag.
+ * event_jobs: event-name to per-event jobs count (heap-allocated unsigned int *,
+ * where NULL == unset).
* jobs: value of the global hook.jobs key. Defaults to 0 if unset.
*/
struct hook_all_config_cb {
@@ -140,6 +142,7 @@ struct hook_all_config_cb {
struct strmap event_hooks;
struct string_list disabled_hooks;
struct strmap parallel_hooks;
+ struct strmap event_jobs;
unsigned int jobs;
};
@@ -222,6 +225,20 @@ static int hook_config_lookup_all(const char *key, const char *value,
int v = git_parse_maybe_bool(value);
if (v >= 0)
strmap_put(&data->parallel_hooks, hook_name, (void *)(uintptr_t)v);
+ } else if (!strcmp(subkey, "jobs")) {
+ unsigned int v;
+ if (!git_parse_uint(value, &v))
+ warning(_("hook.%s.jobs must be a positive integer, ignoring: '%s'"),
+ hook_name, value);
+ else if (!v)
+ warning(_("hook.%s.jobs must be positive, ignoring: 0"), hook_name);
+ else {
+ unsigned int *old;
+ unsigned int *p = xmalloc(sizeof(*p));
+ *p = v;
+ old = strmap_put(&data->event_jobs, hook_name, p);
+ free(old);
+ }
}
free(hook_name);
@@ -252,6 +269,7 @@ void hook_cache_clear(struct hook_config_cache *cache)
free(hooks);
}
strmap_clear(&cache->hooks, 0);
+ strmap_clear(&cache->event_jobs, 1); /* free heap-allocated unsigned int * values */
}
/* Populate `cache` with the complete hook configuration */
@@ -266,6 +284,7 @@ static void build_hook_config_map(struct repository *r,
strmap_init(&cb_data.event_hooks);
string_list_init_dup(&cb_data.disabled_hooks);
strmap_init(&cb_data.parallel_hooks);
+ strmap_init(&cb_data.event_jobs);
/* Parse all configs in one run, capturing hook.* including hook.jobs. */
repo_config(r, hook_config_lookup_all, &cb_data);
@@ -305,6 +324,7 @@ static void build_hook_config_map(struct repository *r,
}
cache->jobs = cb_data.jobs;
+ cache->event_jobs = cb_data.event_jobs;
strmap_clear(&cb_data.commands, 1);
strmap_clear(&cb_data.parallel_hooks, 0); /* values are uintptr_t, not heap ptrs */
@@ -513,6 +533,7 @@ static void run_hooks_opt_clear(struct run_hooks_opt *options)
/* Determine how many jobs to use for hook execution. */
static unsigned int get_hook_jobs(struct repository *r,
struct run_hooks_opt *options,
+ const char *hook_name,
struct string_list *hook_list)
{
unsigned int jobs;
@@ -529,22 +550,36 @@ static unsigned int get_hook_jobs(struct repository *r,
return 1;
/*
- * Resolve effective job count: -jN (when given) overrides config.
- * Default to 1 when both config an -jN are missing.
+ * Resolve effective job count: -j N (when given) overrides config.
+ * hook.<event>.jobs overrides hook.jobs.
+ * Unset configs and -jN default to 1.
*/
- if (options->jobs > 1)
+ if (options->jobs > 1) {
jobs = options->jobs;
- else if (r && r->gitdir && r->hook_config_cache)
+ } else if (r && r->gitdir && r->hook_config_cache) {
/* Use the already-parsed cache (in-repo) */
+ unsigned int *event_jobs = strmap_get(&r->hook_config_cache->event_jobs,
+ hook_name);
jobs = r->hook_config_cache->jobs ? r->hook_config_cache->jobs : 1;
- else
+ if (event_jobs)
+ jobs = *event_jobs;
+ } else {
/* No cache present (out-of-repo call), use direct cfg lookup */
+ unsigned int event_jobs;
+ char *key;
jobs = repo_config_get_uint(r, "hook.jobs", &jobs) ? 1 : jobs;
+ key = xstrfmt("hook.%s.jobs", hook_name);
+ if (!repo_config_get_uint(r, key, &event_jobs) && event_jobs)
+ jobs = event_jobs;
+ free(key);
+ }
/*
* Cap to serial any configured hook not marked as parallel = true.
* This enforces the parallel = false default, even for "traditional"
* hooks from the hookdir which cannot be marked parallel = true.
+ * The same restriction applies whether jobs came from hook.jobs or
+ * hook.<event>.jobs.
*/
for (size_t i = 0; jobs > 1 && i < hook_list->nr; i++) {
struct hook *h = hook_list->items[i].util;
@@ -566,7 +601,7 @@ int run_hooks_opt(struct repository *r, const char *hook_name,
.options = options,
};
int ret = 0;
- unsigned int jobs = get_hook_jobs(r, options, hook_list);
+ unsigned int jobs = get_hook_jobs(r, options, hook_name, hook_list);
const struct run_process_parallel_opts opts = {
.tr2_category = "hook",
.tr2_label = hook_name,
diff --git a/hook.h b/hook.h
index 1f29798a77..22fc59e67a 100644
--- a/hook.h
+++ b/hook.h
@@ -222,6 +222,7 @@ void hook_list_clear(struct string_list *hooks, cb_data_free_fn cb_data_free);
*/
struct hook_config_cache {
struct strmap hooks; /* maps event name -> string_list of hooks */
+ struct strmap event_jobs; /* maps event name -> heap-allocated unsigned int * */
unsigned int jobs; /* hook.jobs config value; 0 if unset (defaults to serial) */
};
diff --git a/t/t1800-hook.sh b/t/t1800-hook.sh
index a6913b8c62..f5c0655adb 100755
--- a/t/t1800-hook.sh
+++ b/t/t1800-hook.sh
@@ -820,4 +820,63 @@ test_expect_success 'hook.jobs=2 is ignored for force-serial hooks (pre-commit)'
test_cmp expect hook.order
'
+test_expect_success 'hook.<event>.jobs overrides hook.jobs for that event' '
+ test_when_finished "rm -f sentinel.started sentinel.done hook.order" &&
+ test_config hook.hook-1.event test-hook &&
+ test_config hook.hook-1.command \
+ "touch sentinel.started; sleep 2; touch sentinel.done" &&
+ test_config hook.hook-1.parallel true &&
+ test_config hook.hook-2.event test-hook &&
+ test_config hook.hook-2.command \
+ "$(sentinel_detector sentinel hook.order)" &&
+ test_config hook.hook-2.parallel true &&
+
+ # Global hook.jobs=1 (serial), but per-event override allows parallel.
+ test_config hook.jobs 1 &&
+ test_config hook.test-hook.jobs 2 &&
+
+ git hook run test-hook >out 2>err &&
+ echo parallel >expect &&
+ test_cmp expect hook.order
+'
+
+test_expect_success 'hook.<event>.jobs=1 forces serial even when hook.jobs>1' '
+ test_when_finished "rm -f sentinel.started sentinel.done hook.order" &&
+ test_config hook.hook-1.event test-hook &&
+ test_config hook.hook-1.command \
+ "touch sentinel.started; sleep 2; touch sentinel.done" &&
+ test_config hook.hook-1.parallel true &&
+ test_config hook.hook-2.event test-hook &&
+ test_config hook.hook-2.command \
+ "$(sentinel_detector sentinel hook.order)" &&
+ test_config hook.hook-2.parallel true &&
+
+ # Global hook.jobs=4 allows parallel, but per-event override forces serial.
+ test_config hook.jobs 4 &&
+ test_config hook.test-hook.jobs 1 &&
+
+ git hook run test-hook >out 2>err &&
+ echo serial >expect &&
+ test_cmp expect hook.order
+'
+
+test_expect_success 'hook.<event>.jobs still requires hook.<name>.parallel=true' '
+ test_when_finished "rm -f sentinel.started sentinel.done hook.order" &&
+ test_config hook.hook-1.event test-hook &&
+ test_config hook.hook-1.command \
+ "touch sentinel.started; sleep 2; touch sentinel.done" &&
+ # hook-1 intentionally has no parallel=true
+ test_config hook.hook-2.event test-hook &&
+ test_config hook.hook-2.command \
+ "$(sentinel_detector sentinel hook.order)" &&
+ # hook-2 also has no parallel=true
+
+ # Per-event jobs=2 but no hook has parallel=true: must still run serially.
+ test_config hook.test-hook.jobs 2 &&
+
+ git hook run test-hook >out 2>err &&
+ echo serial >expect &&
+ test_cmp expect hook.order
+'
+
test_done
--
2.52.0.732.gb351b5166d.dirty
next prev parent reply other threads:[~2026-02-22 0:29 UTC|newest]
Thread overview: 113+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-02-04 17:33 [PATCH 0/4] Run hooks in parallel Adrian Ratiu
2026-02-04 17:33 ` [PATCH 1/4] config: add a repo_config_get_uint() helper Adrian Ratiu
2026-02-04 17:33 ` [PATCH 2/4] hook: allow parallel hook execution Adrian Ratiu
2026-02-11 12:41 ` Patrick Steinhardt
2026-02-12 12:25 ` Adrian Ratiu
2026-02-04 17:33 ` [PATCH 3/4] hook: introduce extensions.hookStdoutToStderr Adrian Ratiu
2026-02-04 17:33 ` [PATCH 4/4] hook: allow runtime enabling extensions.hookStdoutToStderr Adrian Ratiu
2026-02-12 10:43 ` [PATCH 0/4] Run hooks in parallel Phillip Wood
2026-02-12 14:24 ` Adrian Ratiu
2026-02-13 14:39 ` Phillip Wood
2026-02-13 17:21 ` Adrian Ratiu
2026-02-22 0:28 ` [PATCH v2 00/10] " Adrian Ratiu
2026-02-22 0:28 ` [PATCH v2 01/10] repository: fix repo_init() memleak due to missing _clear() Adrian Ratiu
2026-02-22 0:28 ` [PATCH v2 02/10] config: add a repo_config_get_uint() helper Adrian Ratiu
2026-02-22 0:28 ` [PATCH v2 03/10] hook: refactor hook_config_cache from strmap to named struct Adrian Ratiu
2026-02-22 0:28 ` [PATCH v2 04/10] hook: parse the hook.jobs config Adrian Ratiu
2026-02-22 0:28 ` [PATCH v2 05/10] hook: allow parallel hook execution Adrian Ratiu
2026-02-22 0:29 ` [PATCH v2 06/10] hook: mark non-parallelizable hooks Adrian Ratiu
2026-02-22 0:29 ` [PATCH v2 07/10] hook: add -j/--jobs option to git hook run Adrian Ratiu
2026-02-22 0:29 ` Adrian Ratiu [this message]
2026-02-22 0:29 ` [PATCH v2 09/10] hook: introduce extensions.hookStdoutToStderr Adrian Ratiu
2026-02-22 0:29 ` [PATCH v2 10/10] hook: allow runtime enabling extensions.hookStdoutToStderr Adrian Ratiu
2026-03-09 13:37 ` [PATCH v3 0/9] Run hooks in parallel Adrian Ratiu
2026-03-09 13:37 ` [PATCH v3 1/9] repository: fix repo_init() memleak due to missing _clear() Adrian Ratiu
2026-03-15 4:55 ` Junio C Hamano
2026-03-15 5:05 ` Junio C Hamano
2026-03-09 13:37 ` [PATCH v3 2/9] config: add a repo_config_get_uint() helper Adrian Ratiu
2026-03-09 13:37 ` [PATCH v3 3/9] hook: parse the hook.jobs config Adrian Ratiu
2026-03-15 16:13 ` Junio C Hamano
2026-03-09 13:37 ` [PATCH v3 4/9] hook: allow parallel hook execution Adrian Ratiu
2026-03-15 20:46 ` Junio C Hamano
2026-03-18 18:02 ` Adrian Ratiu
2026-03-09 13:37 ` [PATCH v3 5/9] hook: mark non-parallelizable hooks Adrian Ratiu
2026-03-15 20:56 ` Junio C Hamano
2026-03-18 18:40 ` Adrian Ratiu
2026-03-09 13:37 ` [PATCH v3 6/9] hook: add -j/--jobs option to git hook run Adrian Ratiu
2026-03-15 21:00 ` Junio C Hamano
2026-03-18 19:00 ` Adrian Ratiu
2026-03-09 13:37 ` [PATCH v3 7/9] hook: add per-event jobs config Adrian Ratiu
2026-03-16 18:40 ` Junio C Hamano
2026-03-18 19:21 ` Adrian Ratiu
2026-03-09 13:37 ` [PATCH v3 8/9] hook: introduce extensions.hookStdoutToStderr Adrian Ratiu
2026-03-16 18:44 ` Junio C Hamano
2026-03-18 19:50 ` Adrian Ratiu
2026-03-09 13:37 ` [PATCH v3 9/9] hook: allow runtime enabling extensions.hookStdoutToStderr Adrian Ratiu
2026-03-20 13:53 ` [PATCH v4 0/9] Run hooks in parallel Adrian Ratiu
2026-03-20 13:53 ` [PATCH v4 1/9] config: add a repo_config_get_uint() helper Adrian Ratiu
2026-03-20 13:53 ` [PATCH v4 2/9] hook: parse the hook.jobs config Adrian Ratiu
2026-03-24 9:07 ` Patrick Steinhardt
2026-03-24 18:59 ` Adrian Ratiu
2026-03-20 13:53 ` [PATCH v4 3/9] hook: allow parallel hook execution Adrian Ratiu
2026-03-24 9:07 ` Patrick Steinhardt
2026-03-20 13:53 ` [PATCH v4 4/9] hook: allow pre-push parallel execution Adrian Ratiu
2026-03-20 13:53 ` [PATCH v4 5/9] hook: mark non-parallelizable hooks Adrian Ratiu
2026-03-20 13:53 ` [PATCH v4 6/9] hook: add -j/--jobs option to git hook run Adrian Ratiu
2026-03-24 9:07 ` Patrick Steinhardt
2026-03-20 13:53 ` [PATCH v4 7/9] hook: add per-event jobs config Adrian Ratiu
2026-03-24 9:08 ` Patrick Steinhardt
2026-03-20 13:53 ` [PATCH v4 8/9] hook: warn when hook.<friendly-name>.jobs is set Adrian Ratiu
2026-03-24 9:08 ` Patrick Steinhardt
2026-03-20 13:53 ` [PATCH v4 9/9] hook: add hook.<event>.enabled switch Adrian Ratiu
2026-03-24 9:08 ` Patrick Steinhardt
2026-03-25 18:43 ` Adrian Ratiu
2026-03-20 17:24 ` [PATCH v4 0/9] Run hooks in parallel Junio C Hamano
2026-03-23 15:07 ` Adrian Ratiu
2026-03-24 9:07 ` Patrick Steinhardt
2026-03-26 10:18 ` [PATCH v5 00/12] " Adrian Ratiu
2026-03-26 10:18 ` [PATCH v5 01/12] repository: fix repo_init() memleak due to missing _clear() Adrian Ratiu
2026-03-26 10:18 ` [PATCH v5 02/12] config: add a repo_config_get_uint() helper Adrian Ratiu
2026-03-26 10:18 ` [PATCH v5 03/12] hook: parse the hook.jobs config Adrian Ratiu
2026-03-26 10:18 ` [PATCH v5 04/12] hook: allow parallel hook execution Adrian Ratiu
2026-03-26 10:18 ` [PATCH v5 05/12] hook: allow pre-push parallel execution Adrian Ratiu
2026-03-26 10:18 ` [PATCH v5 06/12] hook: mark non-parallelizable hooks Adrian Ratiu
2026-03-26 10:18 ` [PATCH v5 07/12] hook: add -j/--jobs option to git hook run Adrian Ratiu
2026-03-27 14:46 ` Patrick Steinhardt
2026-03-26 10:18 ` [PATCH v5 08/12] hook: add per-event jobs config Adrian Ratiu
2026-03-26 10:18 ` [PATCH v5 09/12] hook: warn when hook.<friendly-name>.jobs is set Adrian Ratiu
2026-03-27 14:46 ` Patrick Steinhardt
2026-03-26 10:18 ` [PATCH v5 10/12] hook: move is_known_hook() to hook.c for wider use Adrian Ratiu
2026-03-27 14:46 ` Patrick Steinhardt
2026-03-27 15:59 ` Adrian Ratiu
2026-03-26 10:18 ` [PATCH v5 11/12] hook: add hook.<event>.enabled switch Adrian Ratiu
2026-03-26 10:18 ` [PATCH v5 12/12] hook: allow hook.jobs=-1 to use all available CPU cores Adrian Ratiu
2026-04-04 8:29 ` [PATCH v6 00/12] Run hooks in parallel Adrian Ratiu
2026-04-04 8:29 ` [PATCH v6 01/12] repository: fix repo_init() memleak due to missing _clear() Adrian Ratiu
2026-04-04 8:29 ` [PATCH v6 02/12] config: add a repo_config_get_uint() helper Adrian Ratiu
2026-04-04 8:29 ` [PATCH v6 03/12] hook: parse the hook.jobs config Adrian Ratiu
2026-04-04 8:29 ` [PATCH v6 04/12] hook: allow parallel hook execution Adrian Ratiu
2026-04-04 8:29 ` [PATCH v6 05/12] hook: allow pre-push parallel execution Adrian Ratiu
2026-04-04 8:29 ` [PATCH v6 06/12] hook: mark non-parallelizable hooks Adrian Ratiu
2026-04-04 8:29 ` [PATCH v6 07/12] hook: add -j/--jobs option to git hook run Adrian Ratiu
2026-04-04 8:29 ` [PATCH v6 08/12] hook: add per-event jobs config Adrian Ratiu
2026-04-04 8:29 ` [PATCH v6 09/12] hook: warn when hook.<friendly-name>.jobs is set Adrian Ratiu
2026-04-04 8:29 ` [PATCH v6 10/12] hook: move is_known_hook() to hook.c for wider use Adrian Ratiu
2026-04-04 8:29 ` [PATCH v6 11/12] hook: add hook.<event>.enabled switch Adrian Ratiu
2026-04-04 8:29 ` [PATCH v6 12/12] hook: allow hook.jobs=-1 to use all available CPU cores Adrian Ratiu
2026-04-06 16:24 ` [PATCH v6 00/12] Run hooks in parallel Junio C Hamano
2026-04-08 10:17 ` Patrick Steinhardt
2026-04-08 16:57 ` Junio C Hamano
2026-04-10 9:05 ` [PATCH v7 00/13] " Adrian Ratiu
2026-04-10 9:05 ` [PATCH v7 01/13] repository: fix repo_init() memleak due to missing _clear() Adrian Ratiu
2026-04-10 9:05 ` [PATCH v7 02/13] config: add a repo_config_get_uint() helper Adrian Ratiu
2026-04-10 9:05 ` [PATCH v7 03/13] hook: parse the hook.jobs config Adrian Ratiu
2026-04-10 9:05 ` [PATCH v7 04/13] hook: allow parallel hook execution Adrian Ratiu
2026-04-10 9:06 ` [PATCH v7 05/13] hook: allow pre-push parallel execution Adrian Ratiu
2026-04-10 9:06 ` [PATCH v7 06/13] hook: mark non-parallelizable hooks Adrian Ratiu
2026-04-10 9:06 ` [PATCH v7 07/13] hook: add -j/--jobs option to git hook run Adrian Ratiu
2026-04-10 9:06 ` [PATCH v7 08/13] hook: add per-event jobs config Adrian Ratiu
2026-04-10 9:06 ` [PATCH v7 09/13] hook: warn when hook.<friendly-name>.jobs is set Adrian Ratiu
2026-04-10 9:06 ` [PATCH v7 10/13] hook: move is_known_hook() to hook.c for wider use Adrian Ratiu
2026-04-10 9:06 ` [PATCH v7 11/13] hook: add hook.<event>.enabled switch Adrian Ratiu
2026-04-10 9:06 ` [PATCH v7 12/13] hook: allow hook.jobs=-1 to use all available CPU cores Adrian Ratiu
2026-04-10 9:06 ` [PATCH v7 13/13] t1800: test SIGPIPE with parallel hooks Adrian Ratiu
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260222002904.1879356-9-adrian.ratiu@collabora.com \
--to=adrian.ratiu@collabora.com \
--cc=emilyshaffer@google.com \
--cc=git@vger.kernel.org \
--cc=gitster@pobox.com \
--cc=kristofferhaugsbakk@fastmail.com \
--cc=peff@peff.net \
--cc=ps@pks.im \
--cc=sandals@crustytoothpaste.net \
--cc=steadmon@google.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.