Skip to content

Commit b3c222e

Browse files
committed
Merge branch 'mf/format-patch-cover-letter-format' into mf/format-patch-commit-list-format
* mf/format-patch-cover-letter-format: docs: add usage for the cover-letter fmt feature format-patch: add commitListFormat config format-patch: add ability to use alt cover format format-patch: move cover letter summary generation pretty.c: add %(count) and %(total) placeholders
2 parents 67006b9 + 51ed9f7 commit b3c222e

6 files changed

Lines changed: 212 additions & 14 deletions

File tree

Documentation/config/format.adoc

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,11 @@ format.coverLetter::
101101
generate a cover-letter only when there's more than one patch.
102102
Default is false.
103103

104+
format.commitListFormat::
105+
When the `--cover-letter-format` option is not given, `format-patch`
106+
uses the value of this variable to decide how to format the title of
107+
each commit. Default to `shortlog`.
108+
104109
format.outputDirectory::
105110
Set a custom directory to store the resulting files instead of the
106111
current working directory. All directory components will be created.

Documentation/git-format-patch.adoc

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ SYNOPSIS
2424
[(--reroll-count|-v) <n>]
2525
[--to=<email>] [--cc=<email>]
2626
[--[no-]cover-letter] [--quiet]
27+
[--cover-letter-format=<format-spec>]
2728
[--[no-]encode-email-headers]
2829
[--no-notes | --notes[=<ref>]]
2930
[--interdiff=<previous>]
@@ -322,6 +323,17 @@ feeding the result to `git send-email`.
322323
containing the branch description, shortlog and the overall diffstat. You can
323324
fill in a description in the file before sending it out.
324325
326+
--cover-letter-format=<format-spec>::
327+
Specify the format in which to generate the commit list of the
328+
patch series. This option is available if the user wants to use
329+
an alternative to the default `shortlog` format. The accepted
330+
values for format-spec are "shortlog" or a format string
331+
prefixed with `log:`.
332+
e.g. `log: %s (%an)`
333+
If defined, defaults to the `format.commitListFormat` configuration
334+
variable.
335+
This option is relevant only if a cover letter is generated.
336+
325337
--encode-email-headers::
326338
--no-encode-email-headers::
327339
Encode email headers that have non-ASCII characters with
@@ -453,6 +465,7 @@ with configuration variables.
453465
signOff = true
454466
outputDirectory = <directory>
455467
coverLetter = auto
468+
commitListFormat = shortlog
456469
coverFromDescription = auto
457470
------------
458471

builtin/log.c

Lines changed: 77 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -886,6 +886,7 @@ struct format_config {
886886
char *signature;
887887
char *signature_file;
888888
enum cover_setting config_cover_letter;
889+
char *fmt_cover_letter_commit_list;
889890
char *config_output_directory;
890891
enum cover_from_description cover_from_description_mode;
891892
int show_notes;
@@ -930,6 +931,7 @@ static void format_config_release(struct format_config *cfg)
930931
string_list_clear(&cfg->extra_cc, 0);
931932
strbuf_release(&cfg->sprefix);
932933
free(cfg->fmt_patch_suffix);
934+
free(cfg->fmt_cover_letter_commit_list);
933935
}
934936

935937
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,
10521054
cfg->config_cover_letter = git_config_bool(var, value) ? COVER_ON : COVER_OFF;
10531055
return 0;
10541056
}
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+
}
10551070
if (!strcmp(var, "format.outputdirectory")) {
10561071
FREE_AND_NULL(cfg->config_output_directory);
10571072
return git_config_string(&cfg->config_output_directory, var, value);
@@ -1335,13 +1350,55 @@ static void get_notes_args(struct strvec *arg, struct rev_info *rev)
13351350
}
13361351
}
13371352

1353+
static void generate_shortlog_cover_letter(struct shortlog *log,
1354+
struct rev_info *rev,
1355+
struct commit **list,
1356+
int nr)
1357+
{
1358+
shortlog_init(log);
1359+
log->wrap_lines = 1;
1360+
log->wrap = MAIL_DEFAULT_WRAP;
1361+
log->in1 = 2;
1362+
log->in2 = 4;
1363+
log->file = rev->diffopt.file;
1364+
log->groups = SHORTLOG_GROUP_AUTHOR;
1365+
shortlog_finish_setup(log);
1366+
for (int i = 0; i < nr; i++)
1367+
shortlog_add_commit(log, list[i]);
1368+
1369+
shortlog_output(log);
1370+
}
1371+
1372+
static void generate_commit_list_cover(FILE *cover_file, const char *format,
1373+
struct commit **list, int n)
1374+
{
1375+
struct strbuf commit_line = STRBUF_INIT;
1376+
struct pretty_print_context ctx = {0};
1377+
struct rev_info rev = REV_INFO_INIT;
1378+
1379+
strbuf_init(&commit_line, 0);
1380+
rev.total = n;
1381+
ctx.rev = &rev;
1382+
for (int i = n - 1; i >= 0; i--) {
1383+
rev.nr = n - i;
1384+
repo_format_commit_message(the_repository, list[i], format,
1385+
&commit_line, &ctx);
1386+
fprintf(cover_file, "%s\n", commit_line.buf);
1387+
strbuf_reset(&commit_line);
1388+
}
1389+
fprintf(cover_file, "\n");
1390+
1391+
strbuf_release(&commit_line);
1392+
}
1393+
13381394
static void make_cover_letter(struct rev_info *rev, int use_separate_file,
13391395
struct commit *origin,
13401396
int nr, struct commit **list,
13411397
const char *description_file,
13421398
const char *branch_name,
13431399
int quiet,
1344-
const struct format_config *cfg)
1400+
const struct format_config *cfg,
1401+
const char *format)
13451402
{
13461403
const char *from;
13471404
struct shortlog log;
@@ -1388,18 +1445,12 @@ static void make_cover_letter(struct rev_info *rev, int use_separate_file,
13881445
free(pp.after_subject);
13891446
strbuf_release(&sb);
13901447

1391-
shortlog_init(&log);
1392-
log.wrap_lines = 1;
1393-
log.wrap = MAIL_DEFAULT_WRAP;
1394-
log.in1 = 2;
1395-
log.in2 = 4;
1396-
log.file = rev->diffopt.file;
1397-
log.groups = SHORTLOG_GROUP_AUTHOR;
1398-
shortlog_finish_setup(&log);
1399-
for (i = 0; i < nr; i++)
1400-
shortlog_add_commit(&log, list[i]);
1401-
1402-
shortlog_output(&log);
1448+
if (skip_prefix(format, "log:", &format))
1449+
generate_commit_list_cover(rev->diffopt.file, format, list, nr);
1450+
else if (!strcmp(format, "shortlog"))
1451+
generate_shortlog_cover_letter(&log, rev, list, nr);
1452+
else
1453+
die(_("'%s' is not a valid format string"), format);
14031454

14041455
/* We can only do diffstat with a unique reference point */
14051456
if (origin)
@@ -1917,6 +1968,7 @@ int cmd_format_patch(int argc,
19171968
int just_numbers = 0;
19181969
int ignore_if_in_upstream = 0;
19191970
int cover_letter = -1;
1971+
const char *cover_letter_fmt = NULL;
19201972
int boundary_count = 0;
19211973
int no_binary_diff = 0;
19221974
int zero_commit = 0;
@@ -1963,6 +2015,8 @@ int cmd_format_patch(int argc,
19632015
N_("print patches to standard out")),
19642016
OPT_BOOL(0, "cover-letter", &cover_letter,
19652017
N_("generate a cover letter")),
2018+
OPT_STRING(0, "cover-letter-format", &cover_letter_fmt, N_("format-spec"),
2019+
N_("format spec used for the commit list in the cover letter")),
19662020
OPT_BOOL(0, "numbered-files", &just_numbers,
19672021
N_("use simple number sequence for output file names")),
19682022
OPT_STRING(0, "suffix", &fmt_patch_suffix, N_("sfx"),
@@ -2300,6 +2354,13 @@ int cmd_format_patch(int argc,
23002354
/* nothing to do */
23012355
goto done;
23022356
total = list.nr;
2357+
2358+
if (!cover_letter_fmt) {
2359+
cover_letter_fmt = cfg.fmt_cover_letter_commit_list;
2360+
if (!cover_letter_fmt)
2361+
cover_letter_fmt = "shortlog";
2362+
}
2363+
23032364
if (cover_letter == -1) {
23042365
if (cfg.config_cover_letter == COVER_AUTO)
23052366
cover_letter = (total > 1);
@@ -2386,12 +2447,14 @@ int cmd_format_patch(int argc,
23862447
}
23872448
rev.numbered_files = just_numbers;
23882449
rev.patch_suffix = fmt_patch_suffix;
2450+
23892451
if (cover_letter) {
23902452
if (cfg.thread)
23912453
gen_message_id(&rev, "cover");
23922454
make_cover_letter(&rev, !!output_directory,
23932455
origin, list.nr, list.items,
2394-
description_file, branch_name, quiet, &cfg);
2456+
description_file, branch_name, quiet, &cfg,
2457+
cover_letter_fmt);
23952458
print_bases(&bases, rev.diffopt.file);
23962459
print_signature(signature, rev.diffopt.file);
23972460
total++;

pretty.c

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1549,6 +1549,21 @@ static size_t format_commit_one(struct strbuf *sb, /* in UTF-8 */
15491549
if (!commit->object.parsed)
15501550
parse_object(the_repository, &commit->object.oid);
15511551

1552+
if (starts_with(placeholder, "(count)")) {
1553+
if (!c->pretty_ctx->rev)
1554+
die(_("this format specifier can't be used with this command"));
1555+
strbuf_addf(sb, "%0*d", decimal_width(c->pretty_ctx->rev->total),
1556+
c->pretty_ctx->rev->nr);
1557+
return 7;
1558+
}
1559+
1560+
if (starts_with(placeholder, "(total)")) {
1561+
if (!c->pretty_ctx->rev)
1562+
die(_("this format specifier can't be used with this command"));
1563+
strbuf_addf(sb, "%d", c->pretty_ctx->rev->total);
1564+
return 7;
1565+
}
1566+
15521567
switch (placeholder[0]) {
15531568
case 'H': /* commit hash */
15541569
strbuf_addstr(sb, diff_get_color(c->auto_color, DIFF_COMMIT));

t/t4014-format-patch.sh

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,107 @@ test_expect_success 'filename limit applies only to basename' '
380380
done
381381
'
382382

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 '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+
383484
test_expect_success 'reroll count' '
384485
rm -fr patches &&
385486
git format-patch -o patches --cover-letter --reroll-count 4 main..side >list &&

t/t9902-completion.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2775,6 +2775,7 @@ test_expect_success PERL 'send-email' '
27752775
test_completion "git send-email --cov" <<-\EOF &&
27762776
--cover-from-description=Z
27772777
--cover-letter Z
2778+
--cover-letter-format=Z
27782779
EOF
27792780
test_completion "git send-email --val" <<-\EOF &&
27802781
--validate Z

0 commit comments

Comments
 (0)