Merge branch 'bc/append-signed-off-by'
authorJunio C Hamano <gitster@pobox.com>
Mon, 1 Apr 2013 15:59:23 +0000 (08:59 -0700)
committerJunio C Hamano <gitster@pobox.com>
Mon, 1 Apr 2013 15:59:24 +0000 (08:59 -0700)
Consolidate codepaths that inspect log-message-to-be and decide to
add a new Signed-off-by line in various commands.

* bc/append-signed-off-by:
git-commit: populate the edit buffer with 2 blank lines before s-o-b
Unify appending signoff in format-patch, commit and sequencer
format-patch: update append_signoff prototype
t4014: more tests about appending s-o-b lines
sequencer.c: teach append_signoff to avoid adding a duplicate newline
sequencer.c: teach append_signoff how to detect duplicate s-o-b
sequencer.c: always separate "(cherry picked from" from commit body
sequencer.c: require a conforming footer to be preceded by a blank line
sequencer.c: recognize "(cherry picked from ..." as part of s-o-b footer
t/t3511: add some tests of 'cherry-pick -s' functionality
t/test-lib-functions.sh: allow to specify the tag name to test_commit
commit, cherry-pick -s: remove broken support for multiline rfc2822 fields
sequencer.c: rework search for start of footer to improve clarity

1  2 
builtin/commit.c
builtin/log.c
log-tree.c
revision.h
sequencer.c
t/t4014-format-patch.sh
t/t7502-commit.sh
t/test-lib-functions.sh
diff --combined builtin/commit.c
index d21d07a1a8e9fbc365a4555a0f9e81d0c1f2a7d0,7b9e2ac2ebbe29efbda17bf0640f8796ead9d080..46204375e786ea583ca1a7eabc3060611adc3d04
  #include "sequencer.h"
  
  static const char * const builtin_commit_usage[] = {
 -      N_("git commit [options] [--] <filepattern>..."),
 +      N_("git commit [options] [--] <pathspec>..."),
        NULL
  };
  
  static const char * const builtin_status_usage[] = {
 -      N_("git status [options] [--] <filepattern>..."),
 +      N_("git status [options] [--] <pathspec>..."),
        NULL
  };
  
@@@ -103,7 -103,7 +103,7 @@@ static enum 
        CLEANUP_NONE,
        CLEANUP_ALL
  } cleanup_mode;
 -static char *cleanup_arg;
 +static const char *cleanup_arg;
  
  static enum commit_whence whence;
  static int use_editor = 1, include_status = 1;
@@@ -124,10 -124,8 +124,10 @@@ static int opt_parse_m(const struct opt
        if (unset)
                strbuf_setlen(buf, 0);
        else {
 +              if (buf->len)
 +                      strbuf_addch(buf, '\n');
                strbuf_addstr(buf, arg);
 -              strbuf_addstr(buf, "\n\n");
 +              strbuf_complete_line(buf);
        }
        return 0;
  }
@@@ -702,7 -700,7 +702,7 @@@ static int prepare_to_commit(const cha
                        previous = eol;
                }
  
-               append_signoff(&sb, ignore_footer);
+               append_signoff(&sb, ignore_footer, 0);
        }
  
        if (fwrite(sb.buf, 1, sb.len, s->fp) < sb.len)
                if (cleanup_mode == CLEANUP_ALL)
                        status_printf(s, GIT_COLOR_NORMAL,
                                _("Please enter the commit message for your changes."
 -                              " Lines starting\nwith '#' will be ignored, and an empty"
 -                              " message aborts the commit.\n"));
 +                                " Lines starting\nwith '%c' will be ignored, and an empty"
 +                                " message aborts the commit.\n"), comment_line_char);
                else /* CLEANUP_SPACE, that is. */
                        status_printf(s, GIT_COLOR_NORMAL,
                                _("Please enter the commit message for your changes."
 -                              " Lines starting\n"
 -                              "with '#' will be kept; you may remove them"
 -                              " yourself if you want to.\n"
 -                              "An empty message aborts the commit.\n"));
 +                                " Lines starting\n"
 +                                "with '%c' will be kept; you may remove them"
 +                                " yourself if you want to.\n"
 +                                "An empty message aborts the commit.\n"), comment_line_char);
                if (only_include_assumed)
                        status_printf_ln(s, GIT_COLOR_NORMAL,
                                        "%s", only_include_assumed);
                                ident_shown++ ? "" : "\n",
                                author_ident->buf);
  
 -              if (!user_ident_sufficiently_given())
 +              if (!committer_ident_sufficiently_given())
                        status_printf_ln(s, GIT_COLOR_NORMAL,
                                _("%s"
                                "Committer: %s"),
@@@ -948,14 -946,24 +948,14 @@@ static void handle_untracked_files_arg(
  
  static const char *read_commit_message(const char *name)
  {
 -      const char *out_enc, *out;
 +      const char *out_enc;
        struct commit *commit;
  
        commit = lookup_commit_reference_by_name(name);
        if (!commit)
                die(_("could not lookup commit %s"), name);
        out_enc = get_commit_output_encoding();
 -      out = logmsg_reencode(commit, out_enc);
 -
 -      /*
 -       * If we failed to reencode the buffer, just copy it
 -       * byte for byte so the user can try to fix it up.
 -       * This also handles the case where input and output
 -       * encodings are identical.
 -       */
 -      if (out == NULL)
 -              out = xstrdup(commit->buffer);
 -      return out;
 +      return logmsg_reencode(commit, out_enc);
  }
  
  static int parse_and_validate_options(int argc, const char *argv[],
@@@ -1257,7 -1265,7 +1257,7 @@@ static void print_summary(const char *p
                strbuf_addstr(&format, "\n Author: ");
                strbuf_addbuf_percentquote(&format, &author_ident);
        }
 -      if (!user_ident_sufficiently_given()) {
 +      if (!committer_ident_sufficiently_given()) {
                strbuf_addstr(&format, "\n Committer: ");
                strbuf_addbuf_percentquote(&format, &committer_ident);
                if (advice_implicit_identity) {
@@@ -1312,8 -1320,6 +1312,8 @@@ static int git_commit_config(const cha
                include_status = git_config_bool(k, v);
                return 0;
        }
 +      if (!strcmp(k, "commit.cleanup"))
 +              return git_config_string(&cleanup_arg, k, v);
  
        status = git_gpg_config(k, v, NULL);
        if (status)
        return git_status_config(k, v, s);
  }
  
 -static const char post_rewrite_hook[] = "hooks/post-rewrite";
 -
  static int run_rewrite_hook(const unsigned char *oldsha1,
                            const unsigned char *newsha1)
  {
        int code;
        size_t n;
  
 -      if (access(git_path(post_rewrite_hook), X_OK) < 0)
 +      argv[0] = find_hook("post-rewrite");
 +      if (!argv[0])
                return 0;
  
 -      argv[0] = git_path(post_rewrite_hook);
        argv[1] = "amend";
        argv[2] = NULL;
  
diff --combined builtin/log.c
index 8f0b2e84fef5d1b9c07ea8846c9fbc1318d8d51b,bb48344113cc3ab0e0a9fc4aba0408b43b4637a9..59de484bc29a38fb538e1146a91ddef708ebc3cc
@@@ -22,7 -22,6 +22,7 @@@
  #include "branch.h"
  #include "streaming.h"
  #include "version.h"
 +#include "mailmap.h"
  
  /* Set a default date-time format for git log ("log.date" config variable) */
  static const char *default_date_mode = NULL;
@@@ -31,7 -30,6 +31,7 @@@ static int default_abbrev_commit
  static int default_show_root = 1;
  static int decoration_style;
  static int decoration_given;
 +static int use_mailmap_config;
  static const char *fmt_patch_subject_prefix = "PATCH";
  static const char *fmt_pretty;
  
@@@ -96,18 -94,16 +96,18 @@@ static void cmd_log_init_finish(int arg
                         struct rev_info *rev, struct setup_revision_opt *opt)
  {
        struct userformat_want w;
 -      int quiet = 0, source = 0;
 +      int quiet = 0, source = 0, mailmap = 0;
  
        const struct option builtin_log_options[] = {
                OPT_BOOLEAN(0, "quiet", &quiet, N_("suppress diff output")),
                OPT_BOOLEAN(0, "source", &source, N_("show source")),
 +              OPT_BOOLEAN(0, "use-mailmap", &mailmap, N_("Use mail map file")),
                { OPTION_CALLBACK, 0, "decorate", NULL, NULL, N_("decorate options"),
                  PARSE_OPT_OPTARG, decorate_callback},
                OPT_END()
        };
  
 +      mailmap = use_mailmap_config;
        argc = parse_options(argc, argv, prefix,
                             builtin_log_options, builtin_log_usage,
                             PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN |
        if (source)
                rev->show_source = 1;
  
 +      if (mailmap) {
 +              rev->mailmap = xcalloc(1, sizeof(struct string_list));
 +              read_mailmap(rev->mailmap, NULL);
 +      }
 +
        if (rev->pretty_given && rev->commit_format == CMIT_FMT_RAW) {
                /*
                 * "log --pretty=raw" is special; ignore UI oriented
@@@ -360,11 -351,6 +360,11 @@@ static int git_log_config(const char *v
        }
        if (!prefixcmp(var, "color.decorate."))
                return parse_decorate_color_config(var, 15, value);
 +      if (!strcmp(var, "log.mailmap")) {
 +              use_mailmap_config = git_config_bool(var, value);
 +              return 0;
 +      }
 +
        if (grep_config(var, value, cb) < 0)
                return -1;
        return git_diff_ui_config(var, value, cb);
@@@ -692,7 -678,7 +692,7 @@@ static int reopen_stdout(struct commit 
                         struct rev_info *rev, int quiet)
  {
        struct strbuf filename = STRBUF_INIT;
 -      int suffix_len = strlen(fmt_patch_suffix) + 1;
 +      int suffix_len = strlen(rev->patch_suffix) + 1;
  
        if (output_directory) {
                strbuf_addstr(&filename, output_directory);
                        strbuf_addch(&filename, '/');
        }
  
 -      get_patch_filename(commit, subject, rev->nr, fmt_patch_suffix, &filename);
 +      if (rev->numbered_files)
 +              strbuf_addf(&filename, "%d", rev->nr);
 +      else if (commit)
 +              fmt_output_commit(&filename, commit, rev);
 +      else
 +              fmt_output_subject(&filename, subject, rev);
  
        if (!quiet)
                fprintf(realstdout, "%s\n", filename.buf + outdir_offset);
@@@ -792,6 -773,7 +792,6 @@@ static void add_branch_description(stru
  }
  
  static void make_cover_letter(struct rev_info *rev, int use_stdout,
 -                            int numbered, int numbered_files,
                              struct commit *origin,
                              int nr, struct commit **list, struct commit *head,
                              const char *branch_name,
        committer = git_committer_info(0);
  
        if (!use_stdout &&
 -          reopen_stdout(NULL, numbered_files ? NULL : "cover-letter", rev, quiet))
 +          reopen_stdout(NULL, rev->numbered_files ? NULL : "cover-letter", rev, quiet))
                return;
  
        log_write_email_headers(rev, head, &pp.subject, &pp.after_subject,
@@@ -1034,9 -1016,8 +1034,9 @@@ static char *find_branch_name(struct re
  {
        int i, positive = -1;
        unsigned char branch_sha1[20];
 -      struct strbuf buf = STRBUF_INIT;
 -      const char *branch;
 +      const unsigned char *tip_sha1;
 +      const char *ref;
 +      char *full_ref, *branch = NULL;
  
        for (i = 0; i < rev->cmdline.nr; i++) {
                if (rev->cmdline.rev[i].flags & UNINTERESTING)
                else
                        return NULL;
        }
 -      if (positive < 0)
 +      if (0 <= positive) {
 +              ref = rev->cmdline.rev[positive].name;
 +              tip_sha1 = rev->cmdline.rev[positive].item->sha1;
 +      } else if (!rev->cmdline.nr && rev->pending.nr == 1 &&
 +                 !strcmp(rev->pending.objects[0].name, "HEAD")) {
 +              /*
 +               * No actual ref from command line, but "HEAD" from
 +               * rev->def was added in setup_revisions()
 +               * e.g. format-patch --cover-letter -12
 +               */
 +              ref = "HEAD";
 +              tip_sha1 = rev->pending.objects[0].item->sha1;
 +      } else {
                return NULL;
 -      strbuf_addf(&buf, "refs/heads/%s", rev->cmdline.rev[positive].name);
 -      branch = resolve_ref_unsafe(buf.buf, branch_sha1, 1, NULL);
 -      if (!branch ||
 -          prefixcmp(branch, "refs/heads/") ||
 -          hashcmp(rev->cmdline.rev[positive].item->sha1, branch_sha1))
 -              branch = NULL;
 -      strbuf_release(&buf);
 -      if (branch)
 -              return xstrdup(rev->cmdline.rev[positive].name);
 -      return NULL;
 +      }
 +      if (dwim_ref(ref, strlen(ref), branch_sha1, &full_ref) &&
 +          !prefixcmp(full_ref, "refs/heads/") &&
 +          !hashcmp(tip_sha1, branch_sha1))
 +              branch = xstrdup(full_ref + strlen("refs/heads/"));
 +      free(full_ref);
 +      return branch;
  }
  
  int cmd_format_patch(int argc, const char **argv, const char *prefix)
        int nr = 0, total, i;
        int use_stdout = 0;
        int start_number = -1;
 -      int numbered_files = 0;         /* _just_ numbers */
 +      int just_numbers = 0;
        int ignore_if_in_upstream = 0;
        int cover_letter = 0;
        int boundary_count = 0;
        struct commit *origin = NULL, *head = NULL;
        const char *in_reply_to = NULL;
        struct patch_ids ids;
-       char *add_signoff = NULL;
        struct strbuf buf = STRBUF_INIT;
        int use_patch_format = 0;
        int quiet = 0;
 +      int reroll_count = -1;
        char *branch_name = NULL;
        const struct option builtin_format_patch_options[] = {
                { OPTION_CALLBACK, 'n', "numbered", &numbered, NULL,
                            N_("print patches to standard out")),
                OPT_BOOLEAN(0, "cover-letter", &cover_letter,
                            N_("generate a cover letter")),
 -              OPT_BOOLEAN(0, "numbered-files", &numbered_files,
 +              OPT_BOOLEAN(0, "numbered-files", &just_numbers,
                            N_("use simple number sequence for output file names")),
                OPT_STRING(0, "suffix", &fmt_patch_suffix, N_("sfx"),
                            N_("use <sfx> instead of '.patch'")),
                OPT_INTEGER(0, "start-number", &start_number,
                            N_("start numbering patches at <n> instead of 1")),
 +              OPT_INTEGER('v', "reroll-count", &reroll_count,
 +                          N_("mark the series as Nth re-roll")),
                { OPTION_CALLBACK, 0, "subject-prefix", &rev, N_("prefix"),
                            N_("Use [<prefix>] instead of [PATCH]"),
                            PARSE_OPT_NONEG, subject_prefix_callback },
                             PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN |
                             PARSE_OPT_KEEP_DASHDASH);
  
-       if (do_signoff) {
-               const char *committer;
-               const char *endpos;
-               committer = git_committer_info(IDENT_STRICT);
-               endpos = strchr(committer, '>');
-               if (!endpos)
-                       die(_("bogus committer info %s"), committer);
-               add_signoff = xmemdupz(committer, endpos - committer + 1);
-       }
 +      if (0 < reroll_count) {
 +              struct strbuf sprefix = STRBUF_INIT;
 +              strbuf_addf(&sprefix, "%s v%d",
 +                          rev.subject_prefix, reroll_count);
 +              rev.reroll_count = reroll_count;
 +              rev.subject_prefix = strbuf_detach(&sprefix, NULL);
 +      }
 +
        for (i = 0; i < extra_hdr.nr; i++) {
                strbuf_addstr(&buf, extra_hdr.items[i].string);
                strbuf_addch(&buf, '\n');
                const char *msgid = clean_message_id(in_reply_to);
                string_list_append(rev.ref_message_ids, msgid);
        }
 -      rev.numbered_files = numbered_files;
 +      rev.numbered_files = just_numbers;
        rev.patch_suffix = fmt_patch_suffix;
        if (cover_letter) {
                if (thread)
                        gen_message_id(&rev, "cover");
 -              make_cover_letter(&rev, use_stdout, numbered, numbered_files,
 +              make_cover_letter(&rev, use_stdout,
                                  origin, nr, list, head, branch_name, quiet);
                total++;
                start_number--;
        }
-       rev.add_signoff = add_signoff;
+       rev.add_signoff = do_signoff;
        while (0 <= --nr) {
                int shown;
                commit = list[nr];
                }
  
                if (!use_stdout &&
 -                  reopen_stdout(numbered_files ? NULL : commit, NULL, &rev, quiet))
 +                  reopen_stdout(rev.numbered_files ? NULL : commit, NULL, &rev, quiet))
                        die(_("Failed to create output files"));
                shown = log_tree_commit(&rev, commit);
                free(commit->buffer);
diff --combined log-tree.c
index 3d888238711364338a83e7d81bf27f555352bdca,cf274058caacde4451ad889e644f4ff3302faaf6..92bb2bf48e641ee6161a0e10dd87d9ce06632f7c
@@@ -9,6 -9,7 +9,7 @@@
  #include "string-list.h"
  #include "color.h"
  #include "gpg-interface.h"
+ #include "sequencer.h"
  
  struct decoration name_decoration = { "object names" };
  
@@@ -206,89 -207,6 +207,6 @@@ void show_decorations(struct rev_info *
        putchar(')');
  }
  
- /*
-  * Search for "^[-A-Za-z]+: [^@]+@" pattern. It usually matches
-  * Signed-off-by: and Acked-by: lines.
-  */
- static int detect_any_signoff(char *letter, int size)
- {
-       char *cp;
-       int seen_colon = 0;
-       int seen_at = 0;
-       int seen_name = 0;
-       int seen_head = 0;
-       cp = letter + size;
-       while (letter <= --cp && *cp == '\n')
-               continue;
-       while (letter <= cp) {
-               char ch = *cp--;
-               if (ch == '\n')
-                       break;
-               if (!seen_at) {
-                       if (ch == '@')
-                               seen_at = 1;
-                       continue;
-               }
-               if (!seen_colon) {
-                       if (ch == '@')
-                               return 0;
-                       else if (ch == ':')
-                               seen_colon = 1;
-                       else
-                               seen_name = 1;
-                       continue;
-               }
-               if (('A' <= ch && ch <= 'Z') ||
-                   ('a' <= ch && ch <= 'z') ||
-                   ch == '-') {
-                       seen_head = 1;
-                       continue;
-               }
-               /* no empty last line doesn't match */
-               return 0;
-       }
-       return seen_head && seen_name;
- }
- static void append_signoff(struct strbuf *sb, const char *signoff)
- {
-       static const char signed_off_by[] = "Signed-off-by: ";
-       size_t signoff_len = strlen(signoff);
-       int has_signoff = 0;
-       char *cp;
-       cp = sb->buf;
-       /* First see if we already have the sign-off by the signer */
-       while ((cp = strstr(cp, signed_off_by))) {
-               has_signoff = 1;
-               cp += strlen(signed_off_by);
-               if (cp + signoff_len >= sb->buf + sb->len)
-                       break;
-               if (strncmp(cp, signoff, signoff_len))
-                       continue;
-               if (!isspace(cp[signoff_len]))
-                       continue;
-               /* we already have him */
-               return;
-       }
-       if (!has_signoff)
-               has_signoff = detect_any_signoff(sb->buf, sb->len);
-       if (!has_signoff)
-               strbuf_addch(sb, '\n');
-       strbuf_addstr(sb, signed_off_by);
-       strbuf_add(sb, signoff, signoff_len);
-       strbuf_addch(sb, '\n');
- }
  static unsigned int digits_in_number(unsigned int number)
  {
        unsigned int i = 10, result = 1;
        return result;
  }
  
 -void get_patch_filename(struct commit *commit, const char *subject, int nr,
 -                      const char *suffix, struct strbuf *buf)
 +void fmt_output_subject(struct strbuf *filename,
 +                      const char *subject,
 +                      struct rev_info *info)
  {
 -      int suffix_len = strlen(suffix) + 1;
 -      int start_len = buf->len;
 -
 -      strbuf_addf(buf, commit || subject ? "%04d-" : "%d", nr);
 -      if (commit || subject) {
 -              int max_len = start_len + FORMAT_PATCH_NAME_MAX - suffix_len;
 -              struct pretty_print_context ctx = {0};
 -
 -              if (subject)
 -                      strbuf_addstr(buf, subject);
 -              else if (commit)
 -                      format_commit_message(commit, "%f", buf, &ctx);
 -
 -              if (max_len < buf->len)
 -                      strbuf_setlen(buf, max_len);
 -              strbuf_addstr(buf, suffix);
 -      }
 +      const char *suffix = info->patch_suffix;
 +      int nr = info->nr;
 +      int start_len = filename->len;
 +      int max_len = start_len + FORMAT_PATCH_NAME_MAX - (strlen(suffix) + 1);
 +
 +      if (0 < info->reroll_count)
 +              strbuf_addf(filename, "v%d-", info->reroll_count);
 +      strbuf_addf(filename, "%04d-%s", nr, subject);
 +
 +      if (max_len < filename->len)
 +              strbuf_setlen(filename, max_len);
 +      strbuf_addstr(filename, suffix);
 +}
 +
 +void fmt_output_commit(struct strbuf *filename,
 +                     struct commit *commit,
 +                     struct rev_info *info)
 +{
 +      struct pretty_print_context ctx = {0};
 +      struct strbuf subject = STRBUF_INIT;
 +
 +      format_commit_message(commit, "%f", &subject, &ctx);
 +      fmt_output_subject(filename, subject.buf, info);
 +      strbuf_release(&subject);
  }
  
  void log_write_email_headers(struct rev_info *opt, struct commit *commit,
                         mime_boundary_leader, opt->mime_boundary);
                extra_headers = subject_buffer;
  
 -              get_patch_filename(opt->numbered_files ? NULL : commit, NULL,
 -                                 opt->nr, opt->patch_suffix, &filename);
 +              if (opt->numbered_files)
 +                      strbuf_addf(&filename, "%d", opt->nr);
 +              else
 +                      fmt_output_commit(&filename, commit, opt);
                snprintf(buffer, sizeof(buffer) - 1,
                         "\n--%s%s\n"
                         "Content-Type: text/x-patch;"
@@@ -444,7 -352,7 +362,7 @@@ static void show_signature(struct rev_i
  
        status = verify_signed_buffer(payload.buf, payload.len,
                                      signature.buf, signature.len,
 -                                    &gpg_output);
 +                                    &gpg_output, NULL);
        if (status && !gpg_output.len)
                strbuf_addstr(&gpg_output, "No signature\n");
  
@@@ -508,17 -416,20 +426,17 @@@ static void show_one_mergetag(struct re
        gpg_message_offset = verify_message.len;
  
        payload_size = parse_signature(extra->value, extra->len);
 -      if ((extra->len <= payload_size) ||
 -          (verify_signed_buffer(extra->value, payload_size,
 -                                extra->value + payload_size,
 -                                extra->len - payload_size,
 -                                &verify_message) &&
 -           verify_message.len <= gpg_message_offset)) {
 -              strbuf_addstr(&verify_message, "No signature\n");
 -              status = -1;
 -      }
 -      else if (strstr(verify_message.buf + gpg_message_offset,
 -                      ": Good signature from "))
 -              status = 0;
 -      else
 -              status = -1;
 +      status = -1;
 +      if (extra->len > payload_size)
 +              if (verify_signed_buffer(extra->value, payload_size,
 +                                       extra->value + payload_size,
 +                                       extra->len - payload_size,
 +                                       &verify_message, NULL)) {
 +                      if (verify_message.len <= gpg_message_offset)
 +                              strbuf_addstr(&verify_message, "No signature\n");
 +                      else
 +                              status = 0;
 +              }
  
        show_sig_lines(opt, status, verify_message.buf);
        strbuf_release(&verify_message);
@@@ -669,8 -580,10 +587,10 @@@ void show_log(struct rev_info *opt
        /*
         * And then the pretty-printed message itself
         */
-       if (ctx.need_8bit_cte >= 0)
-               ctx.need_8bit_cte = has_non_ascii(opt->add_signoff);
+       if (ctx.need_8bit_cte >= 0 && opt->add_signoff)
+               ctx.need_8bit_cte =
+                       has_non_ascii(fmt_name(getenv("GIT_COMMITTER_NAME"),
+                                              getenv("GIT_COMMITTER_EMAIL")));
        ctx.date_mode = opt->date_mode;
        ctx.date_mode_explicit = opt->date_mode_explicit;
        ctx.abbrev = opt->diffopt.abbrev;
        ctx.preserve_subject = opt->preserve_subject;
        ctx.reflog_info = opt->reflog_info;
        ctx.fmt = opt->commit_format;
 +      ctx.mailmap = opt->mailmap;
 +      ctx.color = opt->diffopt.use_color;
        pretty_print_commit(&ctx, commit, &msgbuf);
  
        if (opt->add_signoff)
-               append_signoff(&msgbuf, opt->add_signoff);
+               append_signoff(&msgbuf, 0, APPEND_SIGNOFF_DEDUP);
  
        if ((ctx.fmt != CMIT_FMT_USERFORMAT) &&
            ctx.notes_message && *ctx.notes_message) {
diff --combined revision.h
index 5da09ee3efa976b503cba5d13e347aad0f6c764c,d20defabd5ea448f6a55fc6ad08e8a534177da3d..01bd2b7c07719c9628bba13e34b581dc1fdbc0af
@@@ -135,16 -135,14 +135,16 @@@ struct rev_info 
        const char      *mime_boundary;
        const char      *patch_suffix;
        int             numbered_files;
 +      int             reroll_count;
        char            *message_id;
        struct string_list *ref_message_ids;
-       const char      *add_signoff;
+       int             add_signoff;
        const char      *extra_headers;
        const char      *log_reencode;
        const char      *subject_prefix;
        int             no_inline;
        int             show_log_size;
 +      struct string_list *mailmap;
  
        /* Filter by commit log message */
        struct grep_opt grep_filter;
diff --combined sequencer.c
index aef5e8a0170c5b337f1aa3ac7a35be3b54957729,a07d2d00b9f7090e1e5c4526b77fb49f34865206..baa031052e4e3f77bb6d4a257a5669ca5a5d617e
  #define GIT_REFLOG_ACTION "GIT_REFLOG_ACTION"
  
  const char sign_off_header[] = "Signed-off-by: ";
+ static const char cherry_picked_prefix[] = "(cherry picked from commit ";
+ static int is_rfc2822_line(const char *buf, int len)
+ {
+       int i;
+       for (i = 0; i < len; i++) {
+               int ch = buf[i];
+               if (ch == ':')
+                       return 1;
+               if (!isalnum(ch) && ch != '-')
+                       break;
+       }
+       return 0;
+ }
+ static int is_cherry_picked_from_line(const char *buf, int len)
+ {
+       /*
+        * We only care that it looks roughly like (cherry picked from ...)
+        */
+       return len > strlen(cherry_picked_prefix) + 1 &&
+               !prefixcmp(buf, cherry_picked_prefix) && buf[len - 1] == ')';
+ }
+ /*
+  * Returns 0 for non-conforming footer
+  * Returns 1 for conforming footer
+  * Returns 2 when sob exists within conforming footer
+  * Returns 3 when sob exists within conforming footer as last entry
+  */
+ static int has_conforming_footer(struct strbuf *sb, struct strbuf *sob,
+       int ignore_footer)
+ {
+       char prev;
+       int i, k;
+       int len = sb->len - ignore_footer;
+       const char *buf = sb->buf;
+       int found_sob = 0;
+       /* footer must end with newline */
+       if (!len || buf[len - 1] != '\n')
+               return 0;
+       prev = '\0';
+       for (i = len - 1; i > 0; i--) {
+               char ch = buf[i];
+               if (prev == '\n' && ch == '\n') /* paragraph break */
+                       break;
+               prev = ch;
+       }
+       /* require at least one blank line */
+       if (prev != '\n' || buf[i] != '\n')
+               return 0;
+       /* advance to start of last paragraph */
+       while (i < len - 1 && buf[i] == '\n')
+               i++;
+       for (; i < len; i = k) {
+               int found_rfc2822;
+               for (k = i; k < len && buf[k] != '\n'; k++)
+                       ; /* do nothing */
+               k++;
+               found_rfc2822 = is_rfc2822_line(buf + i, k - i - 1);
+               if (found_rfc2822 && sob &&
+                   !strncmp(buf + i, sob->buf, sob->len))
+                       found_sob = k;
+               if (!(found_rfc2822 ||
+                     is_cherry_picked_from_line(buf + i, k - i - 1)))
+                       return 0;
+       }
+       if (found_sob == i)
+               return 3;
+       if (found_sob)
+               return 2;
+       return 1;
+ }
  
  static void remove_sequencer_state(void)
  {
@@@ -186,15 -269,14 +269,15 @@@ static int error_dirty_index(struct rep
        return -1;
  }
  
 -static int fast_forward_to(const unsigned char *to, const unsigned char *from)
 +static int fast_forward_to(const unsigned char *to, const unsigned char *from,
 +                         int unborn)
  {
        struct ref_lock *ref_lock;
  
        read_cache();
        if (checkout_fast_forward(from, to, 1))
                exit(1); /* the callee should have complained already */
 -      ref_lock = lock_any_ref_for_update("HEAD", from, 0);
 +      ref_lock = lock_any_ref_for_update("HEAD", unborn ? null_sha1 : from, 0);
        return write_ref_sha1(ref_lock, to, "cherry-pick");
  }
  
@@@ -237,7 -319,7 +320,7 @@@ static int do_recursive_merge(struct co
        rollback_lock_file(&index_lock);
  
        if (opts->signoff)
-               append_signoff(msgbuf, 0);
+               append_signoff(msgbuf, 0, 0);
  
        if (!clean) {
                int i;
@@@ -391,7 -473,7 +474,7 @@@ static int do_pick_commit(struct commi
        struct commit_message msg = { NULL, NULL, NULL, NULL, NULL };
        char *defmsg = NULL;
        struct strbuf msgbuf = STRBUF_INIT;
 -      int res;
 +      int res, unborn = 0;
  
        if (opts->no_commit) {
                /*
                if (write_cache_as_tree(head, 0, NULL))
                        die (_("Your index file is unmerged."));
        } else {
 -              if (get_sha1("HEAD", head))
 -                      return error(_("You do not have a valid HEAD"));
 -              if (index_differs_from("HEAD", 0))
 +              unborn = get_sha1("HEAD", head);
 +              if (unborn)
 +                      hashcpy(head, EMPTY_TREE_SHA1_BIN);
 +              if (index_differs_from(unborn ? EMPTY_TREE_SHA1_HEX : "HEAD", 0))
                        return error_dirty_index(opts);
        }
        discard_cache();
        else
                parent = commit->parents->item;
  
 -      if (opts->allow_ff && parent && !hashcmp(parent->object.sha1, head))
 -              return fast_forward_to(commit->object.sha1, head);
 +      if (opts->allow_ff &&
 +          ((parent && !hashcmp(parent->object.sha1, head)) ||
 +           (!parent && unborn)))
 +           return fast_forward_to(commit->object.sha1, head, unborn);
  
        if (parent && parse_commit(parent) < 0)
                /* TRANSLATORS: The first %s will be "revert" or
                }
  
                if (opts->record_origin) {
-                       strbuf_addstr(&msgbuf, "(cherry picked from commit ");
+                       if (!has_conforming_footer(&msgbuf, NULL, 0))
+                               strbuf_addch(&msgbuf, '\n');
+                       strbuf_addstr(&msgbuf, cherry_picked_prefix);
                        strbuf_addstr(&msgbuf, sha1_to_hex(commit->object.sha1));
                        strbuf_addstr(&msgbuf, ")\n");
                }
@@@ -1021,62 -1102,67 +1106,67 @@@ int sequencer_pick_revisions(struct rep
        return pick_commits(todo_list, opts);
  }
  
- static int ends_rfc2822_footer(struct strbuf *sb, int ignore_footer)
- {
-       int ch;
-       int hit = 0;
-       int i, j, k;
-       int len = sb->len - ignore_footer;
-       int first = 1;
-       const char *buf = sb->buf;
-       for (i = len - 1; i > 0; i--) {
-               if (hit && buf[i] == '\n')
-                       break;
-               hit = (buf[i] == '\n');
-       }
-       while (i < len - 1 && buf[i] == '\n')
-               i++;
-       for (; i < len; i = k) {
-               for (k = i; k < len && buf[k] != '\n'; k++)
-                       ; /* do nothing */
-               k++;
-               if ((buf[k] == ' ' || buf[k] == '\t') && !first)
-                       continue;
-               first = 0;
-               for (j = 0; i + j < len; j++) {
-                       ch = buf[i + j];
-                       if (ch == ':')
-                               break;
-                       if (isalnum(ch) ||
-                           (ch == '-'))
-                               continue;
-                       return 0;
-               }
-       }
-       return 1;
- }
- void append_signoff(struct strbuf *msgbuf, int ignore_footer)
+ void append_signoff(struct strbuf *msgbuf, int ignore_footer, unsigned flag)
  {
+       unsigned no_dup_sob = flag & APPEND_SIGNOFF_DEDUP;
        struct strbuf sob = STRBUF_INIT;
-       int i;
+       int has_footer;
  
        strbuf_addstr(&sob, sign_off_header);
        strbuf_addstr(&sob, fmt_name(getenv("GIT_COMMITTER_NAME"),
                                getenv("GIT_COMMITTER_EMAIL")));
        strbuf_addch(&sob, '\n');
-       for (i = msgbuf->len - 1 - ignore_footer; i > 0 && msgbuf->buf[i - 1] != '\n'; i--)
-               ; /* do nothing */
-       if (prefixcmp(msgbuf->buf + i, sob.buf)) {
-               if (!i || !ends_rfc2822_footer(msgbuf, ignore_footer))
-                       strbuf_splice(msgbuf, msgbuf->len - ignore_footer, 0, "\n", 1);
-               strbuf_splice(msgbuf, msgbuf->len - ignore_footer, 0, sob.buf, sob.len);
+       /*
+        * If the whole message buffer is equal to the sob, pretend that we
+        * found a conforming footer with a matching sob
+        */
+       if (msgbuf->len - ignore_footer == sob.len &&
+           !strncmp(msgbuf->buf, sob.buf, sob.len))
+               has_footer = 3;
+       else
+               has_footer = has_conforming_footer(msgbuf, &sob, ignore_footer);
+       if (!has_footer) {
+               const char *append_newlines = NULL;
+               size_t len = msgbuf->len - ignore_footer;
+               if (!len) {
+                       /*
+                        * The buffer is completely empty.  Leave foom for
+                        * the title and body to be filled in by the user.
+                        */
+                       append_newlines = "\n\n";
+               } else if (msgbuf->buf[len - 1] != '\n') {
+                       /*
+                        * Incomplete line.  Complete the line and add a
+                        * blank one so that there is an empty line between
+                        * the message body and the sob.
+                        */
+                       append_newlines = "\n\n";
+               } else if (len == 1) {
+                       /*
+                        * Buffer contains a single newline.  Add another
+                        * so that we leave room for the title and body.
+                        */
+                       append_newlines = "\n";
+               } else if (msgbuf->buf[len - 2] != '\n') {
+                       /*
+                        * Buffer ends with a single newline.  Add another
+                        * so that there is an empty line between the message
+                        * body and the sob.
+                        */
+                       append_newlines = "\n";
+               } /* else, the buffer already ends with two newlines. */
+               if (append_newlines)
+                       strbuf_splice(msgbuf, msgbuf->len - ignore_footer, 0,
+                               append_newlines, strlen(append_newlines));
        }
+       if (has_footer != 3 && (!no_dup_sob || has_footer != 2))
+               strbuf_splice(msgbuf, msgbuf->len - ignore_footer, 0,
+                               sob.buf, sob.len);
        strbuf_release(&sob);
  }
diff --combined t/t4014-format-patch.sh
index bb1fc47fe8aa6904555c153f90a6026c45bf3645,95af73f596b096fe8de94d4fd027a09d7401badb..b993dae64574cd2828fc636d3afc15b2c0c2e5a0
@@@ -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 &&
@@@ -837,26 -821,25 +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?=
 + =?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?=
  EOF
  test_expect_success 'format-patch wraps extremely long subject (rfc2047)' '
        rm -rf patches/ &&
@@@ -972,54 -955,274 +972,316 @@@ test_expect_success 'empty subject pref
        test_cmp expect actual
  '
  
 -test_expect_success 'format patch ignores color.ui' '
 -      test_unconfig color.ui &&
 -      git format-patch --stdout -1 >expect &&
 -      test_config color.ui always &&
 -      git format-patch --stdout -1 >actual &&
 -      test_cmp expect actual
 -'
 -
+ append_signoff()
+ {
+       C=$(git commit-tree HEAD^^{tree} -p HEAD) &&
+       git format-patch --stdout --signoff $C^..$C >append_signoff.patch &&
+       sed -n -e "1,/^---$/p" append_signoff.patch |
+               egrep -n "^Subject|Sign|^$"
+ }
+ test_expect_success 'signoff: commit with no body' '
+       append_signoff </dev/null >actual &&
+       cat <<\EOF | sed "s/EOL$//" >expected &&
+ 4:Subject: [PATCH] EOL
+ 8:
+ 9:Signed-off-by: C O Mitter <committer@example.com>
+ EOF
+       test_cmp expected actual
+ '
+ test_expect_success 'signoff: commit with only subject' '
+       echo subject | append_signoff >actual &&
+       cat >expected <<\EOF &&
+ 4:Subject: [PATCH] subject
+ 8:
+ 9:Signed-off-by: C O Mitter <committer@example.com>
+ EOF
+       test_cmp expected actual
+ '
+ test_expect_success 'signoff: commit with only subject that does not end with NL' '
+       printf subject | append_signoff >actual &&
+       cat >expected <<\EOF &&
+ 4:Subject: [PATCH] subject
+ 8:
+ 9:Signed-off-by: C O Mitter <committer@example.com>
+ EOF
+       test_cmp expected actual
+ '
+ test_expect_success 'signoff: no existing signoffs' '
+       append_signoff <<\EOF >actual &&
+ subject
+ body
+ EOF
+       cat >expected <<\EOF &&
+ 4:Subject: [PATCH] subject
+ 8:
+ 10:
+ 11:Signed-off-by: C O Mitter <committer@example.com>
+ EOF
+       test_cmp expected actual
+ '
+ test_expect_success 'signoff: no existing signoffs and no trailing NL' '
+       printf "subject\n\nbody" | append_signoff >actual &&
+       cat >expected <<\EOF &&
+ 4:Subject: [PATCH] subject
+ 8:
+ 10:
+ 11:Signed-off-by: C O Mitter <committer@example.com>
+ EOF
+       test_cmp expected actual
+ '
+ test_expect_success 'signoff: some random signoff' '
+       append_signoff <<\EOF >actual &&
+ subject
+ body
+ Signed-off-by: my@house
+ EOF
+       cat >expected <<\EOF &&
+ 4:Subject: [PATCH] subject
+ 8:
+ 10:
+ 11:Signed-off-by: my@house
+ 12:Signed-off-by: C O Mitter <committer@example.com>
+ EOF
+       test_cmp expected actual
+ '
+ test_expect_success 'signoff: misc conforming footer elements' '
+       append_signoff <<\EOF >actual &&
+ subject
+ body
+ Signed-off-by: my@house
+ (cherry picked from commit da39a3ee5e6b4b0d3255bfef95601890afd80709)
+ Tested-by: Some One <someone@example.com>
+ Bug: 1234
+ EOF
+       cat >expected <<\EOF &&
+ 4:Subject: [PATCH] subject
+ 8:
+ 10:
+ 11:Signed-off-by: my@house
+ 15:Signed-off-by: C O Mitter <committer@example.com>
+ EOF
+       test_cmp expected actual
+ '
+ test_expect_success 'signoff: some random signoff-alike' '
+       append_signoff <<\EOF >actual &&
+ subject
+ body
+ Fooled-by-me: my@house
+ EOF
+       cat >expected <<\EOF &&
+ 4:Subject: [PATCH] subject
+ 8:
+ 11:
+ 12:Signed-off-by: C O Mitter <committer@example.com>
+ EOF
+       test_cmp expected actual
+ '
+ test_expect_success 'signoff: not really a signoff' '
+       append_signoff <<\EOF >actual &&
+ subject
+ I want to mention about Signed-off-by: here.
+ EOF
+       cat >expected <<\EOF &&
+ 4:Subject: [PATCH] subject
+ 8:
+ 9:I want to mention about Signed-off-by: here.
+ 10:
+ 11:Signed-off-by: C O Mitter <committer@example.com>
+ EOF
+       test_cmp expected actual
+ '
+ test_expect_success 'signoff: not really a signoff (2)' '
+       append_signoff <<\EOF >actual &&
+ subject
+ My unfortunate
+ Signed-off-by: example happens to be wrapped here.
+ EOF
+       cat >expected <<\EOF &&
+ 4:Subject: [PATCH] subject
+ 8:
+ 10:Signed-off-by: example happens to be wrapped here.
+ 11:
+ 12:Signed-off-by: C O Mitter <committer@example.com>
+ EOF
+       test_cmp expected actual
+ '
+ test_expect_success 'signoff: valid S-o-b paragraph in the middle' '
+       append_signoff <<\EOF >actual &&
+ subject
+ Signed-off-by: my@house
+ Signed-off-by: your@house
+ A lot of houses.
+ EOF
+       cat >expected <<\EOF &&
+ 4:Subject: [PATCH] subject
+ 8:
+ 9:Signed-off-by: my@house
+ 10:Signed-off-by: your@house
+ 11:
+ 13:
+ 14:Signed-off-by: C O Mitter <committer@example.com>
+ EOF
+       test_cmp expected actual
+ '
+ test_expect_success 'signoff: the same signoff at the end' '
+       append_signoff <<\EOF >actual &&
+ subject
+ body
+ Signed-off-by: C O Mitter <committer@example.com>
+ EOF
+       cat >expected <<\EOF &&
+ 4:Subject: [PATCH] subject
+ 8:
+ 10:
+ 11:Signed-off-by: C O Mitter <committer@example.com>
+ EOF
+       test_cmp expected actual
+ '
+ test_expect_success 'signoff: the same signoff at the end, no trailing NL' '
+       printf "subject\n\nSigned-off-by: C O Mitter <committer@example.com>" |
+               append_signoff >actual &&
+       cat >expected <<\EOF &&
+ 4:Subject: [PATCH] subject
+ 8:
+ 9:Signed-off-by: C O Mitter <committer@example.com>
+ EOF
+       test_cmp expected actual
+ '
+ test_expect_success 'signoff: the same signoff NOT at the end' '
+       append_signoff <<\EOF >actual &&
+ subject
+ body
+ Signed-off-by: C O Mitter <committer@example.com>
+ Signed-off-by: my@house
+ EOF
+       cat >expected <<\EOF &&
+ 4:Subject: [PATCH] subject
+ 8:
+ 10:
+ 11:Signed-off-by: C O Mitter <committer@example.com>
+ 12:Signed-off-by: my@house
+ EOF
+       test_cmp expected actual
+ '
+ test_expect_success 'signoff: detect garbage in non-conforming footer' '
+       append_signoff <<\EOF >actual &&
+ subject
+ body
+ Tested-by: my@house
+ Some Trash
+ Signed-off-by: C O Mitter <committer@example.com>
+ EOF
+       cat >expected <<\EOF &&
+ 4:Subject: [PATCH] subject
+ 8:
+ 10:
+ 13:Signed-off-by: C O Mitter <committer@example.com>
+ 14:
+ 15:Signed-off-by: C O Mitter <committer@example.com>
+ EOF
+       test_cmp expected actual
+ '
+ test_expect_success 'signoff: footer begins with non-signoff without @ sign' '
+       append_signoff <<\EOF >actual &&
+ subject
+ body
+ Reviewed-id: Noone
+ Tested-by: my@house
+ Change-id: Ideadbeef
+ Signed-off-by: C O Mitter <committer@example.com>
+ Bug: 1234
+ EOF
+       cat >expected <<\EOF &&
+ 4:Subject: [PATCH] subject
+ 8:
+ 10:
+ 14:Signed-off-by: C O Mitter <committer@example.com>
+ EOF
+       test_cmp expected actual
+ '
 +test_expect_success 'format patch ignores color.ui' '
 +      test_unconfig color.ui &&
 +      git format-patch --stdout -1 >expect &&
 +      test_config color.ui always &&
 +      git format-patch --stdout -1 >actual &&
 +      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 t/t7502-commit.sh
index 256137f07affdf9ed838bfe5a788c3df11821898,c06a7526c127eff62a46a86490bea942eeead2c8..a4938b1e4549d5082362ec3f5513ffe91f210d39
@@@ -4,15 -4,6 +4,15 @@@ test_description='git commit porcelain-
  
  . ./test-lib.sh
  
 +commit_msg_is () {
 +      expect=commit_msg_is.expect
 +      actual=commit_msg_is.actual
 +
 +      printf "%s" "$(git log --pretty=format:%s%b -1)" >$actual &&
 +      printf "%s" "$1" >$expect &&
 +      test_i18ncmp $expect $actual
 +}
 +
  # Arguments: [<prefix] [<commit message>] [<commit options>]
  check_summary_oneline() {
        test_tick &&
@@@ -171,30 -162,23 +171,30 @@@ test_expect_success 'verbose' 
  
  test_expect_success 'verbose respects diff config' '
  
 -      git config color.diff always &&
 +      test_config color.diff always &&
        git status -v >actual &&
 -      grep "\[1mdiff --git" actual &&
 -      git config --unset color.diff
 +      grep "\[1mdiff --git" actual
  '
  
 -test_expect_success 'cleanup commit messages (verbatim,-t)' '
 +mesg_with_comment_and_newlines='
 +# text
 +
 +'
 +
 +test_expect_success 'prepare file with comment line and trailing newlines'  '
 +      printf "%s" "$mesg_with_comment_and_newlines" >expect
 +'
 +
 +test_expect_success 'cleanup commit messages (verbatim option,-t)' '
  
        echo >>negative &&
 -      { echo;echo "# text";echo; } >expect &&
 -      git commit --cleanup=verbatim -t expect -a &&
 -      git cat-file -p HEAD |sed -e "1,/^\$/d" |head -n 3 >actual &&
 +      git commit --cleanup=verbatim --no-status -t expect -a &&
 +      git cat-file -p HEAD |sed -e "1,/^\$/d" >actual &&
        test_cmp expect actual
  
  '
  
 -test_expect_success 'cleanup commit messages (verbatim,-F)' '
 +test_expect_success 'cleanup commit messages (verbatim option,-F)' '
  
        echo >>negative &&
        git commit --cleanup=verbatim -F expect -a &&
  
  '
  
 -test_expect_success 'cleanup commit messages (verbatim,-m)' '
 +test_expect_success 'cleanup commit messages (verbatim option,-m)' '
  
        echo >>negative &&
 -      git commit --cleanup=verbatim -m "$(cat expect)" -a &&
 +      git commit --cleanup=verbatim -m "$mesg_with_comment_and_newlines" -a &&
        git cat-file -p HEAD |sed -e "1,/^\$/d">actual &&
        test_cmp expect actual
  
  '
  
 -test_expect_success 'cleanup commit messages (whitespace,-F)' '
 +test_expect_success 'cleanup commit messages (whitespace option,-F)' '
  
        echo >>negative &&
        { echo;echo "# text";echo; } >text &&
  
  '
  
 -test_expect_success 'cleanup commit messages (strip,-F)' '
 +test_expect_success 'cleanup commit messages (strip option,-F)' '
  
        echo >>negative &&
        { echo;echo "# text";echo sample;echo; } >text &&
  
  '
  
 -test_expect_success 'cleanup commit messages (strip,-F,-e)' '
 +test_expect_success 'cleanup commit messages (strip option,-F,-e)' '
  
        echo >>negative &&
        { echo;echo sample;echo; } >text &&
@@@ -247,79 -231,10 +247,79 @@@ echo "sampl
  # Please enter the commit message for your changes. Lines starting
  # with '#' will be ignored, and an empty message aborts the commit." >expect
  
 -test_expect_success 'cleanup commit messages (strip,-F,-e): output' '
 +test_expect_success 'cleanup commit messages (strip option,-F,-e): output' '
        test_i18ncmp expect actual
  '
  
 +test_expect_success 'cleanup commit message (fail on invalid cleanup mode option)' '
 +      test_must_fail git commit --cleanup=non-existent
 +'
 +
 +test_expect_success 'cleanup commit message (fail on invalid cleanup mode configuration)' '
 +      test_must_fail git -c commit.cleanup=non-existent commit
 +'
 +
 +test_expect_success 'cleanup commit message (no config and no option uses default)' '
 +      echo content >>file &&
 +      git add file &&
 +      (
 +        test_set_editor "$TEST_DIRECTORY"/t7500/add-content-and-comment &&
 +        git commit --no-status
 +      ) &&
 +      commit_msg_is "commit message"
 +'
 +
 +test_expect_success 'cleanup commit message (option overrides default)' '
 +      echo content >>file &&
 +      git add file &&
 +      (
 +        test_set_editor "$TEST_DIRECTORY"/t7500/add-content-and-comment &&
 +        git commit --cleanup=whitespace --no-status
 +      ) &&
 +      commit_msg_is "commit message # comment"
 +'
 +
 +test_expect_success 'cleanup commit message (config overrides default)' '
 +      echo content >>file &&
 +      git add file &&
 +      (
 +        test_set_editor "$TEST_DIRECTORY"/t7500/add-content-and-comment &&
 +        git -c commit.cleanup=whitespace commit --no-status
 +      ) &&
 +      commit_msg_is "commit message # comment"
 +'
 +
 +test_expect_success 'cleanup commit message (option overrides config)' '
 +      echo content >>file &&
 +      git add file &&
 +      (
 +        test_set_editor "$TEST_DIRECTORY"/t7500/add-content-and-comment &&
 +        git -c commit.cleanup=whitespace commit --cleanup=default
 +      ) &&
 +      commit_msg_is "commit message"
 +'
 +
 +test_expect_success 'cleanup commit message (default, -m)' '
 +      echo content >>file &&
 +      git add file &&
 +      git commit -m "message #comment " &&
 +      commit_msg_is "message #comment"
 +'
 +
 +test_expect_success 'cleanup commit message (whitespace option, -m)' '
 +      echo content >>file &&
 +      git add file &&
 +      git commit --cleanup=whitespace --no-status -m "message #comment " &&
 +      commit_msg_is "message #comment"
 +'
 +
 +test_expect_success 'cleanup commit message (whitespace config, -m)' '
 +      echo content >>file &&
 +      git add file &&
 +      git -c commit.cleanup=whitespace commit --no-status -m "message #comment " &&
 +      commit_msg_is "message #comment"
 +'
 +
  test_expect_success 'message shows author when it is not equal to committer' '
        echo >>negative &&
        git commit -e -m "sample" -a &&
          .git/COMMIT_EDITMSG
  '
  
 -test_expect_success 'setup auto-ident prerequisite' '
 -      if (sane_unset GIT_COMMITTER_EMAIL &&
 -          sane_unset GIT_COMMITTER_NAME &&
 -          git var GIT_COMMITTER_IDENT); then
 -              test_set_prereq AUTOIDENT
 -      else
 -              test_set_prereq NOAUTOIDENT
 -      fi
 -'
 -
  test_expect_success AUTOIDENT 'message shows committer when it is automatic' '
  
        echo >>negative &&
@@@ -346,7 -271,7 +346,7 @@@ echo editor started > "$(pwd)/.git/resu
  exit 0
  EOF
  
 -test_expect_success NOAUTOIDENT 'do not fire editor when committer is bogus' '
 +test_expect_success !AUTOIDENT 'do not fire editor when committer is bogus' '
        >.git/result
        >expect &&
  
@@@ -424,6 -349,18 +424,18 @@@ test_expect_success 'A single-liner sub
  
  '
  
+ test_expect_success 'commit -s places sob on third line after two empty lines' '
+       git commit -s --allow-empty --allow-empty-message &&
+       cat <<-EOF >expect &&
+       Signed-off-by: $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL>
+       EOF
+       sed -e "/^#/d" -e "s/^:.*//" .git/COMMIT_EDITMSG >actual &&
+       test_cmp expect actual
+ '
  write_script .git/FAKE_EDITOR <<\EOF
  mv "$1" "$1.orig"
  (
@@@ -434,6 -371,16 +446,6 @@@ EO
  
  echo '## Custom template' >template
  
 -clear_config () {
 -      (
 -              git config --unset-all "$1"
 -              case $? in
 -              0|5)    exit 0 ;;
 -              *)      exit 1 ;;
 -              esac
 -      )
 -}
 -
  try_commit () {
        git reset --hard &&
        echo >>negative &&
  try_commit_status_combo () {
  
        test_expect_success 'commit' '
 -              clear_config commit.status &&
                try_commit "" &&
                test_i18ngrep "^# Changes to be committed:" .git/COMMIT_EDITMSG
        '
  
        test_expect_success 'commit' '
 -              clear_config commit.status &&
                try_commit "" &&
                test_i18ngrep "^# Changes to be committed:" .git/COMMIT_EDITMSG
        '
  
        test_expect_success 'commit --status' '
 -              clear_config commit.status &&
                try_commit --status &&
                test_i18ngrep "^# Changes to be committed:" .git/COMMIT_EDITMSG
        '
  
        test_expect_success 'commit --no-status' '
 -              clear_config commit.status &&
                try_commit --no-status &&
                test_i18ngrep ! "^# Changes to be committed:" .git/COMMIT_EDITMSG
        '
  
        test_expect_success 'commit with commit.status = yes' '
 -              clear_config commit.status &&
 -              git config commit.status yes &&
 +              test_config commit.status yes &&
                try_commit "" &&
                test_i18ngrep "^# Changes to be committed:" .git/COMMIT_EDITMSG
        '
  
        test_expect_success 'commit with commit.status = no' '
 -              clear_config commit.status &&
 -              git config commit.status no &&
 +              test_config commit.status no &&
                try_commit "" &&
                test_i18ngrep ! "^# Changes to be committed:" .git/COMMIT_EDITMSG
        '
  
        test_expect_success 'commit --status with commit.status = yes' '
 -              clear_config commit.status &&
 -              git config commit.status yes &&
 +              test_config commit.status yes &&
                try_commit --status &&
                test_i18ngrep "^# Changes to be committed:" .git/COMMIT_EDITMSG
        '
  
        test_expect_success 'commit --no-status with commit.status = yes' '
 -              clear_config commit.status &&
 -              git config commit.status yes &&
 +              test_config commit.status yes &&
                try_commit --no-status &&
                test_i18ngrep ! "^# Changes to be committed:" .git/COMMIT_EDITMSG
        '
  
        test_expect_success 'commit --status with commit.status = no' '
 -              clear_config commit.status &&
 -              git config commit.status no &&
 +              test_config commit.status no &&
                try_commit --status &&
                test_i18ngrep "^# Changes to be committed:" .git/COMMIT_EDITMSG
        '
  
        test_expect_success 'commit --no-status with commit.status = no' '
 -              clear_config commit.status &&
 -              git config commit.status no &&
 +              test_config commit.status no &&
                try_commit --no-status &&
                test_i18ngrep ! "^# Changes to be committed:" .git/COMMIT_EDITMSG
        '
@@@ -512,10 -469,4 +524,10 @@@ use_template="-t template
  
  try_commit_status_combo
  
 +test_expect_success 'commit --status with custom comment character' '
 +      test_config core.commentchar ";" &&
 +      try_commit --status &&
 +      test_i18ngrep "^; Changes to be committed:" .git/COMMIT_EDITMSG
 +'
 +
  test_done
diff --combined t/test-lib-functions.sh
index fa62d010f68e3ee97e6754687ad4d08564d3c96b,7c4c6335925d6fa1d383e7623ce937969b13cd79..61d0804435d3e8aed2f3b2c0308bc1a9143f1f5a
@@@ -135,12 -135,12 +135,12 @@@ test_pause () 
        fi
  }
  
- # Call test_commit with the arguments "<message> [<file> [<contents>]]"
+ # Call test_commit with the arguments "<message> [<file> [<contents> [<tag>]]]"
  #
  # This will commit a file with the given contents and the given commit
- # message.  It will also add a tag with <message> as name.
+ # message, and tag the resulting commit with the given tag name.
  #
- # Both <file> and <contents> default to <message>.
+ # <file>, <contents>, and <tag> all default to <message>.
  
  test_commit () {
        notick= &&
                test_tick
        fi &&
        git commit $signoff -m "$1" &&
-       git tag "$1"
+       git tag "${4:-$1}"
  }
  
  # Call test_merge with the arguments "<message> <commit>", where <commit>
@@@ -275,15 -275,6 +275,15 @@@ test_have_prereq () 
  
        for prerequisite
        do
 +              case "$prerequisite" in
 +              !*)
 +                      negative_prereq=t
 +                      prerequisite=${prerequisite#!}
 +                      ;;
 +              *)
 +                      negative_prereq=
 +              esac
 +
                case " $lazily_tested_prereq " in
                *" $prerequisite "*)
                        ;;
                total_prereq=$(($total_prereq + 1))
                case "$satisfied_prereq" in
                *" $prerequisite "*)
 +                      satisfied_this_prereq=t
 +                      ;;
 +              *)
 +                      satisfied_this_prereq=
 +              esac
 +
 +              case "$satisfied_this_prereq,$negative_prereq" in
 +              t,|,t)
                        ok_prereq=$(($ok_prereq + 1))
                        ;;
                *)
 -                      # Keep a list of missing prerequisites
 +                      # Keep a list of missing prerequisites; restore
 +                      # the negative marker if necessary.
 +                      prerequisite=${negative_prereq:+!}$prerequisite
                        if test -z "$missing_prereq"
                        then
                                missing_prereq=$prerequisite
@@@ -602,13 -583,6 +602,13 @@@ test_cmp() 
        $GIT_TEST_CMP "$@"
  }
  
 +# Tests that its two parameters refer to the same revision
 +test_cmp_rev () {
 +      git rev-parse --verify "$1" >expect.rev &&
 +      git rev-parse --verify "$2" >actual.rev &&
 +      test_cmp expect.rev actual.rev
 +}
 +
  # Print a sequence of numbers or letters in increasing order.  This is
  # similar to GNU seq(1), but the latter might not be available
  # everywhere (and does not do letters).  It may be used like: