From: Patrick Steinhardt <ps@pks.im>
To: git@vger.kernel.org
Cc: Derrick Stolee <stolee@gmail.com>
Subject: [PATCH v2 0/8] builtin/maintenance: implement missing tasks compared to git-gc(1)
Date: Wed, 30 Apr 2025 12:25:04 +0200 [thread overview]
Message-ID: <20250430-pks-maintenance-missing-tasks-v2-0-2580b7b8ca3a@pks.im> (raw)
In-Reply-To: <20250425-pks-maintenance-missing-tasks-v1-0-972ed6ab2c0d@pks.im>
Hi,
this small patch series implements the last couple of remaining tasks
that are missing compared to the functionality git-gc(1) provides.
Right now, git-maintenance(1) still executes git-gc(1). With these last
gaps plugged though we can in theory fully replace git-gc(1) with finer
grained tasks without losing any functionality. The benefit is that it
becomes possible for users to have finer-grained control over what
exactly the maintenance does.
This patch series doesn't do that yet, but only implements whatever is
needed to get there.
Changes in v2:
- Introduce "maintenance.worktree-prune.auto", which controls how many
stale worktrees need to exist before executing `git worktree prune`.
- Introduce "maintenance.rerere-gc.auto", which controls how many
stale rerere entries need to exist before executing `git rerere gc`.
- Add tests to verify that "gc.worktreePruneExpire" works.
- Remove some fragile test logic by introducing functions that check
for a given maintenance subprocess.
- Link to v1: https://lore.kernel.org/r/20250425-pks-maintenance-missing-tasks-v1-0-972ed6ab2c0d@pks.im
Thanks!
Patrick
---
Patrick Steinhardt (8):
builtin/gc: fix indentation of `cmd_gc()` parameters
builtin/gc: remove global variables where it trivial to do
builtin/gc: move pruning of worktrees into a separate function
worktree: expose function to retrieve worktree names
builtin/maintenance: introduce "worktree-prune" task
rerere: provide function to collect stale entries
builtin/gc: move rerere garbage collection into separate function
builtin/maintenance: introduce "rerere-gc" task
Documentation/config/maintenance.adoc | 16 ++++
Documentation/git-maintenance.adoc | 8 ++
builtin/gc.c | 153 +++++++++++++++++++++++++++-------
builtin/worktree.c | 25 +++---
rerere.c | 92 +++++++++++++-------
rerere.h | 14 ++++
t/t7900-maintenance.sh | 125 +++++++++++++++++++++++++++
worktree.c | 30 +++++++
worktree.h | 8 ++
9 files changed, 399 insertions(+), 72 deletions(-)
Range-diff versus v1:
1: 0304b81df0b = 1: 9c62b493297 builtin/gc: fix indentation of `cmd_gc()` parameters
2: 22c499601ee = 2: 9ae42b139fa builtin/gc: remove global variables where it trivial to do
3: db9622a408f = 3: 50a5305b6d2 builtin/gc: move pruning of worktrees into a separate function
4: f42205e1b6b = 4: b71dcb0debc worktree: expose function to retrieve worktree names
5: eade37df904 ! 5: 47d31f41c2e builtin/maintenance: introduce "worktree-prune" task
@@ Commit message
Signed-off-by: Patrick Steinhardt <ps@pks.im>
+ ## Documentation/config/maintenance.adoc ##
+@@ Documentation/config/maintenance.adoc: maintenance.reflog-expire.auto::
+ positive value implies the command should run when the number of
+ expired reflog entries in the "HEAD" reflog is at least the value of
+ `maintenance.loose-objects.auto`. The default value is 100.
++
++maintenance.worktree-prune.auto::
++ This integer config option controls how often the `worktree-prune` task
++ should be run as part of `git maintenance run --auto`. If zero, then
++ the `worktree-prune` task will not run with the `--auto` option. A
++ negative value will force the task to run every time. Otherwise, a
++ positive value implies the command should run when the number of
++ prunable worktrees exceeds the value. The default value is 1.
+
## Documentation/git-maintenance.adoc ##
@@ Documentation/git-maintenance.adoc: reflog-expire::
The `reflog-expire` task deletes any entries in the reflog older than the
@@ builtin/gc.c: static int maintenance_task_worktree_prune(struct maintenance_run_
+ struct strbuf reason = STRBUF_INIT;
+ timestamp_t expiry_date;
+ int should_prune = 0;
++ int limit = 1;
++
++ git_config_get_int("maintenance.worktree-prune.auto", &limit);
++ if (limit <= 0) {
++ should_prune = limit < 0;
++ goto out;
++ }
+
+ if (parse_expiry_date(cfg->prune_worktrees_expire, &expiry_date) ||
+ get_worktree_names(the_repository, &worktrees) < 0)
@@ builtin/gc.c: static int maintenance_task_worktree_prune(struct maintenance_run_
+
+ strbuf_reset(&reason);
+ if (should_prune_worktree(worktrees.v[i], &reason, &wtpath, expiry_date)) {
-+ should_prune = 1;
-+ goto out;
++ limit--;
++
++ if (!limit) {
++ should_prune = 1;
++ goto out;
++ }
+ }
+ free(wtpath);
+ }
@@ t/t7900-maintenance.sh: test_expect_success 'reflog-expire task --auto only pack
test_subcommand git reflog expire --all <reflog-expire-auto.txt
'
-+test_expect_success 'worktree-prune task' '
-+ GIT_TRACE2_EVENT="$(pwd)/worktree-prune.txt" \
-+ git maintenance run --task=worktree-prune &&
-+ test_subcommand git worktree prune --expire 3.months.ago <worktree-prune.txt
++test_expect_worktree_prune () {
++ negate=
++ if test "$1" = "!"
++ then
++ negate="!"
++ shift
++ fi
++
++ rm -f "worktree-prune.txt" &&
++ GIT_TRACE2_EVENT="$(pwd)/worktree-prune.txt" "$@" &&
++ test_subcommand $negate git worktree prune --expire 3.months.ago <worktree-prune.txt
++}
++
++test_expect_success 'worktree-prune task without --auto always prunes' '
++ test_expect_worktree_prune git maintenance run --task=worktree-prune
+'
+
+test_expect_success 'worktree-prune task --auto only prunes with prunable worktree' '
-+ GIT_TRACE2_EVENT="$(pwd)/worktree-prune-auto.txt" \
-+ git maintenance run --auto --task=worktree-prune &&
-+ test_subcommand ! git worktree prune --expire 3.months.ago <worktree-prune-auto.txt &&
++ test_expect_worktree_prune ! git maintenance run --auto --task=worktree-prune &&
+ mkdir .git/worktrees &&
+ : >.git/worktrees/abc &&
-+ GIT_TRACE2_EVENT="$(pwd)/worktree-prune-auto.txt" \
-+ git maintenance run --auto --task=worktree-prune &&
-+ test_subcommand git worktree prune --expire 3.months.ago <worktree-prune-auto.txt
++ test_expect_worktree_prune git maintenance run --auto --task=worktree-prune
++'
++
++test_expect_success 'worktree-prune task with --auto honors maintenance.worktree-prune.auto' '
++ # A negative value should always prune.
++ test_expect_worktree_prune git -c maintenance.worktree-prune.auto=-1 maintenance run --auto --task=worktree-prune &&
++
++ mkdir .git/worktrees &&
++ : >.git/worktrees/first &&
++ : >.git/worktrees/second &&
++ : >.git/worktrees/third &&
++
++ # Zero should never prune.
++ test_expect_worktree_prune ! git -c maintenance.worktree-prune.auto=0 maintenance run --auto --task=worktree-prune &&
++ # A positive value should require at least this man prunable worktrees.
++ test_expect_worktree_prune ! git -c maintenance.worktree-prune.auto=4 maintenance run --auto --task=worktree-prune &&
++ test_expect_worktree_prune git -c maintenance.worktree-prune.auto=3 maintenance run --auto --task=worktree-prune
++'
++
++test_expect_success 'worktree-prune task with --auto honors maintenance.worktree-prune.auto' '
++ # A negative value should always prune.
++ test_expect_worktree_prune git -c maintenance.worktree-prune.auto=-1 maintenance run --auto --task=worktree-prune &&
++
++ mkdir .git/worktrees &&
++ : >.git/worktrees/first &&
++ : >.git/worktrees/second &&
++ : >.git/worktrees/third &&
++
++ # Zero should never prune.
++ test_expect_worktree_prune ! git -c maintenance.worktree-prune.auto=0 maintenance run --auto --task=worktree-prune &&
++ # A positive value should require at least this many prunable worktrees.
++ test_expect_worktree_prune ! git -c maintenance.worktree-prune.auto=4 maintenance run --auto --task=worktree-prune &&
++ test_expect_worktree_prune git -c maintenance.worktree-prune.auto=3 maintenance run --auto --task=worktree-prune
++'
++
++test_expect_success 'worktree-prune task honors gc.worktreePruneExpire' '
++ git worktree add worktree &&
++ rm -rf worktree &&
++
++ rm -f worktree-prune.txt &&
++ GIT_TRACE2_EVENT="$(pwd)/worktree-prune.txt" git -c gc.worktreePruneExpire=1.week.ago maintenance run --auto --task=worktree-prune &&
++ test_subcommand ! git worktree prune --expire 1.week.ago <worktree-prune.txt &&
++ test_path_is_dir .git/worktrees/worktree &&
++
++ rm -f worktree-prune.txt &&
++ GIT_TRACE2_EVENT="$(pwd)/worktree-prune.txt" git -c gc.worktreePruneExpire=now maintenance run --auto --task=worktree-prune &&
++ test_subcommand git worktree prune --expire now <worktree-prune.txt &&
++ test_path_is_missing .git/worktrees/worktree
+'
+
test_expect_success '--auto and --schedule incompatible' '
-: ----------- > 6: 5550c115e84 rerere: provide function to collect stale entries
6: 66b2b033743 = 7: f5b234c859e builtin/gc: move rerere garbage collection into separate function
7: 9604fc4fc6b ! 8: 092e57cce01 builtin/maintenance: introduce "rerere-gc" task
@@ Commit message
Signed-off-by: Patrick Steinhardt <ps@pks.im>
+ ## Documentation/config/maintenance.adoc ##
+@@ Documentation/config/maintenance.adoc: maintenance.reflog-expire.auto::
+ expired reflog entries in the "HEAD" reflog is at least the value of
+ `maintenance.loose-objects.auto`. The default value is 100.
+
++maintenance.rerere-gc.auto::
++ This integer config option controls how often the `rerere-gc` task
++ should be run as part of `git maintenance run --auto`. If zero, then
++ the `rerere-gc` task will not run with the `--auto` option. A negative
++ value will force the task to run every time. Otherwise, a positive
++ value implies the command should run when the number of prunable rerere
++ entries exceeds the value. The default value is 20.
++
+ maintenance.worktree-prune.auto::
+ This integer config option controls how often the `worktree-prune` task
+ should be run as part of `git maintenance run --auto`. If zero, then
+
## Documentation/git-maintenance.adoc ##
@@ Documentation/git-maintenance.adoc: reflog-expire::
The `reflog-expire` task deletes any entries in the reflog older than the
@@ builtin/gc.c
#include "environment.h"
#include "hex.h"
#include "config.h"
+@@
+ #include "pack-objects.h"
+ #include "path.h"
+ #include "reflog.h"
++#include "rerere.h"
+ #include "blob.h"
+ #include "tree.h"
+ #include "promisor-remote.h"
@@ builtin/gc.c: static int maintenance_task_rerere_gc(struct maintenance_run_opts *opts UNUSED,
return run_command(&rerere_cmd);
}
@@ builtin/gc.c: static int maintenance_task_rerere_gc(struct maintenance_run_opts
+static int rerere_gc_condition(struct gc_config *cfg UNUSED)
+{
+ struct strbuf path = STRBUF_INIT;
++ struct string_list prunable_dirs = STRING_LIST_INIT_DUP;
++ struct rerere_id *prunable_entries = NULL;
++ size_t prunable_entries_nr;
+ int should_gc = 0;
-+ DIR *dir;
++ int limit = 20;
++
++ git_config_get_int("maintenance.rerere-gc.auto", &limit);
++ if (limit <= 0) {
++ should_gc = limit < 0;
++ goto out;
++ }
+
+ /* Skip garbage collecting the rerere cache in case rerere is disabled. */
+ repo_git_path_replace(the_repository, &path, "rr-cache");
++ if (!is_directory(path.buf))
++ goto out;
+
-+ dir = opendir(path.buf);
-+ if (!dir)
++ if (rerere_collect_stale_entries(the_repository, &prunable_dirs,
++ &prunable_entries, &prunable_entries_nr) < 0)
+ goto out;
-+ should_gc = !!readdir_skip_dot_and_dotdot(dir);
++
++ should_gc = prunable_entries_nr >= limit;
+
+out:
++ string_list_clear(&prunable_dirs, 0);
++ free(prunable_entries);
+ strbuf_release(&path);
-+ closedir(dir);
+ return should_gc;
+}
+
@@ builtin/gc.c: static struct maintenance_task tasks[] = {
static int compare_tasks_by_selection(const void *a_, const void *b_)
## t/t7900-maintenance.sh ##
-@@ t/t7900-maintenance.sh: test_expect_success 'worktree-prune task --auto only prunes with prunable worktr
- test_subcommand git worktree prune --expire 3.months.ago <worktree-prune-auto.txt
+@@ t/t7900-maintenance.sh: test_expect_success 'worktree-prune task honors gc.worktreePruneExpire' '
+ test_path_is_missing .git/worktrees/worktree
'
-+test_expect_success 'rerere-gc task' '
-+ GIT_TRACE2_EVENT="$(pwd)/rerere-gc.txt" \
-+ git maintenance run --task=rerere-gc &&
-+ test_subcommand git rerere gc <rerere-gc.txt
++setup_stale_rerere_entry () {
++ rr=.git/rr-cache/"$(printf "%0$(test_oid hexsz)d" "$1")" &&
++ mkdir -p "$rr" &&
++ >"$rr/preimage" &&
++ >"$rr/postimage" &&
++
++ test-tool chmtime ="$((-61 * 86400))" "$rr/preimage" &&
++ test-tool chmtime ="$((-61 * 86400))" "$rr/postimage"
++}
++
++test_expect_rerere_gc () {
++ negate=
++ if test "$1" = "!"
++ then
++ negate="!"
++ shift
++ fi
++
++ rm -f "rerere-gc.txt" &&
++ GIT_TRACE2_EVENT="$(pwd)/rerere-gc.txt" "$@" &&
++ test_subcommand $negate git rerere gc <rerere-gc.txt
++}
++
++test_expect_success 'rerere-gc task without --auto always collects garbage' '
++ test_expect_rerere_gc git maintenance run --task=rerere-gc
+'
+
-+test_expect_success 'rerere-gc task --auto only prunes with existing rr-cache dir' '
-+ mkdir .git/rr-cache &&
-+ GIT_TRACE2_EVENT="$(pwd)/rerere-gc-auto.txt" \
-+ git maintenance run --auto --task=rerere-gc &&
-+ test_subcommand ! git rerere gc <rerere-gc-auto.txt &&
-+ : >.git/rr-cache/entry &&
-+ GIT_TRACE2_EVENT="$(pwd)/rerere-gc-auto.txt" \
-+ git maintenance run --auto --task=rerere-gc &&
-+ test_subcommand git rerere gc <rerere-gc-auto.txt
++test_expect_success 'rerere-gc task with --auto only prunes with prunable entries' '
++ test_expect_rerere_gc ! git maintenance run --auto --task=rerere-gc &&
++ for i in $(test_seq 19)
++ do
++ setup_stale_rerere_entry $i || return 1
++ done &&
++ test_expect_rerere_gc ! git maintenance run --auto --task=rerere-gc &&
++ setup_stale_rerere_entry 20 &&
++ test_expect_rerere_gc git maintenance run --auto --task=rerere-gc
++'
++
++test_expect_success 'rerere-gc task with --auto honors maintenance.rerere-gc.auto' '
++ # A negative value should always prune.
++ test_expect_rerere_gc git -c maintenance.rerere-gc.auto=-1 maintenance run --auto --task=rerere-gc &&
++
++ for i in $(test_seq 20)
++ do
++ setup_stale_rerere_entry $i || return 1
++ done &&
++
++ # Zero should never prune.
++ test_expect_rerere_gc ! git -c maintenance.rerere-gc.auto=0 maintenance run --auto --task=rerere-gc &&
++ # A positive value should require at least this many stale rerere entries.
++ test_expect_rerere_gc ! git -c maintenance.rerere-gc.auto=21 maintenance run --auto --task=rerere-gc &&
++ test_expect_rerere_gc git -c maintenance.rerere-gc.auto=10 maintenance run --auto --task=rerere-gc
+'
+
test_expect_success '--auto and --schedule incompatible' '
---
base-commit: a2955b34f48265d240ab8c7deb0a929ec2d65fd0
change-id: 20250424-pks-maintenance-missing-tasks-8ffcdd596b73
next prev parent reply other threads:[~2025-04-30 10:25 UTC|newest]
Thread overview: 66+ messages / expand[flat|nested] mbox.gz Atom feed top
2025-04-25 7:29 [PATCH 0/7] builtin/maintenance: implement missing tasks compared to git-gc(1) Patrick Steinhardt
2025-04-25 7:29 ` [PATCH 1/7] builtin/gc: fix indentation of `cmd_gc()` parameters Patrick Steinhardt
2025-04-25 7:29 ` [PATCH 2/7] builtin/gc: remove global variables where it trivial to do Patrick Steinhardt
2025-04-25 7:29 ` [PATCH 3/7] builtin/gc: move pruning of worktrees into a separate function Patrick Steinhardt
2025-04-25 7:29 ` [PATCH 4/7] worktree: expose function to retrieve worktree names Patrick Steinhardt
2025-04-25 7:29 ` [PATCH 5/7] builtin/maintenance: introduce "worktree-prune" task Patrick Steinhardt
2025-04-29 20:02 ` Derrick Stolee
2025-04-30 7:08 ` Patrick Steinhardt
2025-04-25 7:29 ` [PATCH 6/7] builtin/gc: move rerere garbage collection into separate function Patrick Steinhardt
2025-04-25 7:29 ` [PATCH 7/7] builtin/maintenance: introduce "rerere-gc" task Patrick Steinhardt
2025-04-29 20:02 ` [PATCH 0/7] builtin/maintenance: implement missing tasks compared to git-gc(1) Derrick Stolee
2025-04-30 7:08 ` Patrick Steinhardt
2025-04-30 10:25 ` Patrick Steinhardt [this message]
2025-04-30 10:25 ` [PATCH v2 1/8] builtin/gc: fix indentation of `cmd_gc()` parameters Patrick Steinhardt
2025-04-30 10:25 ` [PATCH v2 2/8] builtin/gc: remove global variables where it trivial to do Patrick Steinhardt
2025-04-30 10:25 ` [PATCH v2 3/8] builtin/gc: move pruning of worktrees into a separate function Patrick Steinhardt
2025-04-30 10:25 ` [PATCH v2 4/8] worktree: expose function to retrieve worktree names Patrick Steinhardt
2025-04-30 10:25 ` [PATCH v2 5/8] builtin/maintenance: introduce "worktree-prune" task Patrick Steinhardt
2025-04-30 10:25 ` [PATCH v2 6/8] rerere: provide function to collect stale entries Patrick Steinhardt
2025-04-30 16:58 ` Junio C Hamano
2025-05-02 8:07 ` Patrick Steinhardt
2025-05-02 16:35 ` Junio C Hamano
2025-05-05 7:22 ` Patrick Steinhardt
2025-04-30 10:25 ` [PATCH v2 7/8] builtin/gc: move rerere garbage collection into separate function Patrick Steinhardt
2025-04-30 10:25 ` [PATCH v2 8/8] builtin/maintenance: introduce "rerere-gc" task Patrick Steinhardt
2025-04-30 10:37 ` [PATCH v2 0/8] builtin/maintenance: implement missing tasks compared to git-gc(1) Derrick Stolee
2025-05-02 8:43 ` [PATCH v3 0/7] " Patrick Steinhardt
2025-05-02 8:43 ` [PATCH v3 1/7] builtin/gc: fix indentation of `cmd_gc()` parameters Patrick Steinhardt
2025-05-02 8:43 ` [PATCH v3 2/7] builtin/gc: remove global variables where it trivial to do Patrick Steinhardt
2025-05-02 8:44 ` [PATCH v3 3/7] builtin/gc: move pruning of worktrees into a separate function Patrick Steinhardt
2025-05-02 8:44 ` [PATCH v3 4/7] worktree: expose function to retrieve worktree names Patrick Steinhardt
2025-05-05 8:42 ` Eric Sunshine
2025-05-07 7:06 ` Patrick Steinhardt
2025-05-02 8:44 ` [PATCH v3 5/7] builtin/maintenance: introduce "worktree-prune" task Patrick Steinhardt
2025-05-05 8:59 ` Eric Sunshine
2025-05-07 7:06 ` Patrick Steinhardt
2025-05-02 8:44 ` [PATCH v3 6/7] builtin/gc: move rerere garbage collection into separate function Patrick Steinhardt
2025-05-02 8:44 ` [PATCH v3 7/7] builtin/maintenance: introduce "rerere-gc" task Patrick Steinhardt
2025-05-02 14:57 ` [PATCH v3 0/7] builtin/maintenance: implement missing tasks compared to git-gc(1) Derrick Stolee
2025-05-02 21:07 ` Junio C Hamano
2025-05-05 7:32 ` Patrick Steinhardt
2025-05-05 8:51 ` [PATCH v4 " Patrick Steinhardt
2025-05-05 8:51 ` [PATCH v4 1/7] builtin/gc: fix indentation of `cmd_gc()` parameters Patrick Steinhardt
2025-05-05 8:51 ` [PATCH v4 2/7] builtin/gc: remove global variables where it trivial to do Patrick Steinhardt
2025-05-06 7:44 ` Christian Couder
2025-05-07 7:06 ` Patrick Steinhardt
2025-05-05 8:51 ` [PATCH v4 3/7] builtin/gc: move pruning of worktrees into a separate function Patrick Steinhardt
2025-05-06 7:50 ` Christian Couder
2025-05-07 7:06 ` Patrick Steinhardt
2025-05-05 8:51 ` [PATCH v4 4/7] worktree: expose function to retrieve worktree names Patrick Steinhardt
2025-05-06 8:20 ` Christian Couder
2025-05-06 16:08 ` Eric Sunshine
2025-05-05 8:51 ` [PATCH v4 5/7] builtin/maintenance: introduce "worktree-prune" task Patrick Steinhardt
2025-05-06 7:40 ` Christian Couder
2025-05-07 7:06 ` Patrick Steinhardt
2025-05-05 8:51 ` [PATCH v4 6/7] builtin/gc: move rerere garbage collection into separate function Patrick Steinhardt
2025-05-06 8:39 ` Christian Couder
2025-05-05 8:51 ` [PATCH v4 7/7] builtin/maintenance: introduce "rerere-gc" task Patrick Steinhardt
2025-05-06 9:05 ` [PATCH v4 0/7] builtin/maintenance: implement missing tasks compared to git-gc(1) Christian Couder
2025-05-07 7:21 ` [PATCH v5 0/6] " Patrick Steinhardt
2025-05-07 7:21 ` [PATCH v5 1/6] builtin/gc: fix indentation of `cmd_gc()` parameters Patrick Steinhardt
2025-05-07 7:21 ` [PATCH v5 2/6] builtin/gc: remove global variables where it is trivial to do Patrick Steinhardt
2025-05-07 7:21 ` [PATCH v5 3/6] builtin/gc: move pruning of worktrees into a separate function Patrick Steinhardt
2025-05-07 7:21 ` [PATCH v5 4/6] builtin/maintenance: introduce "worktree-prune" task Patrick Steinhardt
2025-05-07 7:21 ` [PATCH v5 5/6] builtin/gc: move rerere garbage collection into separate function Patrick Steinhardt
2025-05-07 7:21 ` [PATCH v5 6/6] builtin/maintenance: introduce "rerere-gc" task Patrick Steinhardt
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=20250430-pks-maintenance-missing-tasks-v2-0-2580b7b8ca3a@pks.im \
--to=ps@pks.im \
--cc=git@vger.kernel.org \
--cc=stolee@gmail.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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).