teach format-patch to place other authors into in-body "From"

Format-patch generates emails with the "From" address set to the author of each patch. If you are going to send the emails, however, you would want to replace the author identity with yours (if they are not the same), and bump the author identity to an in-body header. Normally this is handled by git-send-email, which does the transformation before sending out the emails. However, some workflows may not use send-email (e.g., imap-send, or a custom script which feeds the mbox to a non-git MUA). They could each implement this feature themselves, but getting it right is non-trivial (one must canonicalize the identities by reversing any RFC2047 encoding or RFC822 quoting of the headers, which has caused many bugs in send-email over the years). This patch takes a different approach: it teaches format-patch a "--from" option which handles the ident check and in-body header while it is writing out the email. It's much simpler to do at this level (because we haven't done any quoting yet), and any workflow based on format-patch can easily turn it on. Signed-off-by: Jeff King <peff@peff.net> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Jeff King committed Jul 3, 2013 at 03:08 UTC a90804752f6ab2b911882d47fafb6c2b78f447c3
7 files changed +128
Documentation/git-format-patch.txt
+15
@@ -187,6 +187,21 @@ will want to ensure that threading is disabled for `git send-email`.
187 The negated form `--no-cc` discards all `Cc:` headers added so
188 far (from config or command line).
189
190 +--from::
191 +--from=<ident>::
192 + Use `ident` in the `From:` header of each commit email. If the
193 + author ident of the commit is not textually identical to the
194 + provided `ident`, place a `From:` header in the body of the
195 + message with the original author. If no `ident` is given, use
196 + the committer ident.
197 ++
198 +Note that this option is only useful if you are actually sending the
199 +emails and want to identify yourself as the sender, but retain the
200 +original author (and `git am` will correctly pick up the in-body
201 +header). Note also that `git send-email` already handles this
202 +transformation for you, and this option should not be used if you are
203 +feeding the result to `git send-email`.
204 +
205 --add-header=<header>::
206 Add an arbitrary header to the email headers. This is in addition
207 to any configured headers, and may be used multiple times.
builtin/log.c
+24
@@ -1112,6 +1112,21 @@ static int cc_callback(const struct option *opt, const char *arg, int unset)
1112 return 0;
1113 }
1114
1115 +static int from_callback(const struct option *opt, const char *arg, int unset)
1116 +{
1117 + char **from = opt->value;
1118 +
1119 + free(*from);
1120 +
1121 + if (unset)
1122 + *from = NULL;
1123 + else if (arg)
1124 + *from = xstrdup(arg);
1125 + else
1126 + *from = xstrdup(git_committer_info(IDENT_NO_DATE));
1127 + return 0;
1128 +}
1129 +
1130 int cmd_format_patch(int argc, const char **argv, const char *prefix)
1131 {
1132 struct commit *commit;
@@ -1134,6 +1149,7 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)
1149 int quiet = 0;
1150 int reroll_count = -1;
1151 char *branch_name = NULL;
1152 + char *from = NULL;
1153 const struct option builtin_format_patch_options[] = {
1154 { OPTION_CALLBACK, 'n', "numbered", &numbered, NULL,
1155 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)
1193 0, to_callback },
1194 { OPTION_CALLBACK, 0, "cc", NULL, N_("email"), N_("add Cc: header"),
1195 0, cc_callback },
1196 + { OPTION_CALLBACK, 0, "from", &from, N_("ident"),
1197 + N_("set From address to <ident> (or committer ident if absent)"),
1198 + PARSE_OPT_OPTARG, from_callback },
1199 OPT_STRING(0, "in-reply-to", &in_reply_to, N_("message-id"),
1200 N_("make first mail a reply to <message-id>")),
1201 { OPTION_CALLBACK, 0, "attach", &rev, N_("boundary"),
@@ -1264,6 +1283,11 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)
1283
1284 rev.extra_headers = strbuf_detach(&buf, NULL);
1285
1286 + if (from) {
1287 + if (split_ident_line(&rev.from_ident, from, strlen(from)))
1288 + die(_("invalid ident line: %s"), from);
1289 + }
1290 +
1291 if (start_number < 0)
1292 start_number = 1;
1293
commit.h
+5
@@ -6,6 +6,7 @@
6 #include "strbuf.h"
7 #include "decorate.h"
8 #include "gpg-interface.h"
9 +#include "string-list.h"
10
11 struct commit_list {
12 struct commit *item;
@@ -95,11 +96,15 @@ struct pretty_print_context {
96 const char *output_encoding;
97 struct string_list *mailmap;
98 int color;
99 + struct ident_split *from_ident;
100
101 /*
102 * Fields below here are manipulated internally by pp_* functions and
103 * should not be counted on by callers.
104 */
105 +
106 + /* Manipulated by the pp_* functions internally. */
107 + struct string_list in_body_headers;
108 };
109
110 struct userformat_want {
log-tree.c
+2
@@ -617,6 +617,8 @@ void show_log(struct rev_info *opt)
617 ctx.fmt = opt->commit_format;
618 ctx.mailmap = opt->mailmap;
619 ctx.color = opt->diffopt.use_color;
620 + if (opt->from_ident.mail_begin && opt->from_ident.name_begin)
621 + ctx.from_ident = &opt->from_ident;
622 pretty_print_commit(&ctx, commit, &msgbuf);
623
624 if (opt->add_signoff)
pretty.c
+38
@@ -432,6 +432,23 @@ void pp_user_info(struct pretty_print_context *pp,
432 map_user(pp->mailmap, &mailbuf, &maillen, &namebuf, &namelen);
433
434 if (pp->fmt == CMIT_FMT_EMAIL) {
435 + if (pp->from_ident) {
436 + struct strbuf buf = STRBUF_INIT;
437 +
438 + strbuf_addstr(&buf, "From: ");
439 + strbuf_add(&buf, namebuf, namelen);
440 + strbuf_addstr(&buf, " <");
441 + strbuf_add(&buf, mailbuf, maillen);
442 + strbuf_addstr(&buf, ">\n");
443 + string_list_append(&pp->in_body_headers,
444 + strbuf_detach(&buf, NULL));
445 +
446 + mailbuf = pp->from_ident->mail_begin;
447 + maillen = pp->from_ident->mail_end - mailbuf;
448 + namebuf = pp->from_ident->name_begin;
449 + namelen = pp->from_ident->name_end - namebuf;
450 + }
451 +
452 strbuf_addstr(sb, "From: ");
453 if (needs_rfc2047_encoding(namebuf, namelen, RFC2047_ADDRESS)) {
454 add_rfc2047(sb, namebuf, namelen,
@@ -1602,6 +1619,16 @@ void pp_title_line(struct pretty_print_context *pp,
1619 }
1620 strbuf_addch(sb, '\n');
1621
1622 + if (need_8bit_cte == 0) {
1623 + int i;
1624 + for (i = 0; i < pp->in_body_headers.nr; i++) {
1625 + if (has_non_ascii(pp->in_body_headers.items[i].string)) {
1626 + need_8bit_cte = 1;
1627 + break;
1628 + }
1629 + }
1630 + }
1631 +
1632 if (need_8bit_cte > 0) {
1633 const char *header_fmt =
1634 "MIME-Version: 1.0\n"
@@ -1615,6 +1642,17 @@ void pp_title_line(struct pretty_print_context *pp,
1642 if (pp->fmt == CMIT_FMT_EMAIL) {
1643 strbuf_addch(sb, '\n');
1644 }
1645 +
1646 + if (pp->in_body_headers.nr) {
1647 + int i;
1648 + for (i = 0; i < pp->in_body_headers.nr; i++) {
1649 + strbuf_addstr(sb, pp->in_body_headers.items[i].string);
1650 + free(pp->in_body_headers.items[i].string);
1651 + }
1652 + string_list_clear(&pp->in_body_headers, 0);
1653 + strbuf_addch(sb, '\n');
1654 + }
1655 +
1656 strbuf_release(&title);
1657 }
1658
revision.h
+1
@@ -144,6 +144,7 @@ struct rev_info {
144 int numbered_files;
145 int reroll_count;
146 char *message_id;
147 + struct ident_split from_ident;
148 struct string_list *ref_message_ids;
149 int add_signoff;
150 const char *extra_headers;
t/t4014-format-patch.sh
+43
@@ -972,6 +972,49 @@ test_expect_success 'empty subject prefix does not have extra space' '
972 test_cmp expect actual
973 '
974
975 +test_expect_success '--from=ident notices bogus ident' '
976 + test_must_fail git format-patch -1 --stdout --from=foo >patch
977 +'
978 +
979 +test_expect_success '--from=ident replaces author' '
980 + git format-patch -1 --stdout --from="Me <me@example.com>" >patch &&
981 + cat >expect <<-\EOF &&
982 + From: Me <me@example.com>
983 +
984 + From: A U Thor <author@example.com>
985 +
986 + EOF
987 + sed -ne "/^From:/p; /^$/p; /^---$/q" <patch >patch.head &&
988 + test_cmp expect patch.head
989 +'
990 +
991 +test_expect_success '--from uses committer ident' '
992 + git format-patch -1 --stdout --from >patch &&
993 + cat >expect <<-\EOF &&
994 + From: C O Mitter <committer@example.com>
995 +
996 + From: A U Thor <author@example.com>
997 +
998 + EOF
999 + sed -ne "/^From:/p; /^$/p; /^---$/q" <patch >patch.head &&
1000 + test_cmp expect patch.head
1001 +'
1002 +
1003 +test_expect_success 'in-body headers trigger content encoding' '
1004 + GIT_AUTHOR_NAME="éxötìc" test_commit exotic &&
1005 + test_when_finished "git reset --hard HEAD^" &&
1006 + git format-patch -1 --stdout --from >patch &&
1007 + cat >expect <<-\EOF &&
1008 + From: C O Mitter <committer@example.com>
1009 + Content-Type: text/plain; charset=UTF-8
1010 +
1011 + From: éxötìc <author@example.com>
1012 +
1013 + EOF
1014 + sed -ne "/^From:/p; /^$/p; /^Content-Type/p; /^---$/q" <patch >patch.head &&
1015 + test_cmp expect patch.head
1016 +'
1017 +
1018 append_signoff()
1019 {
1020 C=$(git commit-tree HEAD^^{tree} -p HEAD) &&