diff options
| author | Junio C Hamano <gitster@pobox.com> | 2026-03-26 14:03:57 -0700 |
|---|---|---|
| committer | Junio C Hamano <gitster@pobox.com> | 2026-03-26 14:03:57 -0700 |
| commit | f54477a8053716221ccbdfbbcb9cef4e9d26fe28 (patch) | |
| tree | 098ab742e268c4bf60b2a572f247b65667cdecff | |
| parent | 67006b9db8b772423ad0706029286096307d2567 (diff) | |
| parent | 36c16a5b7fff7806b475b5fa07eca3a5179d7fa6 (diff) | |
| download | git-f54477a8053716221ccbdfbbcb9cef4e9d26fe28.tar.xz | |
Merge branch 'mf/format-patch-commit-list-format' into mf/format-patch-commit-list-format-doc
* mf/format-patch-commit-list-format:
format-patch: --commit-list-format without prefix
format-patch: add preset for --commit-list-format
format-patch: wrap generate_commit_list_cover()
format.commitListFormat: strip meaning from empty
docs/pretty-formats: add %(count) and %(total)
format-patch: rename --cover-letter-format option
format-patch: refactor generate_commit_list_cover
pretty.c: better die message %(count) and %(total)
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
| -rw-r--r-- | Documentation/config/format.adoc | 5 | ||||
| -rw-r--r-- | Documentation/git-format-patch.adoc | 14 | ||||
| -rw-r--r-- | Documentation/pretty-formats.adoc | 4 | ||||
| -rw-r--r-- | builtin/log.c | 94 | ||||
| -rw-r--r-- | pretty.c | 15 | ||||
| -rwxr-xr-x | t/t4014-format-patch.sh | 125 |
6 files changed, 243 insertions, 14 deletions
diff --git a/Documentation/config/format.adoc b/Documentation/config/format.adoc index ab0710e86a..ef1ed1d250 100644 --- a/Documentation/config/format.adoc +++ b/Documentation/config/format.adoc @@ -101,6 +101,11 @@ format.coverLetter:: generate a cover-letter only when there's more than one patch. Default is false. +format.commitListFormat:: + When the `--cover-letter-format` option is not given, `format-patch` + uses the value of this variable to decide how to format the title of + each commit. Defaults to `shortlog`. + format.outputDirectory:: Set a custom directory to store the resulting files instead of the current working directory. All directory components will be created. diff --git a/Documentation/git-format-patch.adoc b/Documentation/git-format-patch.adoc index 36146006fa..c52dbcc170 100644 --- a/Documentation/git-format-patch.adoc +++ b/Documentation/git-format-patch.adoc @@ -24,6 +24,7 @@ SYNOPSIS [(--reroll-count|-v) <n>] [--to=<email>] [--cc=<email>] [--[no-]cover-letter] [--quiet] + [--commit-list-format=<format-spec>] [--[no-]encode-email-headers] [--no-notes | --notes[=<ref>]] [--interdiff=<previous>] @@ -322,6 +323,18 @@ feeding the result to `git send-email`. containing the branch description, shortlog and the overall diffstat. You can fill in a description in the file before sending it out. +--commit-list-format=<format-spec>:: + Specify the format in which to generate the commit list of the patch + series. The accepted values for format-spec are `shortlog`, `modern` or a + format-string prefixed with `log:`. + e.g. `log: %s (%an)` + The user is allowed to drop the prefix if the format-string contains a + `%<placeholder>`. + If not given, defaults to the `format.commitListFormat` configuration + variable. + This option implies the use of `--cover-letter` unless + `--no-cover-letter` is given. + --encode-email-headers:: --no-encode-email-headers:: Encode email headers that have non-ASCII characters with @@ -453,6 +466,7 @@ with configuration variables. signOff = true outputDirectory = <directory> coverLetter = auto + commitListFormat = shortlog coverFromDescription = auto ------------ diff --git a/Documentation/pretty-formats.adoc b/Documentation/pretty-formats.adoc index 5405e57a60..2ae0eb11a9 100644 --- a/Documentation/pretty-formats.adoc +++ b/Documentation/pretty-formats.adoc @@ -253,6 +253,10 @@ The placeholders are: linkgit:git-rev-list[1]) +%d+:: ref names, like the --decorate option of linkgit:git-log[1] +%D+:: ref names without the " (", ")" wrapping. ++%(count)+:: the number of a patch within a patch series. Used only in + `--commit-list-format` in `format-patch` ++%(total)+:: the total number of patches in a patch series. Used only in + `--commit-list-format` in `format-patch` ++%(decorate++`[:<option>,...]`++)++:: ref names with custom decorations. The `decorate` string may be followed by a colon and zero or more comma-separated options. Option values may contain diff --git a/builtin/log.c b/builtin/log.c index 89e8b8f011..ad7b7215fe 100644 --- a/builtin/log.c +++ b/builtin/log.c @@ -40,6 +40,7 @@ #include "progress.h" #include "commit-slab.h" #include "advice.h" +#include "utf8.h" #include "commit-reach.h" #include "range-diff.h" @@ -886,6 +887,7 @@ struct format_config { char *signature; char *signature_file; enum cover_setting config_cover_letter; + char *fmt_cover_letter_commit_list; char *config_output_directory; enum cover_from_description cover_from_description_mode; int show_notes; @@ -930,6 +932,7 @@ static void format_config_release(struct format_config *cfg) string_list_clear(&cfg->extra_cc, 0); strbuf_release(&cfg->sprefix); free(cfg->fmt_patch_suffix); + free(cfg->fmt_cover_letter_commit_list); } static enum cover_from_description parse_cover_from_description(const char *arg) @@ -1052,6 +1055,10 @@ static int git_format_config(const char *var, const char *value, cfg->config_cover_letter = git_config_bool(var, value) ? COVER_ON : COVER_OFF; return 0; } + if (!strcmp(var, "format.commitlistformat")) { + FREE_AND_NULL(cfg->fmt_cover_letter_commit_list); + return git_config_string(&cfg->fmt_cover_letter_commit_list, var, value); + } if (!strcmp(var, "format.outputdirectory")) { FREE_AND_NULL(cfg->config_output_directory); return git_config_string(&cfg->config_output_directory, var, value); @@ -1335,13 +1342,59 @@ static void get_notes_args(struct strvec *arg, struct rev_info *rev) } } +static void generate_shortlog_cover_letter(struct shortlog *log, + struct rev_info *rev, + struct commit **list, + int nr) +{ + 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 (int i = 0; i < nr; i++) + shortlog_add_commit(log, list[i]); + + shortlog_output(log); +} + +static void generate_commit_list_cover(FILE *cover_file, const char *format, + struct commit **list, int n) +{ + struct strbuf commit_line = STRBUF_INIT; + struct strbuf wrapped_line = STRBUF_INIT; + struct pretty_print_context ctx = {0}; + struct rev_info rev = REV_INFO_INIT; + + rev.total = n; + ctx.rev = &rev; + for (int i = 1; i <= n; i++) { + rev.nr = i; + repo_format_commit_message(the_repository, list[n - i], format, + &commit_line, &ctx); + strbuf_add_wrapped_text(&wrapped_line, commit_line.buf, 0, 0, + MAIL_DEFAULT_WRAP); + fprintf(cover_file, "%s\n", wrapped_line.buf); + strbuf_reset(&commit_line); + strbuf_reset(&wrapped_line); + } + fprintf(cover_file, "\n"); + + strbuf_release(&commit_line); + strbuf_release(&wrapped_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; @@ -1388,18 +1441,17 @@ 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]); - - shortlog_output(&log); + if (skip_prefix(format, "log:", &format)) + generate_commit_list_cover(rev->diffopt.file, format, list, nr); + else if (!strcmp(format, "shortlog")) + generate_shortlog_cover_letter(&log, rev, list, nr); + else if (!strcmp(format, "modern")) + generate_commit_list_cover(rev->diffopt.file, "[%(count)/%(total)] %s", + list, nr); + else if (strchr(format, '%')) + generate_commit_list_cover(rev->diffopt.file, format, list, nr); + else + die(_("'%s' is not a valid format string"), format); /* We can only do diffstat with a unique reference point */ if (origin) @@ -1917,6 +1969,7 @@ int cmd_format_patch(int argc, int just_numbers = 0; int ignore_if_in_upstream = 0; int cover_letter = -1; + const char *cover_letter_fmt = NULL; int boundary_count = 0; int no_binary_diff = 0; int zero_commit = 0; @@ -1963,6 +2016,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, "commit-list-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"), @@ -2300,6 +2355,15 @@ int cmd_format_patch(int argc, /* nothing to do */ goto done; total = list.nr; + + if (!cover_letter_fmt) { + cover_letter_fmt = cfg.fmt_cover_letter_commit_list; + if (!cover_letter_fmt) + cover_letter_fmt = "shortlog"; + } else if (cover_letter == -1) { + cover_letter = 1; + } + if (cover_letter == -1) { if (cfg.config_cover_letter == COVER_AUTO) cover_letter = (total > 1); @@ -2386,12 +2450,14 @@ int cmd_format_patch(int argc, } rev.numbered_files = just_numbers; rev.patch_suffix = fmt_patch_suffix; + 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++; @@ -1549,6 +1549,21 @@ static size_t format_commit_one(struct strbuf *sb, /* in UTF-8 */ if (!commit->object.parsed) parse_object(the_repository, &commit->object.oid); + if (starts_with(placeholder, "(count)")) { + if (!c->pretty_ctx->rev) + die(_("%s is not supported by this command"), "%(count)"); + strbuf_addf(sb, "%0*d", decimal_width(c->pretty_ctx->rev->total), + c->pretty_ctx->rev->nr); + return 7; + } + + if (starts_with(placeholder, "(total)")) { + if (!c->pretty_ctx->rev) + die(_("%s is not supported by this command"), "%(total)"); + strbuf_addf(sb, "%d", c->pretty_ctx->rev->total); + return 7; + } + switch (placeholder[0]) { case 'H': /* commit hash */ strbuf_addstr(sb, diff_get_color(c->auto_color, DIFF_COMMIT)); diff --git a/t/t4014-format-patch.sh b/t/t4014-format-patch.sh index bcdb944017..7517094bd6 100755 --- a/t/t4014-format-patch.sh +++ b/t/t4014-format-patch.sh @@ -380,6 +380,131 @@ test_expect_success 'filename limit applies only to basename' ' done ' +test_expect_success 'cover letter with subject, author and count' ' + rm -rf patches && + test_when_finished "git reset --hard HEAD~1" && + test_when_finished "rm -rf patches test_file" && + touch test_file && + git add test_file && + git commit -m "This is a subject" && + git format-patch --commit-list-format="log:[%(count)/%(total)] %s (%an)" \ + -o patches HEAD~1 && + test_grep "^\[1/1\] This is a subject (A U Thor)$" patches/0000-cover-letter.patch +' + +test_expect_success 'cover letter with custom format no prefix' ' + rm -rf patches && + test_when_finished "git reset --hard HEAD~1" && + test_when_finished "rm -rf patches test_file" && + touch test_file && + git add test_file && + git commit -m "This is a subject" && + git format-patch --commit-list-format="[%(count)/%(total)] %s (%an)" \ + -o patches HEAD~1 && + test_grep "^\[1/1\] This is a subject (A U Thor)$" patches/0000-cover-letter.patch +' + +test_expect_success 'cover letter fail when no prefix and no placeholder' ' + rm -rf patches && + test_when_finished "git reset --hard HEAD~1" && + test_when_finished "rm -rf patches test_file err" && + touch test_file && + git add test_file && + git commit -m "This is a subject" && + test_must_fail git format-patch --commit-list-format="this should fail" \ + -o patches HEAD~1 2>err && + test_grep "is not a valid format string" err +' + +test_expect_success 'cover letter modern format' ' + test_when_finished "git reset --hard HEAD~1" && + test_when_finished "rm -rf patches test_file" && + touch test_file && + git add test_file && + git commit -m "This is a subject" && + git format-patch --commit-list-format="modern" -o patches HEAD~1 && + test_grep "^\[1/1\] This is a subject$" patches/0000-cover-letter.patch +' + +test_expect_success 'cover letter shortlog format' ' + test_when_finished "git reset --hard HEAD~1" && + test_when_finished "rm -rf expect patches result test_file" && + cat >expect <<-"EOF" && + A U Thor (1): + This is a subject + EOF + touch test_file && + git add test_file && + git commit -m "This is a subject" && + git format-patch --commit-list-format=shortlog -o patches HEAD~1 && + grep -E -A 1 "^A U Thor \([[:digit:]]+\):$" patches/0000-cover-letter.patch >result && + cat result && + test_cmp expect result +' + +test_expect_success 'no cover letter but with format specified' ' + test_when_finished "git reset --hard HEAD~1" && + test_when_finished "rm -rf patches result test_file" && + touch test_file && + git add test_file && + git commit -m "This is a subject" && + git format-patch --no-cover-letter --commit-list-format="[%(count)] %s" -o patches HEAD~1 && + test_path_is_missing patches/0000-cover-letter.patch +' + +test_expect_success 'cover letter config with count, subject and author' ' + test_when_finished "rm -rf patches result" && + test_when_finished "git config unset format.coverletter" && + test_when_finished "git config unset format.commitlistformat" && + git config set format.coverletter true && + git config set format.commitlistformat "log:[%(count)/%(total)] %s (%an)" && + git format-patch -o patches HEAD~2 && + grep -E "^[[[:digit:]]+/[[:digit:]]+] .* \(A U Thor\)" patches/0000-cover-letter.patch >result && + test_line_count = 2 result +' + +test_expect_success 'cover letter config with count and author' ' + test_when_finished "rm -rf patches result" && + test_when_finished "git config unset format.coverletter" && + test_when_finished "git config unset format.commitlistformat" && + git config set format.coverletter true && + git config set format.commitlistformat "log:[%(count)/%(total)] (%an)" && + git format-patch -o patches HEAD~2 && + grep -E "^[[[:digit:]]+/[[:digit:]]+] \(A U Thor\)" patches/0000-cover-letter.patch >result && + test_line_count = 2 result +' + +test_expect_success 'cover letter config commitlistformat set to modern' ' + test_when_finished "rm -rf patches result" && + test_when_finished "git config unset format.coverletter" && + test_when_finished "git config unset format.commitlistformat" && + git config set format.coverletter true && + git config set format.commitlistformat modern && + git format-patch -o patches HEAD~2 && + grep -E "^[[[:digit:]]+/[[:digit:]]+] .*$" patches/0000-cover-letter.patch >result && + test_line_count = 2 result +' + +test_expect_success 'cover letter config commitlistformat set to shortlog' ' + test_when_finished "rm -rf patches result" && + test_when_finished "git config unset format.coverletter" && + test_when_finished "git config unset format.commitlistformat" && + git config set format.coverletter true && + git config set format.commitlistformat shortlog && + git format-patch -o patches HEAD~2 && + grep -E "^A U Thor \([[:digit:]]+\)" patches/0000-cover-letter.patch >result && + test_line_count = 1 result +' + +test_expect_success 'cover letter config commitlistformat not set' ' + test_when_finished "rm -rf patches result" && + test_when_finished "git config unset format.coverletter" && + git config set format.coverletter true && + git format-patch -o patches HEAD~2 && + grep -E "^A U Thor \([[:digit:]]+\)" patches/0000-cover-letter.patch >result && + test_line_count = 1 result +' + test_expect_success 'reroll count' ' rm -fr patches && git format-patch -o patches --cover-letter --reroll-count 4 main..side >list && |
