diff --git a/Documentation/git-format-patch.txt b/Documentation/git-format-patch.txt index 39118774af..e394276b1a 100644 --- a/Documentation/git-format-patch.txt +++ b/Documentation/git-format-patch.txt @@ -187,6 +187,21 @@ will want to ensure that threading is disabled for `git send-email`. The negated form `--no-cc` discards all `Cc:` headers added so far (from config or command line). +--from:: +--from=:: + Use `ident` in the `From:` header of each commit email. If the + author ident of the commit is not textually identical to the + provided `ident`, place a `From:` header in the body of the + message with the original author. If no `ident` is given, use + the committer ident. ++ +Note that this option is only useful if you are actually sending the +emails and want to identify yourself as the sender, but retain the +original author (and `git am` will correctly pick up the in-body +header). Note also that `git send-email` already handles this +transformation for you, and this option should not be used if you are +feeding the result to `git send-email`. + --add-header=
:: Add an arbitrary header to the email headers. This is in addition to any configured headers, and may be used multiple times. diff --git a/builtin/log.c b/builtin/log.c index e3222ed9f9..2625f9881a 100644 --- a/builtin/log.c +++ b/builtin/log.c @@ -1112,6 +1112,21 @@ static int cc_callback(const struct option *opt, const char *arg, int unset) return 0; } +static int from_callback(const struct option *opt, const char *arg, int unset) +{ + char **from = opt->value; + + free(*from); + + if (unset) + *from = NULL; + else if (arg) + *from = xstrdup(arg); + else + *from = xstrdup(git_committer_info(IDENT_NO_DATE)); + return 0; +} + int cmd_format_patch(int argc, const char **argv, const char *prefix) { struct commit *commit; @@ -1134,6 +1149,7 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix) int quiet = 0; int reroll_count = -1; char *branch_name = NULL; + char *from = NULL; const struct option builtin_format_patch_options[] = { { OPTION_CALLBACK, 'n', "numbered", &numbered, NULL, N_("use [PATCH n/m] even with a single patch"), @@ -1177,6 +1193,9 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix) 0, to_callback }, { OPTION_CALLBACK, 0, "cc", NULL, N_("email"), N_("add Cc: header"), 0, cc_callback }, + { OPTION_CALLBACK, 0, "from", &from, N_("ident"), + N_("set From address to (or committer ident if absent)"), + PARSE_OPT_OPTARG, from_callback }, OPT_STRING(0, "in-reply-to", &in_reply_to, N_("message-id"), N_("make first mail a reply to ")), { OPTION_CALLBACK, 0, "attach", &rev, N_("boundary"), @@ -1264,6 +1283,11 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix) rev.extra_headers = strbuf_detach(&buf, NULL); + if (from) { + if (split_ident_line(&rev.from_ident, from, strlen(from))) + die(_("invalid ident line: %s"), from); + } + if (start_number < 0) start_number = 1; diff --git a/commit.h b/commit.h index 011ddb146f..c56bc3a091 100644 --- a/commit.h +++ b/commit.h @@ -6,6 +6,7 @@ #include "strbuf.h" #include "decorate.h" #include "gpg-interface.h" +#include "string-list.h" struct commit_list { struct commit *item; @@ -95,11 +96,15 @@ struct pretty_print_context { const char *output_encoding; struct string_list *mailmap; int color; + struct ident_split *from_ident; /* * Fields below here are manipulated internally by pp_* functions and * should not be counted on by callers. */ + + /* Manipulated by the pp_* functions internally. */ + struct string_list in_body_headers; }; struct userformat_want { diff --git a/log-tree.c b/log-tree.c index 2eb69bcfed..67da27f909 100644 --- a/log-tree.c +++ b/log-tree.c @@ -617,6 +617,8 @@ void show_log(struct rev_info *opt) ctx.fmt = opt->commit_format; ctx.mailmap = opt->mailmap; ctx.color = opt->diffopt.use_color; + if (opt->from_ident.mail_begin && opt->from_ident.name_begin) + ctx.from_ident = &opt->from_ident; pretty_print_commit(&ctx, commit, &msgbuf); if (opt->add_signoff) diff --git a/pretty.c b/pretty.c index 68cd7a035b..74563c92b4 100644 --- a/pretty.c +++ b/pretty.c @@ -432,6 +432,23 @@ void pp_user_info(struct pretty_print_context *pp, map_user(pp->mailmap, &mailbuf, &maillen, &namebuf, &namelen); if (pp->fmt == CMIT_FMT_EMAIL) { + if (pp->from_ident) { + struct strbuf buf = STRBUF_INIT; + + strbuf_addstr(&buf, "From: "); + strbuf_add(&buf, namebuf, namelen); + strbuf_addstr(&buf, " <"); + strbuf_add(&buf, mailbuf, maillen); + strbuf_addstr(&buf, ">\n"); + string_list_append(&pp->in_body_headers, + strbuf_detach(&buf, NULL)); + + mailbuf = pp->from_ident->mail_begin; + maillen = pp->from_ident->mail_end - mailbuf; + namebuf = pp->from_ident->name_begin; + namelen = pp->from_ident->name_end - namebuf; + } + strbuf_addstr(sb, "From: "); if (needs_rfc2047_encoding(namebuf, namelen, RFC2047_ADDRESS)) { add_rfc2047(sb, namebuf, namelen, @@ -1602,6 +1619,16 @@ void pp_title_line(struct pretty_print_context *pp, } strbuf_addch(sb, '\n'); + if (need_8bit_cte == 0) { + int i; + for (i = 0; i < pp->in_body_headers.nr; i++) { + if (has_non_ascii(pp->in_body_headers.items[i].string)) { + need_8bit_cte = 1; + break; + } + } + } + if (need_8bit_cte > 0) { const char *header_fmt = "MIME-Version: 1.0\n" @@ -1615,6 +1642,17 @@ void pp_title_line(struct pretty_print_context *pp, if (pp->fmt == CMIT_FMT_EMAIL) { strbuf_addch(sb, '\n'); } + + if (pp->in_body_headers.nr) { + int i; + for (i = 0; i < pp->in_body_headers.nr; i++) { + strbuf_addstr(sb, pp->in_body_headers.items[i].string); + free(pp->in_body_headers.items[i].string); + } + string_list_clear(&pp->in_body_headers, 0); + strbuf_addch(sb, '\n'); + } + strbuf_release(&title); } diff --git a/revision.h b/revision.h index 92d6614af6..95859ba119 100644 --- a/revision.h +++ b/revision.h @@ -144,6 +144,7 @@ struct rev_info { int numbered_files; int reroll_count; char *message_id; + struct ident_split from_ident; struct string_list *ref_message_ids; int add_signoff; const char *extra_headers; diff --git a/t/t4014-format-patch.sh b/t/t4014-format-patch.sh index 58d418098d..668933bfb2 100755 --- a/t/t4014-format-patch.sh +++ b/t/t4014-format-patch.sh @@ -972,6 +972,49 @@ test_expect_success 'empty subject prefix does not have extra space' ' test_cmp expect actual ' +test_expect_success '--from=ident notices bogus ident' ' + test_must_fail git format-patch -1 --stdout --from=foo >patch +' + +test_expect_success '--from=ident replaces author' ' + git format-patch -1 --stdout --from="Me " >patch && + cat >expect <<-\EOF && + From: Me + + From: A U Thor + + EOF + sed -ne "/^From:/p; /^$/p; /^---$/q" patch.head && + test_cmp expect patch.head +' + +test_expect_success '--from uses committer ident' ' + git format-patch -1 --stdout --from >patch && + cat >expect <<-\EOF && + From: C O Mitter + + From: A U Thor + + EOF + sed -ne "/^From:/p; /^$/p; /^---$/q" patch.head && + test_cmp expect patch.head +' + +test_expect_success 'in-body headers trigger content encoding' ' + GIT_AUTHOR_NAME="éxötìc" test_commit exotic && + test_when_finished "git reset --hard HEAD^" && + git format-patch -1 --stdout --from >patch && + cat >expect <<-\EOF && + From: C O Mitter + Content-Type: text/plain; charset=UTF-8 + + From: éxötìc + + EOF + sed -ne "/^From:/p; /^$/p; /^Content-Type/p; /^---$/q" patch.head && + test_cmp expect patch.head +' + append_signoff() { C=$(git commit-tree HEAD^^{tree} -p HEAD) &&