Subject: [PATCH v2 1/2] format-patch: add ability to use alt cover format

From: Mirko Faina <[email-masked]>
Date: Tue, 24 Feb 2026 10:29:01 +0100

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 the ability to format-patch 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:%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 <[email-masked]>
---
 builtin/log.c | 65 ++++++++++++++++++++++++++++++++++++++-------------
 1 file changed, 49 insertions(+), 16 deletions(-)

diff --git a/builtin/log.c b/builtin/log.c
index c1cd3999a7..5e99660d7c 100644
--- a/builtin/log.c
+++ b/builtin/log.c
@@ -1324,13 +1324,32 @@ static void get_notes_args(struct strvec *arg, struct rev_info *rev)
 	}
 }
 
+static void generate_commit_list_cover(FILE *cover_file,const char *format,
+				       struct commit **list, int n)
+{
+	struct strbuf commit_line = STRBUF_INIT;
+	struct pretty_print_context ctx = {0};
+
+	strbuf_init(&commit_line, 0);
+	for (int i = n - 1; i >= 0; i--) {
+		strbuf_addf(&commit_line, "[%0*d/%d] ", decimal_width(n), n - i, n);
+		repo_format_commit_message(the_repository, list[i], format, &commit_line, &ctx);
+		fprintf(cover_file, "%s\n", commit_line.buf);
+		strbuf_reset(&commit_line);
+	}
+	fprintf(cover_file, "\n");
+
+	strbuf_release(&commit_line);
+}
+
 static void make_cover_letter(struct rev_info *rev, int use_separate_file,
 			      struct commit *origin,
 			      int nr, struct commit **list,
 			      const char *description_file,
 			      const char *branch_name,
 			      int quiet,
-			      const struct format_config *cfg)
+			      const struct format_config *cfg,
+			      const char *format)
 {
 	const char *from;
 	struct shortlog log;
@@ -1342,6 +1361,8 @@ static void make_cover_letter(struct rev_info *rev, int use_separate_file,
 	struct commit *head = list[0];
 	char *to_free = NULL;
 
+	assert(format);
+
 	if (!cmit_fmt_is_mail(rev->commit_format))
 		die(_("cover letter needs email format"));
 
@@ -1377,18 +1398,22 @@ static void make_cover_letter(struct rev_info *rev, int use_separate_file,
 	free(pp.after_subject);
 	strbuf_release(&sb);
 
-	shortlog_init(&log);
-	log.wrap_lines = 1;
-	log.wrap = MAIL_DEFAULT_WRAP;
-	log.in1 = 2;
-	log.in2 = 4;
-	log.file = rev->diffopt.file;
-	log.groups = SHORTLOG_GROUP_AUTHOR;
-	shortlog_finish_setup(&log);
-	for (i = 0; i < nr; i++)
-		shortlog_add_commit(&log, list[i]);
+	if (skip_prefix(format, "log:", &format)) {
+		generate_commit_list_cover(rev->diffopt.file, format, list, nr);
+	} else {
+		shortlog_init(&log);
+		log.wrap_lines = 1;
+		log.wrap = MAIL_DEFAULT_WRAP;
+		log.in1 = 2;
+		log.in2 = 4;
+		log.file = rev->diffopt.file;
+		log.groups = SHORTLOG_GROUP_AUTHOR;
+		shortlog_finish_setup(&log);
+		for (i = 0; i < nr; i++)
+			shortlog_add_commit(&log, list[i]);
 
-	shortlog_output(&log);
+		shortlog_output(&log);
+	}
 
 	/* We can only do diffstat with a unique reference point */
 	if (origin)
@@ -1906,6 +1931,7 @@ int cmd_format_patch(int argc,
 	int just_numbers = 0;
 	int ignore_if_in_upstream = 0;
 	int cover_letter = -1;
+	char *cover_letter_fmt = NULL;
 	int boundary_count = 0;
 	int no_binary_diff = 0;
 	int zero_commit = 0;
@@ -1952,6 +1978,8 @@ int cmd_format_patch(int argc,
 			    N_("print patches to standard out")),
 		OPT_BOOL(0, "cover-letter", &cover_letter,
 			    N_("generate a cover letter")),
+		OPT_STRING(0, "cover-letter-format", &cover_letter_fmt, N_("format-spec"),
+			    N_("format spec used for the commit list in the cover letter")),
 		OPT_BOOL(0, "numbered-files", &just_numbers,
 			    N_("use simple number sequence for output file names")),
 		OPT_STRING(0, "suffix", &fmt_patch_suffix, N_("sfx"),
@@ -2289,13 +2317,14 @@ int cmd_format_patch(int argc,
 		/* nothing to do */
 		goto done;
 	total = list.nr;
+
 	if (cover_letter == -1) {
 		if (cfg.config_cover_letter == COVER_AUTO)
-			cover_letter = (total > 1);
+			cover_letter = total > 1;
 		else if ((idiff_prev.nr || rdiff_prev) && (total > 1))
-			cover_letter = (cfg.config_cover_letter != COVER_OFF);
+			cover_letter = cfg.config_cover_letter != COVER_OFF;
 		else
-			cover_letter = (cfg.config_cover_letter == COVER_ON);
+			cover_letter = cfg.config_cover_letter == COVER_ON;
 	}
 	if (!cfg.keep_subject && cfg.auto_number && (total > 1 || cover_letter))
 		cfg.numbered = 1;
@@ -2375,12 +2404,16 @@ int cmd_format_patch(int argc,
 	}
 	rev.numbered_files = just_numbers;
 	rev.patch_suffix = fmt_patch_suffix;
+
+	if (cover_letter && !cover_letter_fmt)
+		cover_letter_fmt = "shortlog";
+
 	if (cover_letter) {
 		if (cfg.thread)
 			gen_message_id(&rev, "cover");
 		make_cover_letter(&rev, !!output_directory,
 				  origin, list.nr, list.items,
-				  description_file, branch_name, quiet, &cfg);
+				  description_file, branch_name, quiet, &cfg, cover_letter_fmt);
 		print_bases(&bases, rev.diffopt.file);
 		print_signature(signature, rev.diffopt.file);
 		total++;
-- 
2.53.0.3.g6a0c7aecfd