format-patch: add commitListFormat config

Using "--cover-letter" we can tell format-patch to generate a cover letter, in this cover letter there's a list of commits included in the patch series and the format is specified by the "--cover-letter-format" option. Would be useful if this format could be configured from the config file instead of always needing to pass it from the command line. Teach format-patch how to read the format spec for the cover letter from the config files. The variable it should look for is called format.commitListFormat. Possible values: - commitListFormat is set but no string is passed: it will default to "[%(count)/%(total)] %s" - if a string is passed: will use it as a format spec. Note that this is either "shortlog" or a format spec prefixed by "log:" e.g."log:%s (%an)" - if commitListFormat is not set: it will default to the shortlog format. 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 be0ef6fcd2379ea3dc1569d6d8c360d6d59d031f
2 files changed +74
builtin/log.c
+21
@@ -886,6 +886,7 @@ struct format_config {
886 char *signature;
887 char *signature_file;
888 enum cover_setting config_cover_letter;
889 + char *fmt_cover_letter_commit_list;
890 char *config_output_directory;
891 enum cover_from_description cover_from_description_mode;
892 int show_notes;
@@ -930,6 +931,7 @@ static void format_config_release(struct format_config *cfg)
931 string_list_clear(&cfg->extra_cc, 0);
932 strbuf_release(&cfg->sprefix);
933 free(cfg->fmt_patch_suffix);
934 + free(cfg->fmt_cover_letter_commit_list);
935 }
936
937 static enum cover_from_description parse_cover_from_description(const char *arg)
@@ -1052,6 +1054,19 @@ static int git_format_config(const char *var, const char *value,
1054 cfg->config_cover_letter = git_config_bool(var, value) ? COVER_ON : COVER_OFF;
1055 return 0;
1056 }
1057 + if (!strcmp(var, "format.commitlistformat")) {
1058 + struct strbuf tmp = STRBUF_INIT;
1059 + strbuf_init(&tmp, 0);
1060 + if (value)
1061 + strbuf_addstr(&tmp, value);
1062 + else
1063 + strbuf_addstr(&tmp, "log:[%(count)/%(total)] %s");
1064 +
1065 + FREE_AND_NULL(cfg->fmt_cover_letter_commit_list);
1066 + git_config_string(&cfg->fmt_cover_letter_commit_list, var, tmp.buf);
1067 + strbuf_release(&tmp);
1068 + return 0;
1069 + }
1070 if (!strcmp(var, "format.outputdirectory")) {
1071 FREE_AND_NULL(cfg->config_output_directory);
1072 return git_config_string(&cfg->config_output_directory, var, value);
@@ -2329,6 +2344,12 @@ int cmd_format_patch(int argc,
2344 goto done;
2345 total = list.nr;
2346
2347 + if (!cover_letter_fmt) {
2348 + cover_letter_fmt = cfg.fmt_cover_letter_commit_list;
2349 + if (!cover_letter_fmt)
2350 + cover_letter_fmt = "shortlog";
2351 + }
2352 +
2353 if (cover_letter == -1) {
2354 if (cfg.config_cover_letter == COVER_AUTO)
2355 cover_letter = (total > 1);
t/t4014-format-patch.sh
+53
@@ -428,6 +428,59 @@ test_expect_success 'cover letter no format' '
428 test_line_count = 1 result
429 '
430
431 +test_expect_success 'cover letter config with count, subject and author' '
432 + test_when_finished "rm -rf patches result" &&
433 + test_when_finished "git config unset format.coverletter" &&
434 + test_when_finished "git config unset format.commitlistformat" &&
435 + git config set format.coverletter true &&
436 + git config set format.commitlistformat "log:[%(count)/%(total)] %s (%an)" &&
437 + git format-patch -o patches HEAD~2 &&
438 + grep -E "^[[[:digit:]]+/[[:digit:]]+] .* \(A U Thor\)" patches/0000-cover-letter.patch >result &&
439 + test_line_count = 2 result
440 +'
441 +
442 +test_expect_success 'cover letter config with count and author' '
443 + test_when_finished "rm -rf patches result" &&
444 + test_when_finished "git config unset format.coverletter" &&
445 + test_when_finished "git config unset format.commitlistformat" &&
446 + git config set format.coverletter true &&
447 + git config set format.commitlistformat "log:[%(count)/%(total)] (%an)" &&
448 + git format-patch -o patches HEAD~2 &&
449 + grep -E "^[[[:digit:]]+/[[:digit:]]+] \(A U Thor\)" patches/0000-cover-letter.patch >result &&
450 + test_line_count = 2 result
451 +'
452 +
453 +test_expect_success 'cover letter config commitlistformat set but no format' '
454 + test_when_finished "rm -rf patches result" &&
455 + test_when_finished "git config unset format.coverletter" &&
456 + test_when_finished "git config unset format.commitlistformat" &&
457 + git config set format.coverletter true &&
458 + printf "\tcommitlistformat" >> .git/config &&
459 + git format-patch -o patches HEAD~2 &&
460 + grep -E "^[[[:digit:]]+/[[:digit:]]+] .*" patches/0000-cover-letter.patch >result &&
461 + test_line_count = 2 result
462 +'
463 +
464 +test_expect_success 'cover letter config commitlistformat set to shortlog' '
465 + test_when_finished "rm -rf patches result" &&
466 + test_when_finished "git config unset format.coverletter" &&
467 + test_when_finished "git config unset format.commitlistformat" &&
468 + git config set format.coverletter true &&
469 + git config set format.commitlistformat shortlog &&
470 + git format-patch -o patches HEAD~2 &&
471 + grep -E "^A U Thor \([[:digit:]]+\)" patches/0000-cover-letter.patch >result &&
472 + test_line_count = 1 result
473 +'
474 +
475 +test_expect_success 'cover letter config commitlistformat not set' '
476 + test_when_finished "rm -rf patches result" &&
477 + test_when_finished "git config unset format.coverletter" &&
478 + git config set format.coverletter true &&
479 + git format-patch -o patches HEAD~2 &&
480 + grep -E "^A U Thor \([[:digit:]]+\)" patches/0000-cover-letter.patch >result &&
481 + test_line_count = 1 result
482 +'
483 +
484 test_expect_success 'reroll count' '
485 rm -fr patches &&
486 git format-patch -o patches --cover-letter --reroll-count 4 main..side >list &&