format-patch: add ability to use alt cover format

Often when sending patch series there's a need to clarify to the reviewer what's the purpose of said series, since it might be difficult to understand it from reading the commits messages one by one. "git format-patch" provides the useful "--cover-letter" flag to declare if we want it to generate a template for us to use. By default it will generate a "git shortlog" of the changes, which developers find less useful than they'd like, mainly because the shortlog groups commits by author, and gives no obvious chronological order. Give format-patch the ability to specify an alternative format spec through the "--cover-letter-format" option. This option either takes "shortlog", which is the current format, or a format spec prefixed with "log:". Example: git format-patch --cover-letter \ --cover-letter-format="log:[%(count)/%(total)] %s (%an)" HEAD~3 [1/3] this is a commit summary (Mirko Faina) [2/3] this is another commit summary (Mirko Faina) ... Signed-off-by: Mirko Faina <mroik@delayed.space> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Mirko Faina committed Mar 7, 2026 at 00:34 UTC 6005932d95ff05541f9dbe8c49a45b7abaf7432e
3 files changed +86 -3
builtin/log.c
+37 -3
@@ -1343,13 +1343,36 @@ static void generate_shortlog_cover_letter(struct shortlog *log,
1343 shortlog_output(log);
1344 }
1345
1346 +static void generate_commit_list_cover(FILE *cover_file, const char *format,
1347 + struct commit **list, int n)
1348 +{
1349 + struct strbuf commit_line = STRBUF_INIT;
1350 + struct pretty_print_context ctx = {0};
1351 + struct rev_info rev = REV_INFO_INIT;
1352 +
1353 + strbuf_init(&commit_line, 0);
1354 + rev.total = n;
1355 + ctx.rev = &rev;
1356 + for (int i = n - 1; i >= 0; i--) {
1357 + rev.nr = n - i;
1358 + repo_format_commit_message(the_repository, list[i], format,
1359 + &commit_line, &ctx);
1360 + fprintf(cover_file, "%s\n", commit_line.buf);
1361 + strbuf_reset(&commit_line);
1362 + }
1363 + fprintf(cover_file, "\n");
1364 +
1365 + strbuf_release(&commit_line);
1366 +}
1367 +
1368 static void make_cover_letter(struct rev_info *rev, int use_separate_file,
1369 struct commit *origin,
1370 int nr, struct commit **list,
1371 const char *description_file,
1372 const char *branch_name,
1373 int quiet,
1352 - const struct format_config *cfg)
1374 + const struct format_config *cfg,
1375 + const char *format)
1376 {
1377 const char *committer;
1378 struct shortlog log;
@@ -1396,7 +1419,12 @@ static void make_cover_letter(struct rev_info *rev, int use_separate_file,
1419 free(pp.after_subject);
1420 strbuf_release(&sb);
1421
1399 - generate_shortlog_cover_letter(&log, rev, list, nr);
1422 + if (skip_prefix(format, "log:", &format))
1423 + generate_commit_list_cover(rev->diffopt.file, format, list, nr);
1424 + else if (!strcmp(format, "shortlog"))
1425 + generate_shortlog_cover_letter(&log, rev, list, nr);
1426 + else
1427 + die(_("'%s' is not a valid format string"), format);
1428
1429 /* We can only do diffstat with a unique reference point */
1430 if (origin)
@@ -1914,6 +1942,7 @@ int cmd_format_patch(int argc,
1942 int just_numbers = 0;
1943 int ignore_if_in_upstream = 0;
1944 int cover_letter = -1;
1945 + const char *cover_letter_fmt = NULL;
1946 int boundary_count = 0;
1947 int no_binary_diff = 0;
1948 int zero_commit = 0;
@@ -1960,6 +1989,8 @@ int cmd_format_patch(int argc,
1989 N_("print patches to standard out")),
1990 OPT_BOOL(0, "cover-letter", &cover_letter,
1991 N_("generate a cover letter")),
1992 + OPT_STRING(0, "cover-letter-format", &cover_letter_fmt, N_("format-spec"),
1993 + N_("format spec used for the commit list in the cover letter")),
1994 OPT_BOOL(0, "numbered-files", &just_numbers,
1995 N_("use simple number sequence for output file names")),
1996 OPT_STRING(0, "suffix", &fmt_patch_suffix, N_("sfx"),
@@ -2297,6 +2328,7 @@ int cmd_format_patch(int argc,
2328 /* nothing to do */
2329 goto done;
2330 total = list.nr;
2331 +
2332 if (cover_letter == -1) {
2333 if (cfg.config_cover_letter == COVER_AUTO)
2334 cover_letter = (total > 1);
@@ -2383,12 +2415,14 @@ int cmd_format_patch(int argc,
2415 }
2416 rev.numbered_files = just_numbers;
2417 rev.patch_suffix = fmt_patch_suffix;
2418 +
2419 if (cover_letter) {
2420 if (cfg.thread)
2421 gen_message_id(&rev, "cover");
2422 make_cover_letter(&rev, !!output_directory,
2423 origin, list.nr, list.items,
2391 - description_file, branch_name, quiet, &cfg);
2424 + description_file, branch_name, quiet, &cfg,
2425 + cover_letter_fmt);
2426 print_bases(&bases, rev.diffopt.file);
2427 print_signature(signature, rev.diffopt.file);
2428 total++;
t/t4014-format-patch.sh
+48
@@ -380,6 +380,54 @@ test_expect_success 'filename limit applies only to basename' '
380 done
381 '
382
383 +test_expect_success 'cover letter with subject, author and count' '
384 + rm -rf patches &&
385 + test_when_finished "git reset --hard HEAD~1" &&
386 + test_when_finished "rm -rf patches result test_file" &&
387 + touch test_file &&
388 + git add test_file &&
389 + git commit -m "This is a subject" &&
390 + git format-patch --cover-letter \
391 + --cover-letter-format="log:[%(count)/%(total)] %s (%an)" -o patches HEAD~1 &&
392 + grep "^\[1/1\] This is a subject (A U Thor)$" patches/0000-cover-letter.patch >result &&
393 + test_line_count = 1 result
394 +'
395 +
396 +test_expected_success 'cover letter with author and count' '
397 + test_when_finished "git reset --hard HEAD~1" &&
398 + test_when_finished "rm -rf patches result test_file" &&
399 + touch test_file &&
400 + git add test_file &&
401 + git commit -m "This is a subject" &&
402 + git format-patch --cover-letter \
403 + --cover-letter-format="log:[%(count)/%(total)] %an" -o patches HEAD~1 &&
404 + grep "^\[1/1\] A U Thor$" patches/0000-cover-letter.patch >result &&
405 + test_line_count = 1 result
406 +'
407 +
408 +test_expect_success 'cover letter shortlog' '
409 + test_when_finished "git reset --hard HEAD~1" &&
410 + test_when_finished "rm -rf patches result test_file" &&
411 + touch test_file &&
412 + git add test_file &&
413 + git commit -m "This is a subject" &&
414 + git format-patch --cover-letter --cover-letter-format=shortlog \
415 + -o patches HEAD~1 &&
416 + sed -n -e "/^A U Thor/p;" patches/0000-cover-letter.patch >result &&
417 + test_line_count = 1 result
418 +'
419 +
420 +test_expect_success 'cover letter no format' '
421 + test_when_finished "git reset --hard HEAD~1" &&
422 + test_when_finished "rm -rf patches result test_file" &&
423 + touch test_file &&
424 + git add test_file &&
425 + git commit -m "This is a subject" &&
426 + git format-patch --cover-letter -o patches HEAD~1 &&
427 + sed -n -e "/^A U Thor/p;" patches/0000-cover-letter.patch >result &&
428 + test_line_count = 1 result
429 +'
430 +
431 test_expect_success 'reroll count' '
432 rm -fr patches &&
433 git format-patch -o patches --cover-letter --reroll-count 4 main..side >list &&
t/t9902-completion.sh
+1
@@ -2774,6 +2774,7 @@ test_expect_success PERL 'send-email' '
2774 test_completion "git send-email --cov" <<-\EOF &&
2775 --cover-from-description=Z
2776 --cover-letter Z
2777 + --cover-letter-format=Z
2778 EOF
2779 test_completion "git send-email --val" <<-\EOF &&
2780 --validate Z