pretty: support "mboxrd" output format

This output format prevents format-patch output from breaking readers if somebody copy+pasted an mbox into a commit message. Unlike the traditional "mboxo" format, "mboxrd" is designed to be fully-reversible. "mboxrd" also gracefully degrades to showing extra ">" in existing "mboxo" readers. This degradation is preferable to breaking message splitting completely, a problem I've seen in "mboxcl" due to having multiple, non-existent, or inaccurate Content-Length headers. "mboxcl2" is a non-starter since it's inherits the problems of "mboxcl" while being completely incompatible with existing tooling based around mailsplit. ref: http://homepage.ntlworld.com/jonathan.deboynepollard/FGA/mail-mbox-formats.html Signed-off-by: Eric Wong <e@80x24.org> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Eric Wong committed Jun 5, 2016 at 04:46 UTC 9f23e040615857e3909db51a5420f338c9831b5a
5 files changed +75 -11
builtin/log.c
+1 -1
@@ -953,7 +953,7 @@ static void make_cover_letter(struct rev_info *rev, int use_stdout,
953 struct pretty_print_context pp = {0};
954 struct commit *head = list[0];
955
956 - if (rev->commit_format != CMIT_FMT_EMAIL)
956 + if (!cmit_fmt_is_mail(rev->commit_format))
957 die(_("Cover letter needs email format"));
958
959 committer = git_committer_info(0);
commit.h
+6
@@ -131,11 +131,17 @@ enum cmit_fmt {
131 CMIT_FMT_FULLER,
132 CMIT_FMT_ONELINE,
133 CMIT_FMT_EMAIL,
134 + CMIT_FMT_MBOXRD,
135 CMIT_FMT_USERFORMAT,
136
137 CMIT_FMT_UNSPECIFIED
138 };
139
140 +static inline int cmit_fmt_is_mail(enum cmit_fmt fmt)
141 +{
142 + return (fmt == CMIT_FMT_EMAIL || fmt == CMIT_FMT_MBOXRD);
143 +}
144 +
145 struct pretty_print_context {
146 /*
147 * Callers should tweak these to change the behavior of pp_* functions.
log-tree.c
+2 -2
@@ -603,7 +603,7 @@ void show_log(struct rev_info *opt)
603 * Print header line of header..
604 */
605
606 - if (opt->commit_format == CMIT_FMT_EMAIL) {
606 + if (cmit_fmt_is_mail(opt->commit_format)) {
607 log_write_email_headers(opt, commit, &ctx.subject, &extra_headers,
608 &ctx.need_8bit_cte);
609 } else if (opt->commit_format != CMIT_FMT_USERFORMAT) {
@@ -694,7 +694,7 @@ void show_log(struct rev_info *opt)
694
695 if ((ctx.fmt != CMIT_FMT_USERFORMAT) &&
696 ctx.notes_message && *ctx.notes_message) {
697 - if (ctx.fmt == CMIT_FMT_EMAIL) {
697 + if (cmit_fmt_is_mail(ctx.fmt)) {
698 strbuf_addstr(&msgbuf, "---\n");
699 opt->shown_dashes = 1;
700 }
pretty.c
+25 -8
@@ -92,6 +92,7 @@ static void setup_commit_formats(void)
92 { "medium", CMIT_FMT_MEDIUM, 0, 8 },
93 { "short", CMIT_FMT_SHORT, 0, 0 },
94 { "email", CMIT_FMT_EMAIL, 0, 0 },
95 + { "mboxrd", CMIT_FMT_MBOXRD, 0, 0 },
96 { "fuller", CMIT_FMT_FULLER, 0, 8 },
97 { "full", CMIT_FMT_FULL, 0, 8 },
98 { "oneline", CMIT_FMT_ONELINE, 1, 0 }
@@ -444,7 +445,7 @@ void pp_user_info(struct pretty_print_context *pp,
445 if (pp->mailmap)
446 map_user(pp->mailmap, &mailbuf, &maillen, &namebuf, &namelen);
447
447 - if (pp->fmt == CMIT_FMT_EMAIL) {
448 + if (cmit_fmt_is_mail(pp->fmt)) {
449 if (pp->from_ident && ident_cmp(pp->from_ident, &ident)) {
450 struct strbuf buf = STRBUF_INIT;
451
@@ -494,6 +495,7 @@ void pp_user_info(struct pretty_print_context *pp,
495 show_ident_date(&ident, &pp->date_mode));
496 break;
497 case CMIT_FMT_EMAIL:
498 + case CMIT_FMT_MBOXRD:
499 strbuf_addf(sb, "Date: %s\n",
500 show_ident_date(&ident, DATE_MODE(RFC2822)));
501 break;
@@ -535,7 +537,7 @@ static void add_merge_info(const struct pretty_print_context *pp,
537 {
538 struct commit_list *parent = commit->parents;
539
538 - if ((pp->fmt == CMIT_FMT_ONELINE) || (pp->fmt == CMIT_FMT_EMAIL) ||
540 + if ((pp->fmt == CMIT_FMT_ONELINE) || (cmit_fmt_is_mail(pp->fmt)) ||
541 !parent || !parent->next)
542 return;
543
@@ -1614,7 +1616,7 @@ void pp_title_line(struct pretty_print_context *pp,
1616 if (pp->after_subject) {
1617 strbuf_addstr(sb, pp->after_subject);
1618 }
1617 - if (pp->fmt == CMIT_FMT_EMAIL) {
1619 + if (cmit_fmt_is_mail(pp->fmt)) {
1620 strbuf_addch(sb, '\n');
1621 }
1622
@@ -1697,6 +1699,16 @@ static void pp_handle_indent(struct pretty_print_context *pp,
1699 strbuf_add(sb, line, linelen);
1700 }
1701
1702 +static int is_mboxrd_from(const char *line, int len)
1703 +{
1704 + /*
1705 + * a line matching /^From $/ here would only have len == 4
1706 + * at this point because is_empty_line would've trimmed all
1707 + * trailing space
1708 + */
1709 + return len > 4 && starts_with(line + strspn(line, ">"), "From ");
1710 +}
1711 +
1712 void pp_remainder(struct pretty_print_context *pp,
1713 const char **msg_p,
1714 struct strbuf *sb,
@@ -1725,8 +1737,13 @@ void pp_remainder(struct pretty_print_context *pp,
1737 else if (pp->expand_tabs_in_log)
1738 strbuf_add_tabexpand(sb, pp->expand_tabs_in_log,
1739 line, linelen);
1728 - else
1740 + else {
1741 + if (pp->fmt == CMIT_FMT_MBOXRD &&
1742 + is_mboxrd_from(line, linelen))
1743 + strbuf_addch(sb, '>');
1744 +
1745 strbuf_add(sb, line, linelen);
1746 + }
1747 strbuf_addch(sb, '\n');
1748 }
1749 }
@@ -1750,14 +1767,14 @@ void pretty_print_commit(struct pretty_print_context *pp,
1767 encoding = get_log_output_encoding();
1768 msg = reencoded = logmsg_reencode(commit, NULL, encoding);
1769
1753 - if (pp->fmt == CMIT_FMT_ONELINE || pp->fmt == CMIT_FMT_EMAIL)
1770 + if (pp->fmt == CMIT_FMT_ONELINE || cmit_fmt_is_mail(pp->fmt))
1771 indent = 0;
1772
1773 /*
1774 * We need to check and emit Content-type: to mark it
1775 * as 8-bit if we haven't done so.
1776 */
1760 - if (pp->fmt == CMIT_FMT_EMAIL && need_8bit_cte == 0) {
1777 + if (cmit_fmt_is_mail(pp->fmt) && need_8bit_cte == 0) {
1778 int i, ch, in_body;
1779
1780 for (in_body = i = 0; (ch = msg[i]); i++) {
@@ -1785,7 +1802,7 @@ void pretty_print_commit(struct pretty_print_context *pp,
1802 msg = skip_empty_lines(msg);
1803
1804 /* These formats treat the title line specially. */
1788 - if (pp->fmt == CMIT_FMT_ONELINE || pp->fmt == CMIT_FMT_EMAIL)
1805 + if (pp->fmt == CMIT_FMT_ONELINE || cmit_fmt_is_mail(pp->fmt))
1806 pp_title_line(pp, &msg, sb, encoding, need_8bit_cte);
1807
1808 beginning_of_body = sb->len;
@@ -1802,7 +1819,7 @@ void pretty_print_commit(struct pretty_print_context *pp,
1819 * format. Make sure we did not strip the blank line
1820 * between the header and the body.
1821 */
1805 - if (pp->fmt == CMIT_FMT_EMAIL && sb->len <= beginning_of_body)
1822 + if (cmit_fmt_is_mail(pp->fmt) && sb->len <= beginning_of_body)
1823 strbuf_addch(sb, '\n');
1824
1825 unuse_commit_buffer(commit, reencoded);
t/t4014-format-patch.sh
+41
@@ -1565,4 +1565,45 @@ test_expect_success 'format-patch --base overrides format.useAutoBase' '
1565 test_cmp expected actual
1566 '
1567
1568 +test_expect_success 'format-patch --pretty=mboxrd' '
1569 + sp=" " &&
1570 + cat >msg <<-INPUT_END &&
1571 + mboxrd should escape the body
1572 +
1573 + From could trip up a loose mbox parser
1574 + >From extra escape for reversibility
1575 + >>From extra escape for reversibility 2
1576 + from lower case not escaped
1577 + Fromm bad speling not escaped
1578 + From with leading space not escaped
1579 +
1580 + F
1581 + From
1582 + From$sp
1583 + From $sp
1584 + From $sp
1585 + INPUT_END
1586 +
1587 + cat >expect <<-INPUT_END &&
1588 + >From could trip up a loose mbox parser
1589 + >>From extra escape for reversibility
1590 + >>>From extra escape for reversibility 2
1591 + from lower case not escaped
1592 + Fromm bad speling not escaped
1593 + From with leading space not escaped
1594 +
1595 + F
1596 + From
1597 + From
1598 + From
1599 + From
1600 + INPUT_END
1601 +
1602 + C=$(git commit-tree HEAD^^{tree} -p HEAD <msg) &&
1603 + git format-patch --pretty=mboxrd --stdout -1 $C~1..$C >patch &&
1604 + git grep -h --no-index -A11 \
1605 + "^>From could trip up a loose mbox parser" patch >actual &&
1606 + test_cmp expect actual
1607 +'
1608 +
1609 test_done