Merge branch 'ks/rfc2047-one-char-at-a-time'
authorJunio C Hamano <gitster@pobox.com>
Mon, 25 Mar 2013 21:00:46 +0000 (14:00 -0700)
committerJunio C Hamano <gitster@pobox.com>
Mon, 25 Mar 2013 21:00:46 +0000 (14:00 -0700)
When "format-patch" quoted a non-ascii strings on the header files,
it incorrectly applied rfc2047 and chopped a single character in
the middle of it.

* ks/rfc2047-one-char-at-a-time:
format-patch: RFC 2047 says multi-octet character may not be split

1  2 
pretty.c
t/t4014-format-patch.sh
utf8.c
utf8.h
diff --combined pretty.c
index b57adefdd296f7cdf410bdf87dd48e192d31353e,cccee87026253454e76d10e86d3f5609e187c8e0..41f04e669d3b9c38f49e0853053a6ba27b97357a
+++ b/pretty.c
@@@ -345,7 -345,7 +345,7 @@@ static int needs_rfc2047_encoding(cons
        return 0;
  }
  
- static void add_rfc2047(struct strbuf *sb, const char *line, int len,
+ static void add_rfc2047(struct strbuf *sb, const char *line, size_t len,
                       const char *encoding, enum rfc2047_type type)
  {
        static const int max_encoded_length = 76; /* per rfc2047 */
        strbuf_grow(sb, len * 3 + strlen(encoding) + 100);
        strbuf_addf(sb, "=?%s?q?", encoding);
        line_len += strlen(encoding) + 5; /* 5 for =??q? */
-       for (i = 0; i < len; i++) {
-               unsigned ch = line[i] & 0xFF;
-               int is_special = is_rfc2047_special(ch, type);
+       while (len) {
+               /*
+                * RFC 2047, section 5 (3):
+                *
+                * Each 'encoded-word' MUST represent an integral number of
+                * characters.  A multi-octet character may not be split across
+                * adjacent 'encoded- word's.
+                */
+               const unsigned char *p = (const unsigned char *)line;
+               int chrlen = mbs_chrlen(&line, &len, encoding);
+               int is_special = (chrlen > 1) || is_rfc2047_special(*p, type);
+               /* "=%02X" * chrlen, or the byte itself */
+               const char *encoded_fmt = is_special ? "=%02X"    : "%c";
+               int         encoded_len = is_special ? 3 * chrlen : 1;
  
                /*
                 * According to RFC 2047, we could encode the special character
                 * causes ' ' to be encoded as '=20', avoiding this problem.
                 */
  
-               if (line_len + 2 + (is_special ? 3 : 1) > max_encoded_length) {
+               if (line_len + encoded_len + 2 > max_encoded_length) {
+                       /* It won't fit with trailing "?=" --- break the line */
                        strbuf_addf(sb, "?=\n =?%s?q?", encoding);
                        line_len = strlen(encoding) + 5 + 1; /* =??q? plus SP */
                }
  
-               if (is_special) {
-                       strbuf_addf(sb, "=%02X", ch);
-                       line_len += 3;
-               } else {
-                       strbuf_addch(sb, ch);
-                       line_len++;
-               }
+               for (i = 0; i < chrlen; i++)
+                       strbuf_addf(sb, encoded_fmt, p[i]);
+               line_len += encoded_len;
        }
        strbuf_addstr(sb, "?=");
  }
@@@ -387,79 -397,56 +397,79 @@@ void pp_user_info(const struct pretty_p
                  const char *what, struct strbuf *sb,
                  const char *line, const char *encoding)
  {
 +      struct strbuf name;
 +      struct strbuf mail;
 +      struct ident_split ident;
 +      int linelen;
 +      char *line_end, *date;
 +      const char *mailbuf, *namebuf;
 +      size_t namelen, maillen;
        int max_length = 78; /* per rfc2822 */
 -      char *date;
 -      int namelen;
        unsigned long time;
        int tz;
  
        if (pp->fmt == CMIT_FMT_ONELINE)
                return;
 -      date = strchr(line, '>');
 -      if (!date)
 +
 +      line_end = strchr(line, '\n');
 +      if (!line_end) {
 +              line_end = strchr(line, '\0');
 +              if (!line_end)
 +                      return;
 +      }
 +
 +      linelen = ++line_end - line;
 +      if (split_ident_line(&ident, line, linelen))
                return;
 -      namelen = ++date - line;
 -      time = strtoul(date, &date, 10);
 +
 +
 +      mailbuf = ident.mail_begin;
 +      maillen = ident.mail_end - ident.mail_begin;
 +      namebuf = ident.name_begin;
 +      namelen = ident.name_end - ident.name_begin;
 +
 +      if (pp->mailmap)
 +              map_user(pp->mailmap, &mailbuf, &maillen, &namebuf, &namelen);
 +
 +      strbuf_init(&mail, 0);
 +      strbuf_init(&name, 0);
 +
 +      strbuf_add(&mail, mailbuf, maillen);
 +      strbuf_add(&name, namebuf, namelen);
 +
 +      namelen = name.len + mail.len + 3; /* ' ' + '<' + '>' */
 +      time = strtoul(ident.date_begin, &date, 10);
        tz = strtol(date, NULL, 10);
  
        if (pp->fmt == CMIT_FMT_EMAIL) {
 -              char *name_tail = strchr(line, '<');
 -              int display_name_length;
 -              if (!name_tail)
 -                      return;
 -              while (line < name_tail && isspace(name_tail[-1]))
 -                      name_tail--;
 -              display_name_length = name_tail - line;
                strbuf_addstr(sb, "From: ");
 -              if (needs_rfc2047_encoding(line, display_name_length, RFC2047_ADDRESS)) {
 -                      add_rfc2047(sb, line, display_name_length,
 -                                              encoding, RFC2047_ADDRESS);
 +              if (needs_rfc2047_encoding(name.buf, name.len, RFC2047_ADDRESS)) {
 +                      add_rfc2047(sb, name.buf, name.len,
 +                                  encoding, RFC2047_ADDRESS);
                        max_length = 76; /* per rfc2047 */
 -              } else if (needs_rfc822_quoting(line, display_name_length)) {
 +              } else if (needs_rfc822_quoting(name.buf, name.len)) {
                        struct strbuf quoted = STRBUF_INIT;
 -                      add_rfc822_quoted(&quoted, line, display_name_length);
 +                      add_rfc822_quoted(&quoted, name.buf, name.len);
                        strbuf_add_wrapped_bytes(sb, quoted.buf, quoted.len,
                                                        -6, 1, max_length);
                        strbuf_release(&quoted);
                } else {
 -                      strbuf_add_wrapped_bytes(sb, line, display_name_length,
 -                                                      -6, 1, max_length);
 +                      strbuf_add_wrapped_bytes(sb, name.buf, name.len,
 +                                               -6, 1, max_length);
                }
 -              if (namelen - display_name_length + last_line_length(sb) > max_length) {
 +              if (namelen - name.len + last_line_length(sb) > max_length)
                        strbuf_addch(sb, '\n');
 -                      if (!isspace(name_tail[0]))
 -                              strbuf_addch(sb, ' ');
 -              }
 -              strbuf_add(sb, name_tail, namelen - display_name_length);
 -              strbuf_addch(sb, '\n');
 +
 +              strbuf_addf(sb, " <%s>\n", mail.buf);
        } else {
 -              strbuf_addf(sb, "%s: %.*s%.*s\n", what,
 +              strbuf_addf(sb, "%s: %.*s%s <%s>\n", what,
                              (pp->fmt == CMIT_FMT_FULLER) ? 4 : 0,
 -                            "    ", namelen, line);
 +                            "    ", name.buf, mail.buf);
        }
 +
 +      strbuf_release(&mail);
 +      strbuf_release(&name);
 +
        switch (pp->fmt) {
        case CMIT_FMT_MEDIUM:
                strbuf_addf(sb, "Date:   %s\n", show_date(time, tz, pp->date_mode));
@@@ -524,11 -511,10 +534,11 @@@ static void add_merge_info(const struc
        strbuf_addch(sb, '\n');
  }
  
 -static char *get_header(const struct commit *commit, const char *key)
 +static char *get_header(const struct commit *commit, const char *msg,
 +                      const char *key)
  {
        int key_len = strlen(key);
 -      const char *line = commit->buffer;
 +      const char *line = msg;
  
        while (line) {
                const char *eol = strchr(line, '\n'), *next;
@@@ -589,81 -575,28 +599,81 @@@ char *logmsg_reencode(const struct comm
        static const char *utf8 = "UTF-8";
        const char *use_encoding;
        char *encoding;
 +      char *msg = commit->buffer;
        char *out;
  
 -      if (!*output_encoding)
 -              return NULL;
 -      encoding = get_header(commit, "encoding");
 +      if (!msg) {
 +              enum object_type type;
 +              unsigned long size;
 +
 +              msg = read_sha1_file(commit->object.sha1, &type, &size);
 +              if (!msg)
 +                      die("Cannot read commit object %s",
 +                          sha1_to_hex(commit->object.sha1));
 +              if (type != OBJ_COMMIT)
 +                      die("Expected commit for '%s', got %s",
 +                          sha1_to_hex(commit->object.sha1), typename(type));
 +      }
 +
 +      if (!output_encoding || !*output_encoding)
 +              return msg;
 +      encoding = get_header(commit, msg, "encoding");
        use_encoding = encoding ? encoding : utf8;
 -      if (same_encoding(use_encoding, output_encoding))
 -              if (encoding) /* we'll strip encoding header later */
 -                      out = xstrdup(commit->buffer);
 -              else
 -                      return NULL; /* nothing to do */
 -      else
 -              out = reencode_string(commit->buffer,
 -                                    output_encoding, use_encoding);
 +      if (same_encoding(use_encoding, output_encoding)) {
 +              /*
 +               * No encoding work to be done. If we have no encoding header
 +               * at all, then there's nothing to do, and we can return the
 +               * message verbatim (whether newly allocated or not).
 +               */
 +              if (!encoding)
 +                      return msg;
 +
 +              /*
 +               * Otherwise, we still want to munge the encoding header in the
 +               * result, which will be done by modifying the buffer. If we
 +               * are using a fresh copy, we can reuse it. But if we are using
 +               * the cached copy from commit->buffer, we need to duplicate it
 +               * to avoid munging commit->buffer.
 +               */
 +              out = msg;
 +              if (out == commit->buffer)
 +                      out = xstrdup(out);
 +      }
 +      else {
 +              /*
 +               * There's actual encoding work to do. Do the reencoding, which
 +               * still leaves the header to be replaced in the next step. At
 +               * this point, we are done with msg. If we allocated a fresh
 +               * copy, we can free it.
 +               */
 +              out = reencode_string(msg, output_encoding, use_encoding);
 +              if (out && msg != commit->buffer)
 +                      free(msg);
 +      }
 +
 +      /*
 +       * This replacement actually consumes the buffer we hand it, so we do
 +       * not have to worry about freeing the old "out" here.
 +       */
        if (out)
                out = replace_encoding_header(out, output_encoding);
  
        free(encoding);
 -      return out;
 +      /*
 +       * If the re-encoding failed, out might be NULL here; in that
 +       * case we just return the commit message verbatim.
 +       */
 +      return out ? out : msg;
 +}
 +
 +void logmsg_free(char *msg, const struct commit *commit)
 +{
 +      if (msg != commit->buffer)
 +              free(msg);
  }
  
 -static int mailmap_name(char *email, int email_len, char *name, int name_len)
 +static int mailmap_name(const char **email, size_t *email_len,
 +                      const char **name, size_t *name_len)
  {
        static struct string_list *mail_map;
        if (!mail_map) {
@@@ -680,26 -613,36 +690,26 @@@ static size_t format_person_part(struc
        const int placeholder_len = 2;
        int tz;
        unsigned long date = 0;
 -      char person_name[1024];
 -      char person_mail[1024];
        struct ident_split s;
 -      const char *name_start, *name_end, *mail_start, *mail_end;
 +      const char *name, *mail;
 +      size_t maillen, namelen;
  
        if (split_ident_line(&s, msg, len) < 0)
                goto skip;
  
 -      name_start = s.name_begin;
 -      name_end = s.name_end;
 -      mail_start = s.mail_begin;
 -      mail_end = s.mail_end;
 -
 -      if (part == 'N' || part == 'E') { /* mailmap lookup */
 -              snprintf(person_name, sizeof(person_name), "%.*s",
 -                       (int)(name_end - name_start), name_start);
 -              snprintf(person_mail, sizeof(person_mail), "%.*s",
 -                       (int)(mail_end - mail_start), mail_start);
 -              mailmap_name(person_mail, sizeof(person_mail), person_name, sizeof(person_name));
 -              name_start = person_name;
 -              name_end = name_start + strlen(person_name);
 -              mail_start = person_mail;
 -              mail_end = mail_start +  strlen(person_mail);
 -      }
 +      name = s.name_begin;
 +      namelen = s.name_end - s.name_begin;
 +      mail = s.mail_begin;
 +      maillen = s.mail_end - s.mail_begin;
 +
 +      if (part == 'N' || part == 'E') /* mailmap lookup */
 +              mailmap_name(&mail, &maillen, &name, &namelen);
        if (part == 'n' || part == 'N') {       /* name */
 -              strbuf_add(sb, name_start, name_end-name_start);
 +              strbuf_add(sb, name, namelen);
                return placeholder_len;
        }
        if (part == 'e' || part == 'E') {       /* email */
 -              strbuf_add(sb, mail_start, mail_end-mail_start);
 +              strbuf_add(sb, mail, maillen);
                return placeholder_len;
        }
  
@@@ -759,10 -702,8 +769,10 @@@ struct format_commit_context 
        unsigned commit_signature_parsed:1;
        struct {
                char *gpg_output;
 +              char *gpg_status;
                char good_bad;
                char *signer;
 +              char *key;
        } signature;
        char *message;
        size_t width, indent1, indent2;
@@@ -950,13 -891,13 +960,13 @@@ static struct 
        char result;
        const char *check;
  } signature_check[] = {
 -      { 'G', ": Good signature from " },
 -      { 'B', ": BAD signature from " },
 +      { 'G', "\n[GNUPG:] GOODSIG " },
 +      { 'B', "\n[GNUPG:] BADSIG " },
  };
  
  static void parse_signature_lines(struct format_commit_context *ctx)
  {
 -      const char *buf = ctx->signature.gpg_output;
 +      const char *buf = ctx->signature.gpg_status;
        int i;
  
        for (i = 0; i < ARRAY_SIZE(signature_check); i++) {
                        continue;
                ctx->signature.good_bad = signature_check[i].result;
                found += strlen(signature_check[i].check);
 +              ctx->signature.key = xmemdupz(found, 16);
 +              found += 17;
                next = strchrnul(found, '\n');
                ctx->signature.signer = xmemdupz(found, next - found);
                break;
@@@ -979,7 -918,6 +989,7 @@@ static void parse_commit_signature(stru
        struct strbuf payload = STRBUF_INIT;
        struct strbuf signature = STRBUF_INIT;
        struct strbuf gpg_output = STRBUF_INIT;
 +      struct strbuf gpg_status = STRBUF_INIT;
        int status;
  
        ctx->commit_signature_parsed = 1;
                goto out;
        status = verify_signed_buffer(payload.buf, payload.len,
                                      signature.buf, signature.len,
 -                                    &gpg_output);
 +                                    &gpg_output, &gpg_status);
        if (status && !gpg_output.len)
                goto out;
        ctx->signature.gpg_output = strbuf_detach(&gpg_output, NULL);
 +      ctx->signature.gpg_status = strbuf_detach(&gpg_status, NULL);
        parse_signature_lines(ctx);
  
   out:
 +      strbuf_release(&gpg_status);
        strbuf_release(&gpg_output);
        strbuf_release(&payload);
        strbuf_release(&signature);
@@@ -1034,19 -970,12 +1044,19 @@@ static size_t format_commit_one(struct 
        switch (placeholder[0]) {
        case 'C':
                if (placeholder[1] == '(') {
 -                      const char *end = strchr(placeholder + 2, ')');
 +                      const char *begin = placeholder + 2;
 +                      const char *end = strchr(begin, ')');
                        char color[COLOR_MAXLEN];
 +
                        if (!end)
                                return 0;
 -                      color_parse_mem(placeholder + 2,
 -                                      end - (placeholder + 2),
 +                      if (!prefixcmp(begin, "auto,")) {
 +                              if (!want_color(c->pretty_ctx->color))
 +                                      return end - placeholder + 1;
 +                              begin += 5;
 +                      }
 +                      color_parse_mem(begin,
 +                                      end - begin,
                                        "--pretty format", color);
                        strbuf_addstr(sb, color);
                        return end - placeholder + 1;
                }
                return 0;       /* unknown %g placeholder */
        case 'N':
 -              if (c->pretty_ctx->show_notes) {
 -                      format_display_notes(commit->object.sha1, sb,
 -                                  get_log_output_encoding(), 0);
 +              if (c->pretty_ctx->notes_message) {
 +                      strbuf_addstr(sb, c->pretty_ctx->notes_message);
                        return 1;
                }
                return 0;
                        if (c->signature.signer)
                                strbuf_addstr(sb, c->signature.signer);
                        break;
 +              case 'K':
 +                      if (c->signature.key)
 +                              strbuf_addstr(sb, c->signature.key);
 +                      break;
                }
                return 2;
        }
@@@ -1335,18 -1261,29 +1345,18 @@@ void format_commit_message(const struc
                           const struct pretty_print_context *pretty_ctx)
  {
        struct format_commit_context context;
 -      static const char utf8[] = "UTF-8";
        const char *output_enc = pretty_ctx->output_encoding;
  
        memset(&context, 0, sizeof(context));
        context.commit = commit;
        context.pretty_ctx = pretty_ctx;
        context.wrap_start = sb->len;
 -      context.message = commit->buffer;
 -      if (output_enc) {
 -              char *enc = get_header(commit, "encoding");
 -              if (strcmp(enc ? enc : utf8, output_enc)) {
 -                      context.message = logmsg_reencode(commit, output_enc);
 -                      if (!context.message)
 -                              context.message = commit->buffer;
 -              }
 -              free(enc);
 -      }
 +      context.message = logmsg_reencode(commit, output_enc);
  
        strbuf_expand(sb, format, format_commit_item, &context);
        rewrap_message_tail(sb, &context, 0, 0, 0);
  
 -      if (context.message != commit->buffer)
 -              free(context.message);
 +      logmsg_free(context.message, commit);
        free(context.signature.gpg_output);
        free(context.signature.signer);
  }
@@@ -1376,7 -1313,7 +1386,7 @@@ static void pp_header(const struct pret
                        continue;
                }
  
 -              if (!memcmp(line, "parent ", 7)) {
 +              if (!prefixcmp(line, "parent ")) {
                        if (linelen != 48)
                                die("bad parent line in commit");
                        continue;
                 * FULL shows both authors but not dates.
                 * FULLER shows both authors and dates.
                 */
 -              if (!memcmp(line, "author ", 7)) {
 +              if (!prefixcmp(line, "author ")) {
                        strbuf_grow(sb, linelen + 80);
                        pp_user_info(pp, "Author", sb, line + 7, encoding);
                }
 -              if (!memcmp(line, "committer ", 10) &&
 +              if (!prefixcmp(line, "committer ") &&
                    (pp->fmt == CMIT_FMT_FULL || pp->fmt == CMIT_FMT_FULLER)) {
                        strbuf_grow(sb, linelen + 80);
                        pp_user_info(pp, "Commit", sb, line + 10, encoding);
@@@ -1487,13 -1424,23 +1497,13 @@@ void pp_remainder(const struct pretty_p
        }
  }
  
 -char *reencode_commit_message(const struct commit *commit, const char **encoding_p)
 -{
 -      const char *encoding;
 -
 -      encoding = get_log_output_encoding();
 -      if (encoding_p)
 -              *encoding_p = encoding;
 -      return logmsg_reencode(commit, encoding);
 -}
 -
  void pretty_print_commit(const struct pretty_print_context *pp,
                         const struct commit *commit,
                         struct strbuf *sb)
  {
        unsigned long beginning_of_body;
        int indent = 4;
 -      const char *msg = commit->buffer;
 +      const char *msg;
        char *reencoded;
        const char *encoding;
        int need_8bit_cte = pp->need_8bit_cte;
                return;
        }
  
 -      reencoded = reencode_commit_message(commit, &encoding);
 -      if (reencoded) {
 -              msg = reencoded;
 -      }
 +      encoding = get_log_output_encoding();
 +      msg = reencoded = logmsg_reencode(commit, encoding);
  
        if (pp->fmt == CMIT_FMT_ONELINE || pp->fmt == CMIT_FMT_EMAIL)
                indent = 0;
        if (pp->fmt == CMIT_FMT_EMAIL && sb->len <= beginning_of_body)
                strbuf_addch(sb, '\n');
  
 -      if (pp->show_notes)
 -              format_display_notes(commit->object.sha1, sb, encoding,
 -                                   NOTES_SHOW_HEADER | NOTES_INDENT);
 -
 -      free(reencoded);
 +      logmsg_free(reencoded, commit);
  }
  
  void pp_commit_easy(enum cmit_fmt fmt, const struct commit *commit,
diff --combined t/t4014-format-patch.sh
index 7fa3647514b616b1a40b233b23123a00c4d3ff6c,ba437379bc45255111a72c5a30ffd3b8b4f63543..bb1fc47fe8aa6904555c153f90a6026c45bf3645
@@@ -155,7 -155,7 +155,7 @@@ test_expect_failure 'additional comman
        git config --replace-all format.headers "Cc: R E Cipient <rcipient@example.com>" &&
        git format-patch --cc="S. E. Cipient <scipient@example.com>" --stdout master..side | sed -e "/^\$/q" >patch5 &&
        grep "^Cc: R E Cipient <rcipient@example.com>,\$" patch5 &&
 -      grep "^ *"S. E. Cipient" <scipient@example.com>\$" patch5
 +      grep "^ *\"S. E. Cipient\" <scipient@example.com>\$" patch5
  '
  
  test_expect_success 'command line headers' '
@@@ -183,7 -183,7 +183,7 @@@ test_expect_success 'command line To: h
  test_expect_failure 'command line To: header (rfc822)' '
  
        git format-patch --to="R. E. Cipient <rcipient@example.com>" --stdout master..side | sed -e "/^\$/q" >patch8 &&
 -      grep "^To: "R. E. Cipient" <rcipient@example.com>\$" patch8
 +      grep "^To: \"R. E. Cipient\" <rcipient@example.com>\$" patch8
  '
  
  test_expect_failure 'command line To: header (rfc2047)' '
@@@ -203,7 -203,7 +203,7 @@@ test_expect_failure 'configuration To: 
  
        git config format.to "R. E. Cipient <rcipient@example.com>" &&
        git format-patch --stdout master..side | sed -e "/^\$/q" >patch9 &&
 -      grep "^To: "R. E. Cipient" <rcipient@example.com>\$" patch9
 +      grep "^To: \"R. E. Cipient\" <rcipient@example.com>\$" patch9
  '
  
  test_expect_failure 'configuration To: header (rfc2047)' '
@@@ -271,22 -271,6 +271,22 @@@ test_expect_success 'multiple files' 
        ls patches/0001-Side-changes-1.patch patches/0002-Side-changes-2.patch patches/0003-Side-changes-3-with-n-backslash-n-in-it.patch
  '
  
 +test_expect_success 'reroll count' '
 +      rm -fr patches &&
 +      git format-patch -o patches --cover-letter --reroll-count 4 master..side >list &&
 +      ! grep -v "^patches/v4-000[0-3]-" list &&
 +      sed -n -e "/^Subject: /p" $(cat list) >subjects &&
 +      ! grep -v "^Subject: \[PATCH v4 [0-3]/3\] " subjects
 +'
 +
 +test_expect_success 'reroll count (-v)' '
 +      rm -fr patches &&
 +      git format-patch -o patches --cover-letter -v4 master..side >list &&
 +      ! grep -v "^patches/v4-000[0-3]-" list &&
 +      sed -n -e "/^Subject: /p" $(cat list) >subjects &&
 +      ! grep -v "^Subject: \[PATCH v4 [0-3]/3\] " subjects
 +'
 +
  check_threading () {
        expect="$1" &&
        shift &&
@@@ -666,19 -650,8 +666,19 @@@ test_expect_success 'format-patch --in-
  '
  
  test_expect_success 'format-patch --signoff' '
 -      git format-patch -1 --signoff --stdout |
 -      grep "^Signed-off-by: $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL>"
 +      git format-patch -1 --signoff --stdout >out &&
 +      grep "^Signed-off-by: $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL>" out
 +'
 +
 +test_expect_success 'format-patch --notes --signoff' '
 +      git notes --ref test add -m "test message" HEAD &&
 +      git format-patch -1 --signoff --stdout --notes=test >out &&
 +      # Three dashes must come after S-o-b
 +      ! sed "/^Signed-off-by: /q" out | grep "test message" &&
 +      sed "1,/^Signed-off-by: /d" out | grep "test message" &&
 +      # Notes message must come after three dashes
 +      ! sed "/^---$/q" out | grep "test message" &&
 +      sed "1,/^---$/d" out | grep "test message"
  '
  
  echo "fatal: --name-only does not make sense" > expect.name-only
@@@ -837,25 -810,26 +837,26 @@@ Subject: [PATCH] =?UTF-8?q?f=C3=B6=C3=B
   =?UTF-8?q?=C3=B6=C3=B6=20bar=20f=C3=B6=C3=B6=20bar=20f=C3=B6=C3=B6=20bar?=
   =?UTF-8?q?=20f=C3=B6=C3=B6=20bar=20f=C3=B6=C3=B6=20bar=20f=C3=B6=C3=B6=20?=
   =?UTF-8?q?bar=20f=C3=B6=C3=B6=20bar=20f=C3=B6=C3=B6=20bar=20f=C3=B6=C3=B6?=
-  =?UTF-8?q?=20bar=20f=C3=B6=C3=B6=20bar=20f=C3=B6=C3=B6=20bar=20f=C3=B6=C3?=
-  =?UTF-8?q?=B6=20bar=20f=C3=B6=C3=B6=20bar=20f=C3=B6=C3=B6=20bar=20f=C3=B6?=
-  =?UTF-8?q?=C3=B6=20bar=20f=C3=B6=C3=B6=20bar=20f=C3=B6=C3=B6=20bar=20f=C3?=
-  =?UTF-8?q?=B6=C3=B6=20bar=20f=C3=B6=C3=B6=20bar=20f=C3=B6=C3=B6=20bar=20f?=
+  =?UTF-8?q?=20bar=20f=C3=B6=C3=B6=20bar=20f=C3=B6=C3=B6=20bar=20f=C3=B6?=
+  =?UTF-8?q?=C3=B6=20bar=20f=C3=B6=C3=B6=20bar=20f=C3=B6=C3=B6=20bar=20f?=
   =?UTF-8?q?=C3=B6=C3=B6=20bar=20f=C3=B6=C3=B6=20bar=20f=C3=B6=C3=B6=20bar?=
   =?UTF-8?q?=20f=C3=B6=C3=B6=20bar=20f=C3=B6=C3=B6=20bar=20f=C3=B6=C3=B6=20?=
   =?UTF-8?q?bar=20f=C3=B6=C3=B6=20bar=20f=C3=B6=C3=B6=20bar=20f=C3=B6=C3=B6?=
-  =?UTF-8?q?=20bar=20f=C3=B6=C3=B6=20bar=20f=C3=B6=C3=B6=20bar=20f=C3=B6=C3?=
-  =?UTF-8?q?=B6=20bar=20f=C3=B6=C3=B6=20bar=20f=C3=B6=C3=B6=20bar=20f=C3=B6?=
-  =?UTF-8?q?=C3=B6=20bar=20f=C3=B6=C3=B6=20bar=20f=C3=B6=C3=B6=20bar=20f=C3?=
-  =?UTF-8?q?=B6=C3=B6=20bar=20f=C3=B6=C3=B6=20bar=20f=C3=B6=C3=B6=20bar=20f?=
+  =?UTF-8?q?=20bar=20f=C3=B6=C3=B6=20bar=20f=C3=B6=C3=B6=20bar=20f=C3=B6?=
+  =?UTF-8?q?=C3=B6=20bar=20f=C3=B6=C3=B6=20bar=20f=C3=B6=C3=B6=20bar=20f?=
   =?UTF-8?q?=C3=B6=C3=B6=20bar=20f=C3=B6=C3=B6=20bar=20f=C3=B6=C3=B6=20bar?=
   =?UTF-8?q?=20f=C3=B6=C3=B6=20bar=20f=C3=B6=C3=B6=20bar=20f=C3=B6=C3=B6=20?=
   =?UTF-8?q?bar=20f=C3=B6=C3=B6=20bar=20f=C3=B6=C3=B6=20bar=20f=C3=B6=C3=B6?=
-  =?UTF-8?q?=20bar=20f=C3=B6=C3=B6=20bar=20f=C3=B6=C3=B6=20bar=20f=C3=B6=C3?=
-  =?UTF-8?q?=B6=20bar=20f=C3=B6=C3=B6=20bar=20f=C3=B6=C3=B6=20bar=20f=C3=B6?=
-  =?UTF-8?q?=C3=B6=20bar=20f=C3=B6=C3=B6=20bar=20f=C3=B6=C3=B6=20bar=20f=C3?=
-  =?UTF-8?q?=B6=C3=B6=20bar=20f=C3=B6=C3=B6=20bar=20f=C3=B6=C3=B6=20bar=20f?=
-  =?UTF-8?q?=C3=B6=C3=B6=20bar=20f=C3=B6=C3=B6=20bar?=
+  =?UTF-8?q?=20bar=20f=C3=B6=C3=B6=20bar=20f=C3=B6=C3=B6=20bar=20f=C3=B6?=
+  =?UTF-8?q?=C3=B6=20bar=20f=C3=B6=C3=B6=20bar=20f=C3=B6=C3=B6=20bar=20f?=
+  =?UTF-8?q?=C3=B6=C3=B6=20bar=20f=C3=B6=C3=B6=20bar=20f=C3=B6=C3=B6=20bar?=
+  =?UTF-8?q?=20f=C3=B6=C3=B6=20bar=20f=C3=B6=C3=B6=20bar=20f=C3=B6=C3=B6=20?=
+  =?UTF-8?q?bar=20f=C3=B6=C3=B6=20bar=20f=C3=B6=C3=B6=20bar=20f=C3=B6=C3=B6?=
+  =?UTF-8?q?=20bar=20f=C3=B6=C3=B6=20bar=20f=C3=B6=C3=B6=20bar=20f=C3=B6?=
+  =?UTF-8?q?=C3=B6=20bar=20f=C3=B6=C3=B6=20bar=20f=C3=B6=C3=B6=20bar=20f?=
+  =?UTF-8?q?=C3=B6=C3=B6=20bar=20f=C3=B6=C3=B6=20bar=20f=C3=B6=C3=B6=20bar?=
+  =?UTF-8?q?=20f=C3=B6=C3=B6=20bar=20f=C3=B6=C3=B6=20bar=20f=C3=B6=C3=B6=20?=
+  =?UTF-8?q?bar?=
  EOF
  test_expect_success 'format-patch wraps extremely long subject (rfc2047)' '
        rm -rf patches/ &&
@@@ -979,46 -953,4 +980,46 @@@ test_expect_success 'format patch ignor
        test_cmp expect actual
  '
  
 +test_expect_success 'cover letter using branch description (1)' '
 +      git checkout rebuild-1 &&
 +      test_config branch.rebuild-1.description hello &&
 +      git format-patch --stdout --cover-letter master >actual &&
 +      grep hello actual >/dev/null
 +'
 +
 +test_expect_success 'cover letter using branch description (2)' '
 +      git checkout rebuild-1 &&
 +      test_config branch.rebuild-1.description hello &&
 +      git format-patch --stdout --cover-letter rebuild-1~2..rebuild-1 >actual &&
 +      grep hello actual >/dev/null
 +'
 +
 +test_expect_success 'cover letter using branch description (3)' '
 +      git checkout rebuild-1 &&
 +      test_config branch.rebuild-1.description hello &&
 +      git format-patch --stdout --cover-letter ^master rebuild-1 >actual &&
 +      grep hello actual >/dev/null
 +'
 +
 +test_expect_success 'cover letter using branch description (4)' '
 +      git checkout rebuild-1 &&
 +      test_config branch.rebuild-1.description hello &&
 +      git format-patch --stdout --cover-letter master.. >actual &&
 +      grep hello actual >/dev/null
 +'
 +
 +test_expect_success 'cover letter using branch description (5)' '
 +      git checkout rebuild-1 &&
 +      test_config branch.rebuild-1.description hello &&
 +      git format-patch --stdout --cover-letter -2 HEAD >actual &&
 +      grep hello actual >/dev/null
 +'
 +
 +test_expect_success 'cover letter using branch description (6)' '
 +      git checkout rebuild-1 &&
 +      test_config branch.rebuild-1.description hello &&
 +      git format-patch --stdout --cover-letter -2 >actual &&
 +      grep hello actual >/dev/null
 +'
 +
  test_done
diff --combined utf8.c
index 8f6e84b7b3cf589f437a81ee81fd0228b2faf2f2,0b30b54847127ec1e4af04b72ed5c6bd9ffef733..7f648574a550432e3160fcef178f894faf0bdd54
--- 1/utf8.c
--- 2/utf8.c
+++ b/utf8.c
@@@ -323,7 -323,7 +323,7 @@@ static size_t display_mode_esc_sequence
   * If indent is negative, assume that already -indent columns have been
   * consumed (and no extra indent is necessary for the first line).
   */
 -int strbuf_add_wrapped_text(struct strbuf *buf,
 +void strbuf_add_wrapped_text(struct strbuf *buf,
                const char *text, int indent1, int indent2, int width)
  {
        int indent, w, assume_utf8 = 1;
  
        if (width <= 0) {
                strbuf_add_indented_text(buf, text, indent1, indent2);
 -              return 1;
 +              return;
        }
  
  retry:
                        if (w <= width || !space) {
                                const char *start = bol;
                                if (!c && text == start)
 -                                      return w;
 +                                      return;
                                if (space)
                                        start = space;
                                else
                                        strbuf_addchars(buf, ' ', indent);
                                strbuf_add(buf, start, text - start);
                                if (!c)
 -                                      return w;
 +                                      return;
                                space = text;
                                if (c == '\t')
                                        w |= 0x07;
@@@ -405,12 -405,13 +405,12 @@@ new_line
        }
  }
  
 -int strbuf_add_wrapped_bytes(struct strbuf *buf, const char *data, int len,
 +void strbuf_add_wrapped_bytes(struct strbuf *buf, const char *data, int len,
                             int indent, int indent2, int width)
  {
        char *tmp = xstrndup(data, len);
 -      int r = strbuf_add_wrapped_text(buf, tmp, indent, indent2, width);
 +      strbuf_add_wrapped_text(buf, tmp, indent, indent2, width);
        free(tmp);
 -      return r;
  }
  
  int is_encoding_utf8(const char *name)
@@@ -429,27 -430,6 +429,27 @@@ int same_encoding(const char *src, cons
        return !strcasecmp(src, dst);
  }
  
 +/*
 + * Wrapper for fprintf and returns the total number of columns required
 + * for the printed string, assuming that the string is utf8.
 + */
 +int utf8_fprintf(FILE *stream, const char *format, ...)
 +{
 +      struct strbuf buf = STRBUF_INIT;
 +      va_list arg;
 +      int columns;
 +
 +      va_start(arg, format);
 +      strbuf_vaddf(&buf, format, arg);
 +      va_end(arg);
 +
 +      columns = fputs(buf.buf, stream);
 +      if (0 <= columns) /* keep the error from the I/O */
 +              columns = utf8_strwidth(buf.buf);
 +      strbuf_release(&buf);
 +      return columns;
 +}
 +
  /*
   * Given a buffer and its encoding, return it re-encoded
   * with iconv.  If the conversion fails, returns NULL.
@@@ -507,27 -487,50 +507,66 @@@ char *reencode_string(const char *in, c
  
        if (!in_encoding)
                return NULL;
 +
        conv = iconv_open(out_encoding, in_encoding);
 -      if (conv == (iconv_t) -1)
 -              return NULL;
 +      if (conv == (iconv_t) -1) {
 +              /*
 +               * Some platforms do not have the variously spelled variants of
 +               * UTF-8, so let's fall back to trying the most official
 +               * spelling. We do so only as a fallback in case the platform
 +               * does understand the user's spelling, but not our official
 +               * one.
 +               */
 +              if (is_encoding_utf8(in_encoding))
 +                      in_encoding = "UTF-8";
 +              if (is_encoding_utf8(out_encoding))
 +                      out_encoding = "UTF-8";
 +              conv = iconv_open(out_encoding, in_encoding);
 +              if (conv == (iconv_t) -1)
 +                      return NULL;
 +      }
 +
        out = reencode_string_iconv(in, strlen(in), conv);
        iconv_close(conv);
        return out;
  }
  #endif
+ /*
+  * Returns first character length in bytes for multi-byte `text` according to
+  * `encoding`.
+  *
+  * - The `text` pointer is updated to point at the next character.
+  * - When `remainder_p` is not NULL, on entry `*remainder_p` is how much bytes
+  *   we can consume from text, and on exit `*remainder_p` is reduced by returned
+  *   character length. Otherwise `text` is treated as limited by NUL.
+  */
+ int mbs_chrlen(const char **text, size_t *remainder_p, const char *encoding)
+ {
+       int chrlen;
+       const char *p = *text;
+       size_t r = (remainder_p ? *remainder_p : SIZE_MAX);
+       if (r < 1)
+               return 0;
+       if (is_encoding_utf8(encoding)) {
+               pick_one_utf8_char(&p, &r);
+               chrlen = p ? (p - *text)
+                          : 1 /* not valid UTF-8 -> raw byte sequence */;
+       }
+       else {
+               /*
+                * TODO use iconv to decode one char and obtain its chrlen
+                * for now, let's treat encodings != UTF-8 as one-byte
+                */
+               chrlen = 1;
+       }
+       *text += chrlen;
+       if (remainder_p)
+               *remainder_p -= chrlen;
+       return chrlen;
+ }
diff --combined utf8.h
index 501b2bd9c4fd3fdce6dcbba1a3e3f7c0211d6c8f,465c01d8c35f7c409ad71e5a15ca62e13af34f31..1f8ecad1e88451c622de08ea14414021da64f402
--- 1/utf8.h
--- 2/utf8.h
+++ b/utf8.h
@@@ -8,11 -8,10 +8,11 @@@ int utf8_strwidth(const char *string)
  int is_utf8(const char *text);
  int is_encoding_utf8(const char *name);
  int same_encoding(const char *, const char *);
 +int utf8_fprintf(FILE *, const char *, ...);
  
 -int strbuf_add_wrapped_text(struct strbuf *buf,
 +void strbuf_add_wrapped_text(struct strbuf *buf,
                const char *text, int indent, int indent2, int width);
 -int strbuf_add_wrapped_bytes(struct strbuf *buf, const char *data, int len,
 +void strbuf_add_wrapped_bytes(struct strbuf *buf, const char *data, int len,
                             int indent, int indent2, int width);
  
  #ifndef NO_ICONV
@@@ -22,4 -21,6 +22,6 @@@ char *reencode_string(const char *in, c
  #define reencode_string(a,b,c) NULL
  #endif
  
+ int mbs_chrlen(const char **text, size_t *remainder_p, const char *encoding);
  #endif