Merge branch 'db/cover-letter'
authorJunio C Hamano <gitster@pobox.com>
Wed, 27 Feb 2008 20:06:41 +0000 (12:06 -0800)
committerJunio C Hamano <gitster@pobox.com>
Wed, 27 Feb 2008 20:06:41 +0000 (12:06 -0800)
* db/cover-letter:
Improve collection of information for format-patch --cover-letter
Add API access to shortlog
t4014: Replace sed's non-standard 'Q' by standard 'q'
Support a --cc=<email> option in format-patch
Combine To: and Cc: headers
Fix format.headers not ending with a newline
Add tests for extra headers in format-patch
Add a --cover-letter option to format-patch
Export some email and pretty-printing functions
Improve message-id generation flow control for format-patch
Add more tests for format-patch

Conflicts:

builtin-log.c
builtin-shortlog.c
pretty.c

12 files changed:
Documentation/git-format-patch.txt
builtin-log.c
builtin-shortlog.c
commit.h
log-tree.c
log-tree.h
pretty.c
revision.h
shortlog.h [new file with mode: 0644]
t/t4013-diff-various.sh
t/t4013/diff.format-patch_--stdout_--cover-letter_-n_initial..master^ [new file with mode: 0644]
t/t4014-format-patch.sh
index 651efe6ca16a02841c49f4b6a57ae2cf5ae8183d..b5207b76040d02e34452175779340658b943a84e 100644 (file)
@@ -10,13 +10,15 @@ SYNOPSIS
 --------
 [verse]
 'git-format-patch' [-k] [-o <dir> | --stdout] [--thread]
-                   [--attach[=<boundary>] | --inline[=<boundary>]]
-                   [-s | --signoff] [<common diff options>]
-                   [-n | --numbered | -N | --no-numbered]
-                   [--start-number <n>] [--numbered-files]
-                   [--in-reply-to=Message-Id] [--suffix=.<sfx>]
-                   [--ignore-if-in-upstream]
-                   [--subject-prefix=Subject-Prefix]
+                  [--attach[=<boundary>] | --inline[=<boundary>]]
+                  [-s | --signoff] [<common diff options>]
+                  [-n | --numbered | -N | --no-numbered]
+                  [--start-number <n>] [--numbered-files]
+                  [--in-reply-to=Message-Id] [--suffix=.<sfx>]
+                  [--ignore-if-in-upstream]
+                  [--subject-prefix=Subject-Prefix]
+                  [--cc=<email>]
+                  [--cover-letter]
                   [ <since> | <revision range> ]
 
 DESCRIPTION
@@ -135,6 +137,15 @@ include::diff-options.txt[]
        allows for useful naming of a patch series, and can be
        combined with the --numbered option.
 
+--cc=<email>::
+       Add a "Cc:" header to the email headers. This is in addition
+       to any configured headers, and may be used multiple times.
+
+--cover-letter::
+       Generate a cover letter template.  You still have to fill in
+       a description, but the shortlog and the diffstat will be
+       generated for you.
+
 --suffix=.<sfx>::
        Instead of using `.patch` as the suffix for generated
        filenames, use specified suffix.  A common alternative is
index c67d63cb1cf588877fa10a7546757f2cd249c5c6..3209ea5c6d61d18dfe885f07b20d11119799b803 100644 (file)
@@ -15,6 +15,8 @@
 #include "reflog-walk.h"
 #include "patch-ids.h"
 #include "refs.h"
+#include "run-command.h"
+#include "shortlog.h"
 
 static int default_show_root = 1;
 static const char *fmt_patch_subject_prefix = "PATCH";
@@ -428,24 +430,47 @@ static int istitlechar(char c)
                (c >= '0' && c <= '9') || c == '.' || c == '_';
 }
 
-static char *extra_headers = NULL;
-static int extra_headers_size = 0;
 static const char *fmt_patch_suffix = ".patch";
 static int numbered = 0;
 static int auto_number = 0;
 
+static char **extra_hdr;
+static int extra_hdr_nr;
+static int extra_hdr_alloc;
+
+static char **extra_to;
+static int extra_to_nr;
+static int extra_to_alloc;
+
+static char **extra_cc;
+static int extra_cc_nr;
+static int extra_cc_alloc;
+
+static void add_header(const char *value)
+{
+       int len = strlen(value);
+       while (value[len - 1] == '\n')
+               len--;
+       if (!strncasecmp(value, "to: ", 4)) {
+               ALLOC_GROW(extra_to, extra_to_nr + 1, extra_to_alloc);
+               extra_to[extra_to_nr++] = xstrndup(value + 4, len - 4);
+               return;
+       }
+       if (!strncasecmp(value, "cc: ", 4)) {
+               ALLOC_GROW(extra_cc, extra_cc_nr + 1, extra_cc_alloc);
+               extra_cc[extra_cc_nr++] = xstrndup(value + 4, len - 4);
+               return;
+       }
+       ALLOC_GROW(extra_hdr, extra_hdr_nr + 1, extra_hdr_alloc);
+       extra_hdr[extra_hdr_nr++] = xstrndup(value, len);
+}
+
 static int git_format_config(const char *var, const char *value)
 {
        if (!strcmp(var, "format.headers")) {
-               int len;
-
                if (!value)
                        die("format.headers without value");
-               len = strlen(value);
-               extra_headers_size += len + 1;
-               extra_headers = xrealloc(extra_headers, extra_headers_size);
-               extra_headers[extra_headers_size - len - 1] = 0;
-               strcat(extra_headers, value);
+               add_header(value);
                return 0;
        }
        if (!strcmp(var, "format.suffix")) {
@@ -470,74 +495,81 @@ static int git_format_config(const char *var, const char *value)
 }
 
 
+static const char *get_oneline_for_filename(struct commit *commit,
+                                           int keep_subject)
+{
+       static char filename[PATH_MAX];
+       char *sol;
+       int len = 0;
+       int suffix_len = strlen(fmt_patch_suffix) + 1;
+
+       sol = strstr(commit->buffer, "\n\n");
+       if (!sol)
+               filename[0] = '\0';
+       else {
+               int j, space = 0;
+
+               sol += 2;
+               /* strip [PATCH] or [PATCH blabla] */
+               if (!keep_subject && !prefixcmp(sol, "[PATCH")) {
+                       char *eos = strchr(sol + 6, ']');
+                       if (eos) {
+                               while (isspace(*eos))
+                                       eos++;
+                               sol = eos;
+                       }
+               }
+
+               for (j = 0;
+                    j < FORMAT_PATCH_NAME_MAX - suffix_len - 5 &&
+                            len < sizeof(filename) - suffix_len &&
+                            sol[j] && sol[j] != '\n';
+                    j++) {
+                       if (istitlechar(sol[j])) {
+                               if (space) {
+                                       filename[len++] = '-';
+                                       space = 0;
+                               }
+                               filename[len++] = sol[j];
+                               if (sol[j] == '.')
+                                       while (sol[j + 1] == '.')
+                                               j++;
+                       } else
+                               space = 1;
+               }
+               while (filename[len - 1] == '.'
+                      || filename[len - 1] == '-')
+                       len--;
+               filename[len] = '\0';
+       }
+       return filename;
+}
+
 static FILE *realstdout = NULL;
 static const char *output_directory = NULL;
 
-static int reopen_stdout(struct commit *commit, int nr, int keep_subject,
-                        int numbered_files)
+static int reopen_stdout(const char *oneline, int nr, int total)
 {
        char filename[PATH_MAX];
-       char *sol;
        int len = 0;
        int suffix_len = strlen(fmt_patch_suffix) + 1;
 
        if (output_directory) {
-               if (strlen(output_directory) >=
+               len = snprintf(filename, sizeof(filename), "%s",
+                               output_directory);
+               if (len >=
                    sizeof(filename) - FORMAT_PATCH_NAME_MAX - suffix_len)
                        return error("name of output directory is too long");
-               strlcpy(filename, output_directory, sizeof(filename) - suffix_len);
-               len = strlen(filename);
                if (filename[len - 1] != '/')
                        filename[len++] = '/';
        }
 
-       if (numbered_files) {
-               sprintf(filename + len, "%d", nr);
-               len = strlen(filename);
-
-       } else {
-               sprintf(filename + len, "%04d", nr);
-               len = strlen(filename);
-
-               sol = strstr(commit->buffer, "\n\n");
-               if (sol) {
-                       int j, space = 1;
-
-                       sol += 2;
-                       /* strip [PATCH] or [PATCH blabla] */
-                       if (!keep_subject && !prefixcmp(sol, "[PATCH")) {
-                               char *eos = strchr(sol + 6, ']');
-                               if (eos) {
-                                       while (isspace(*eos))
-                                               eos++;
-                                       sol = eos;
-                               }
-                       }
-
-                       for (j = 0;
-                            j < FORMAT_PATCH_NAME_MAX - suffix_len - 5 &&
-                                    len < sizeof(filename) - suffix_len &&
-                                    sol[j] && sol[j] != '\n';
-                            j++) {
-                               if (istitlechar(sol[j])) {
-                                       if (space) {
-                                               filename[len++] = '-';
-                                               space = 0;
-                                       }
-                                       filename[len++] = sol[j];
-                                       if (sol[j] == '.')
-                                               while (sol[j + 1] == '.')
-                                                       j++;
-                               } else
-                                       space = 1;
-                       }
-                       while (filename[len - 1] == '.'
-                              || filename[len - 1] == '-')
-                               len--;
-                       filename[len] = 0;
-               }
-               if (len + suffix_len >= sizeof(filename))
-                       return error("Patch pathname too long");
+       if (!oneline)
+               len += sprintf(filename + len, "%d", nr);
+       else {
+               len += sprintf(filename + len, "%04d-", nr);
+               len += snprintf(filename + len, sizeof(filename) - len - 1
+                               - suffix_len, "%s", oneline);
                strcpy(filename + len, fmt_patch_suffix);
        }
 
@@ -594,16 +626,89 @@ static void get_patch_ids(struct rev_info *rev, struct patch_ids *ids, const cha
        o2->flags = flags2;
 }
 
-static void gen_message_id(char *dest, unsigned int length, char *base)
+static void gen_message_id(struct rev_info *info, char *base)
 {
        const char *committer = git_committer_info(IDENT_WARN_ON_NO_NAME);
        const char *email_start = strrchr(committer, '<');
        const char *email_end = strrchr(committer, '>');
-       if(!email_start || !email_end || email_start > email_end - 1)
+       struct strbuf buf;
+       if (!email_start || !email_end || email_start > email_end - 1)
                die("Could not extract email from committer identity.");
-       snprintf(dest, length, "%s.%lu.git.%.*s", base,
-                (unsigned long) time(NULL),
-                (int)(email_end - email_start - 1), email_start + 1);
+       strbuf_init(&buf, 0);
+       strbuf_addf(&buf, "%s.%lu.git.%.*s", base,
+                   (unsigned long) time(NULL),
+                   (int)(email_end - email_start - 1), email_start + 1);
+       info->message_id = strbuf_detach(&buf, NULL);
+}
+
+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 *committer;
+       const char *origin_sha1, *head_sha1;
+       const char *argv[7];
+       const char *subject_start = NULL;
+       const char *body = "*** SUBJECT HERE ***\n\n*** BLURB HERE ***\n";
+       const char *msg;
+       const char *extra_headers = rev->extra_headers;
+       struct shortlog log;
+       struct strbuf sb;
+       int i;
+       const char *encoding = "utf-8";
+
+       if (rev->commit_format != CMIT_FMT_EMAIL)
+               die("Cover letter needs email format");
+
+       if (!use_stdout && reopen_stdout(numbered_files ?
+                               NULL : "cover-letter", 0, rev->total))
+               return;
+
+       head_sha1 = sha1_to_hex(head->object.sha1);
+
+       log_write_email_headers(rev, head_sha1, &subject_start, &extra_headers);
+
+       committer = git_committer_info(0);
+
+       msg = body;
+       strbuf_init(&sb, 0);
+       pp_user_info(NULL, CMIT_FMT_EMAIL, &sb, committer, DATE_RFC2822,
+                    encoding);
+       pp_title_line(CMIT_FMT_EMAIL, &msg, &sb, subject_start, extra_headers,
+                     encoding, 0);
+       pp_remainder(CMIT_FMT_EMAIL, &msg, &sb, 0);
+       printf("%s\n", sb.buf);
+
+       strbuf_release(&sb);
+
+       shortlog_init(&log);
+       for (i = 0; i < nr; i++)
+               shortlog_add_commit(&log, list[i]);
+
+       shortlog_output(&log);
+
+       /*
+        * We can only do diffstat with a unique reference point
+        */
+       if (!origin)
+               return;
+
+       origin_sha1 = sha1_to_hex(origin->object.sha1);
+
+       argv[0] = "diff";
+       argv[1] = "--stat";
+       argv[2] = "--summary";
+       argv[3] = head_sha1;
+       argv[4] = "--not";
+       argv[5] = origin_sha1;
+       argv[6] = "--";
+       argv[7] = NULL;
+       fflush(stdout);
+       run_command_v_opt(argv, RUN_GIT_CMD);
+
+       fflush(stdout);
+       printf("\n");
 }
 
 static const char *clean_message_id(const char *msg_id)
@@ -641,11 +746,13 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)
        int subject_prefix = 0;
        int ignore_if_in_upstream = 0;
        int thread = 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;
-       char message_id[1024];
-       char ref_message_id[1024];
+       struct strbuf buf;
 
        git_config(git_format_config);
        init_revisions(&rev, prefix);
@@ -658,7 +765,6 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)
        DIFF_OPT_SET(&rev.diffopt, RECURSIVE);
 
        rev.subject_prefix = fmt_patch_subject_prefix;
-       rev.extra_headers = extra_headers;
 
        /*
         * Parse the arguments before setup_revisions(), or something
@@ -686,6 +792,10 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)
                                die("Need a number for --start-number");
                        start_number = strtol(argv[i], NULL, 10);
                }
+               else if (!prefixcmp(argv[i], "--cc=")) {
+                       ALLOC_GROW(extra_cc, extra_cc_nr + 1, extra_cc_alloc);
+                       extra_cc[extra_cc_nr++] = xstrdup(argv[i] + 5);
+               }
                else if (!strcmp(argv[i], "-k") ||
                                !strcmp(argv[i], "--keep-subject")) {
                        keep_subject = 1;
@@ -742,11 +852,44 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)
                        rev.subject_prefix = argv[i] + 17;
                } else if (!prefixcmp(argv[i], "--suffix="))
                        fmt_patch_suffix = argv[i] + 9;
+               else if (!strcmp(argv[i], "--cover-letter"))
+                       cover_letter = 1;
                else
                        argv[j++] = argv[i];
        }
        argc = j;
 
+       strbuf_init(&buf, 0);
+
+       for (i = 0; i < extra_hdr_nr; i++) {
+               strbuf_addstr(&buf, extra_hdr[i]);
+               strbuf_addch(&buf, '\n');
+       }
+
+       if (extra_to_nr)
+               strbuf_addstr(&buf, "To: ");
+       for (i = 0; i < extra_to_nr; i++) {
+               if (i)
+                       strbuf_addstr(&buf, "    ");
+               strbuf_addstr(&buf, extra_to[i]);
+               if (i + 1 < extra_to_nr)
+                       strbuf_addch(&buf, ',');
+               strbuf_addch(&buf, '\n');
+       }
+
+       if (extra_cc_nr)
+               strbuf_addstr(&buf, "Cc: ");
+       for (i = 0; i < extra_cc_nr; i++) {
+               if (i)
+                       strbuf_addstr(&buf, "    ");
+               strbuf_addstr(&buf, extra_cc[i]);
+               if (i + 1 < extra_cc_nr)
+                       strbuf_addch(&buf, ',');
+               strbuf_addch(&buf, '\n');
+       }
+
+       rev.extra_headers = strbuf_detach(&buf, 0);
+
        if (start_number < 0)
                start_number = 1;
        if (numbered && keep_subject)
@@ -793,6 +936,18 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)
                 * get_revision() to do the usual traversal.
                 */
        }
+       if (cover_letter) {
+               /* remember the range */
+               int i;
+               for (i = 0; i < rev.pending.nr; i++) {
+                       struct object *o = rev.pending.objects[i].item;
+                       if (!(o->flags & UNINTERESTING))
+                               head = (struct commit *)o;
+               }
+               /* We can't generate a cover letter without any patches */
+               if (!head)
+                       return 0;
+       }
 
        if (ignore_if_in_upstream)
                get_patch_ids(&rev, &ids, prefix);
@@ -802,7 +957,15 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)
 
        if (prepare_revision_walk(&rev))
                die("revision walk setup failed");
+       rev.boundary = 1;
        while ((commit = get_revision(&rev)) != NULL) {
+               if (commit->object.flags & BOUNDARY) {
+                       fprintf(stderr, "Boundary %s\n", sha1_to_hex(commit->object.sha1));
+                       boundary_count++;
+                       origin = (boundary_count == 1) ? commit : NULL;
+                       continue;
+               }
+
                /* ignore merges */
                if (commit->parents && commit->parents->next)
                        continue;
@@ -820,29 +983,42 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)
                numbered = 1;
        if (numbered)
                rev.total = total + start_number - 1;
-       rev.add_signoff = add_signoff;
        if (in_reply_to)
                rev.ref_message_id = clean_message_id(in_reply_to);
+       if (cover_letter) {
+               if (thread)
+                       gen_message_id(&rev, "cover");
+               make_cover_letter(&rev, use_stdout, numbered, numbered_files,
+                                 origin, nr, list, head);
+               total++;
+               start_number--;
+       }
+       rev.add_signoff = add_signoff;
        while (0 <= --nr) {
                int shown;
                commit = list[nr];
                rev.nr = total - nr + (start_number - 1);
                /* Make the second and subsequent mails replies to the first */
                if (thread) {
-                       if (nr == (total - 2)) {
-                               strncpy(ref_message_id, message_id,
-                                       sizeof(ref_message_id));
-                               ref_message_id[sizeof(ref_message_id)-1]='\0';
-                               rev.ref_message_id = ref_message_id;
+                       /* Have we already had a message ID? */
+                       if (rev.message_id) {
+                               /*
+                                * If we've got the ID to be a reply
+                                * to, discard the current ID;
+                                * otherwise, make everything a reply
+                                * to that.
+                                */
+                               if (rev.ref_message_id)
+                                       free(rev.message_id);
+                               else
+                                       rev.ref_message_id = rev.message_id;
                        }
-                       gen_message_id(message_id, sizeof(message_id),
-                                      sha1_to_hex(commit->object.sha1));
-                       rev.message_id = message_id;
+                       gen_message_id(&rev, sha1_to_hex(commit->object.sha1));
                }
-               if (!use_stdout)
-                       if (reopen_stdout(commit, rev.nr, keep_subject,
-                                         numbered_files))
-                               die("Failed to create output files");
+               if (!use_stdout && reopen_stdout(numbered_files ? NULL :
+                               get_oneline_for_filename(commit, keep_subject),
+                               rev.nr, rev.total))
+                       die("Failed to create output files");
                shown = log_tree_commit(&rev, commit);
                free(commit->buffer);
                commit->buffer = NULL;
index 0055a57aeb24392de0dcf337bb93f30fceccd88f..af31abaaf862bb85a72e8052cf28a874d34c714a 100644 (file)
@@ -6,13 +6,11 @@
 #include "revision.h"
 #include "utf8.h"
 #include "mailmap.h"
+#include "shortlog.h"
 
 static const char shortlog_usage[] =
 "git-shortlog [-n] [-s] [-e] [<commit-id>... ]";
 
-static char *common_repo_prefix;
-static int email;
-
 static int compare_by_number(const void *a1, const void *a2)
 {
        const struct path_list_item *i1 = a1, *i2 = a2;
@@ -26,13 +24,11 @@ static int compare_by_number(const void *a1, const void *a2)
                return -1;
 }
 
-static struct path_list mailmap = {NULL, 0, 0, 0};
-
-static void insert_one_record(struct path_list *list,
+static void insert_one_record(struct shortlog *log,
                              const char *author,
                              const char *oneline)
 {
-       const char *dot3 = common_repo_prefix;
+       const char *dot3 = log->common_repo_prefix;
        char *buffer, *p;
        struct path_list_item *item;
        struct path_list *onelines;
@@ -47,7 +43,7 @@ static void insert_one_record(struct path_list *list,
        eoemail = strchr(boemail, '>');
        if (!eoemail)
                return;
-       if (!map_email(&mailmap, boemail+1, namebuf, sizeof(namebuf))) {
+       if (!map_email(&log->mailmap, boemail+1, namebuf, sizeof(namebuf))) {
                while (author < boemail && isspace(*author))
                        author++;
                for (len = 0;
@@ -61,14 +57,14 @@ static void insert_one_record(struct path_list *list,
        else
                len = strlen(namebuf);
 
-       if (email) {
+       if (log->email) {
                size_t room = sizeof(namebuf) - len - 1;
                int maillen = eoemail - boemail + 1;
                snprintf(namebuf + len, room, " %.*s", maillen, boemail);
        }
 
        buffer = xstrdup(namebuf);
-       item = path_list_insert(buffer, list);
+       item = path_list_insert(buffer, &log->list);
        if (item->util == NULL)
                item->util = xcalloc(1, sizeof(struct path_list));
        else
@@ -114,7 +110,7 @@ static void insert_one_record(struct path_list *list,
        onelines->items[onelines->nr++].path = buffer;
 }
 
-static void read_from_stdin(struct path_list *list)
+static void read_from_stdin(struct shortlog *log)
 {
        char author[1024], oneline[1024];
 
@@ -128,39 +124,43 @@ static void read_from_stdin(struct path_list *list)
                while (fgets(oneline, sizeof(oneline), stdin) &&
                       oneline[0] == '\n')
                        ; /* discard blanks */
-               insert_one_record(list, author + 8, oneline);
+               insert_one_record(log, author + 8, oneline);
        }
 }
 
-static void get_from_rev(struct rev_info *rev, struct path_list *list)
+void shortlog_add_commit(struct shortlog *log, struct commit *commit)
 {
-       struct commit *commit;
-
-       if (prepare_revision_walk(rev))
-               die("revision walk setup failed");
-       while ((commit = get_revision(rev)) != NULL) {
-               const char *author = NULL, *buffer;
+       const char *author = NULL, *buffer;
 
-               buffer = commit->buffer;
-               while (*buffer && *buffer != '\n') {
-                       const char *eol = strchr(buffer, '\n');
+       buffer = commit->buffer;
+       while (*buffer && *buffer != '\n') {
+               const char *eol = strchr(buffer, '\n');
 
-                       if (eol == NULL)
-                               eol = buffer + strlen(buffer);
-                       else
-                               eol++;
+               if (eol == NULL)
+                       eol = buffer + strlen(buffer);
+               else
+                       eol++;
 
-                       if (!prefixcmp(buffer, "author "))
-                               author = buffer + 7;
-                       buffer = eol;
-               }
-               if (!author)
-                       die("Missing author: %s",
-                           sha1_to_hex(commit->object.sha1));
-               if (*buffer)
-                       buffer++;
-               insert_one_record(list, author, !*buffer ? "<none>" : buffer);
+               if (!prefixcmp(buffer, "author "))
+                       author = buffer + 7;
+               buffer = eol;
        }
+       if (!author)
+               die("Missing author: %s",
+                   sha1_to_hex(commit->object.sha1));
+       if (*buffer)
+               buffer++;
+       insert_one_record(log, author, !*buffer ? "<none>" : buffer);
+}
+
+static void get_from_rev(struct rev_info *rev, struct shortlog *log)
+{
+       struct commit *commit;
+
+       if (prepare_revision_walk(rev))
+               die("revision walk setup failed");
+       while ((commit = get_revision(rev)) != NULL)
+               shortlog_add_commit(log, commit);
 }
 
 static int parse_uint(char const **arg, int comma)
@@ -212,29 +212,38 @@ static void parse_wrap_args(const char *arg, int *in1, int *in2, int *wrap)
                die(wrap_arg_usage);
 }
 
+void shortlog_init(struct shortlog *log)
+{
+       memset(log, 0, sizeof(*log));
+
+       read_mailmap(&log->mailmap, ".mailmap", &log->common_repo_prefix);
+
+       log->list.strdup_paths = 1;
+       log->wrap = DEFAULT_WRAPLEN;
+       log->in1 = DEFAULT_INDENT1;
+       log->in2 = DEFAULT_INDENT2;
+}
+
 int cmd_shortlog(int argc, const char **argv, const char *prefix)
 {
+       struct shortlog log;
        struct rev_info rev;
-       struct path_list list = { NULL, 0, 0, 1 };
-       int i, j, sort_by_number = 0, summary = 0;
-       int wrap_lines = 0;
-       int wrap = DEFAULT_WRAPLEN;
-       int in1 = DEFAULT_INDENT1;
-       int in2 = DEFAULT_INDENT2;
+
+       shortlog_init(&log);
 
        /* since -n is a shadowed rev argument, parse our args first */
        while (argc > 1) {
                if (!strcmp(argv[1], "-n") || !strcmp(argv[1], "--numbered"))
-                       sort_by_number = 1;
+                       log.sort_by_number = 1;
                else if (!strcmp(argv[1], "-s") ||
                                !strcmp(argv[1], "--summary"))
-                       summary = 1;
+                       log.summary = 1;
                else if (!strcmp(argv[1], "-e") ||
                         !strcmp(argv[1], "--email"))
-                       email = 1;
+                       log.email = 1;
                else if (!prefixcmp(argv[1], "-w")) {
-                       wrap_lines = 1;
-                       parse_wrap_args(argv[1], &in1, &in2, &wrap);
+                       log.wrap_lines = 1;
+                       parse_wrap_args(argv[1], &log.in1, &log.in2, &log.wrap);
                }
                else if (!strcmp(argv[1], "-h") || !strcmp(argv[1], "--help"))
                        usage(shortlog_usage);
@@ -248,34 +257,38 @@ int cmd_shortlog(int argc, const char **argv, const char *prefix)
        if (argc > 1)
                die ("unrecognized argument: %s", argv[1]);
 
-       read_mailmap(&mailmap, ".mailmap", &common_repo_prefix);
-
        /* assume HEAD if from a tty */
        if (!rev.pending.nr && isatty(0))
                add_head_to_pending(&rev);
        if (rev.pending.nr == 0) {
-               read_from_stdin(&list);
+               read_from_stdin(&log);
        }
        else
-               get_from_rev(&rev, &list);
+               get_from_rev(&rev, &log);
 
-       if (sort_by_number)
-               qsort(list.items, list.nr, sizeof(struct path_list_item),
-                       compare_by_number);
+       shortlog_output(&log);
+       return 0;
+}
 
-       for (i = 0; i < list.nr; i++) {
-               struct path_list *onelines = list.items[i].util;
+void shortlog_output(struct shortlog *log)
+{
+       int i, j;
+       if (log->sort_by_number)
+               qsort(log->list.items, log->list.nr, sizeof(struct path_list_item),
+                       compare_by_number);
+       for (i = 0; i < log->list.nr; i++) {
+               struct path_list *onelines = log->list.items[i].util;
 
-               if (summary) {
-                       printf("%6d\t%s\n", onelines->nr, list.items[i].path);
+               if (log->summary) {
+                       printf("%6d\t%s\n", onelines->nr, log->list.items[i].path);
                } else {
-                       printf("%s (%d):\n", list.items[i].path, onelines->nr);
+                       printf("%s (%d):\n", log->list.items[i].path, onelines->nr);
                        for (j = onelines->nr - 1; j >= 0; j--) {
                                const char *msg = onelines->items[j].path;
 
-                               if (wrap_lines) {
-                                       int col = print_wrapped_text(msg, in1, in2, wrap);
-                                       if (col != wrap)
+                               if (log->wrap_lines) {
+                                       int col = print_wrapped_text(msg, log->in1, log->in2, log->wrap);
+                                       if (col != log->wrap)
                                                putchar('\n');
                                }
                                else
@@ -287,13 +300,11 @@ int cmd_shortlog(int argc, const char **argv, const char *prefix)
                onelines->strdup_paths = 1;
                path_list_clear(onelines, 1);
                free(onelines);
-               list.items[i].util = NULL;
+               log->list.items[i].util = NULL;
        }
 
-       list.strdup_paths = 1;
-       path_list_clear(&list, 1);
-       mailmap.strdup_paths = 1;
-       path_list_clear(&mailmap, 1);
-
-       return 0;
+       log->list.strdup_paths = 1;
+       path_list_clear(&log->list, 1);
+       log->mailmap.strdup_paths = 1;
+       path_list_clear(&log->mailmap, 1);
 }
index 10e2b5d4cfdc7ac129ead711421ccc51d2667f02..80d65b96dafe33b128e4444d81046360eeb105af 100644 (file)
--- a/commit.h
+++ b/commit.h
@@ -71,6 +71,21 @@ extern void pretty_print_commit(enum cmit_fmt fmt, const struct commit*,
                                 int abbrev, const char *subject,
                                 const char *after_subject, enum date_mode,
                                int non_ascii_present);
+void pp_user_info(const char *what, enum cmit_fmt fmt, struct strbuf *sb,
+                  const char *line, enum date_mode dmode,
+                  const char *encoding);
+void pp_title_line(enum cmit_fmt fmt,
+                  const char **msg_p,
+                  struct strbuf *sb,
+                  const char *subject,
+                  const char *after_subject,
+                  const char *encoding,
+                  int plain_non_ascii);
+void pp_remainder(enum cmit_fmt fmt,
+                 const char **msg_p,
+                 struct strbuf *sb,
+                 int indent);
+
 
 /** Removes the first commit from a list sorted by date, and adds all
  * of its parents.
index e9ba6df9d230e8adc12df32b502729c09c8e7d5a..608f697cf3860245510bff907a643d9b6143e724 100644 (file)
@@ -137,6 +137,72 @@ static int has_non_ascii(const char *s)
        return 0;
 }
 
+void log_write_email_headers(struct rev_info *opt, const char *name,
+                            const char **subject_p, const char **extra_headers_p)
+{
+       const char *subject = NULL;
+       const char *extra_headers = opt->extra_headers;
+       if (opt->total > 0) {
+               static char buffer[64];
+               snprintf(buffer, sizeof(buffer),
+                        "Subject: [%s %0*d/%d] ",
+                        opt->subject_prefix,
+                        digits_in_number(opt->total),
+                        opt->nr, opt->total);
+               subject = buffer;
+       } else if (opt->total == 0 && opt->subject_prefix && *opt->subject_prefix) {
+               static char buffer[256];
+               snprintf(buffer, sizeof(buffer),
+                        "Subject: [%s] ",
+                        opt->subject_prefix);
+               subject = buffer;
+       } else {
+               subject = "Subject: ";
+       }
+
+       printf("From %s Mon Sep 17 00:00:00 2001\n", name);
+       if (opt->message_id)
+               printf("Message-Id: <%s>\n", opt->message_id);
+       if (opt->ref_message_id)
+               printf("In-Reply-To: <%s>\nReferences: <%s>\n",
+                      opt->ref_message_id, opt->ref_message_id);
+       if (opt->mime_boundary) {
+               static char subject_buffer[1024];
+               static char buffer[1024];
+               snprintf(subject_buffer, sizeof(subject_buffer) - 1,
+                        "%s"
+                        "MIME-Version: 1.0\n"
+                        "Content-Type: multipart/mixed;"
+                        " boundary=\"%s%s\"\n"
+                        "\n"
+                        "This is a multi-part message in MIME "
+                        "format.\n"
+                        "--%s%s\n"
+                        "Content-Type: text/plain; "
+                        "charset=UTF-8; format=fixed\n"
+                        "Content-Transfer-Encoding: 8bit\n\n",
+                        extra_headers ? extra_headers : "",
+                        mime_boundary_leader, opt->mime_boundary,
+                        mime_boundary_leader, opt->mime_boundary);
+               extra_headers = subject_buffer;
+
+               snprintf(buffer, sizeof(buffer) - 1,
+                        "--%s%s\n"
+                        "Content-Type: text/x-patch;"
+                        " name=\"%s.diff\"\n"
+                        "Content-Transfer-Encoding: 8bit\n"
+                        "Content-Disposition: %s;"
+                        " filename=\"%s.diff\"\n\n",
+                        mime_boundary_leader, opt->mime_boundary,
+                        name,
+                        opt->no_inline ? "attachment" : "inline",
+                        name);
+               opt->diffopt.stat_sep = buffer;
+       }
+       *subject_p = subject;
+       *extra_headers_p = extra_headers;
+}
+
 void show_log(struct rev_info *opt, const char *sep)
 {
        struct strbuf msgbuf;
@@ -188,64 +254,8 @@ void show_log(struct rev_info *opt, const char *sep)
         */
 
        if (opt->commit_format == CMIT_FMT_EMAIL) {
-               char *sha1 = sha1_to_hex(commit->object.sha1);
-               if (opt->total > 0) {
-                       static char buffer[64];
-                       snprintf(buffer, sizeof(buffer),
-                                       "Subject: [%s %0*d/%d] ",
-                                       opt->subject_prefix,
-                                       digits_in_number(opt->total),
-                                       opt->nr, opt->total);
-                       subject = buffer;
-               } else if (opt->total == 0 && opt->subject_prefix && *opt->subject_prefix) {
-                       static char buffer[256];
-                       snprintf(buffer, sizeof(buffer),
-                                       "Subject: [%s] ",
-                                       opt->subject_prefix);
-                       subject = buffer;
-               } else {
-                       subject = "Subject: ";
-               }
-
-               printf("From %s Mon Sep 17 00:00:00 2001\n", sha1);
-               if (opt->message_id)
-                       printf("Message-Id: <%s>\n", opt->message_id);
-               if (opt->ref_message_id)
-                       printf("In-Reply-To: <%s>\nReferences: <%s>\n",
-                              opt->ref_message_id, opt->ref_message_id);
-               if (opt->mime_boundary) {
-                       static char subject_buffer[1024];
-                       static char buffer[1024];
-                       snprintf(subject_buffer, sizeof(subject_buffer) - 1,
-                                "%s"
-                                "MIME-Version: 1.0\n"
-                                "Content-Type: multipart/mixed;"
-                                " boundary=\"%s%s\"\n"
-                                "\n"
-                                "This is a multi-part message in MIME "
-                                "format.\n"
-                                "--%s%s\n"
-                                "Content-Type: text/plain; "
-                                "charset=UTF-8; format=fixed\n"
-                                "Content-Transfer-Encoding: 8bit\n\n",
-                                extra_headers ? extra_headers : "",
-                                mime_boundary_leader, opt->mime_boundary,
-                                mime_boundary_leader, opt->mime_boundary);
-                       extra_headers = subject_buffer;
-
-                       snprintf(buffer, sizeof(buffer) - 1,
-                                "--%s%s\n"
-                                "Content-Type: text/x-patch;"
-                                " name=\"%s.diff\"\n"
-                                "Content-Transfer-Encoding: 8bit\n"
-                                "Content-Disposition: %s;"
-                                " filename=\"%s.diff\"\n\n",
-                                mime_boundary_leader, opt->mime_boundary,
-                                sha1,
-                                opt->no_inline ? "attachment" : "inline",
-                                sha1);
-                       opt->diffopt.stat_sep = buffer;
-               }
+               log_write_email_headers(opt, sha1_to_hex(commit->object.sha1),
+                                       &subject, &extra_headers);
        } else if (opt->commit_format != CMIT_FMT_USERFORMAT) {
                fputs(diff_get_color_opt(&opt->diffopt, DIFF_COMMIT), stdout);
                if (opt->commit_format != CMIT_FMT_ONELINE)
index b33f7cd7ac2ef6a2587109c4ee618d63ccedae96..0cc9344eabdd0046edf360958376b2037ef643dc 100644 (file)
@@ -13,5 +13,7 @@ int log_tree_commit(struct rev_info *, struct commit *);
 int log_tree_opt_parse(struct rev_info *, const char **, int);
 void show_log(struct rev_info *opt, const char *sep);
 void show_decorations(struct commit *commit);
+void log_write_email_headers(struct rev_info *opt, const char *name,
+                            const char **subject_p, const char **extra_headers_p);
 
 #endif
index 997f5837d521ded5f158d12abc0c025f74e83341..a5b2d8c6048c78e360b3526c247b9b272bb3ffc3 100644 (file)
--- a/pretty.c
+++ b/pretty.c
@@ -110,9 +110,9 @@ static void add_rfc2047(struct strbuf *sb, const char *line, int len,
        strbuf_addstr(sb, "?=");
 }
 
-static void add_user_info(const char *what, enum cmit_fmt fmt, struct strbuf *sb,
-                        const char *line, enum date_mode dmode,
-                        const char *encoding)
+void pp_user_info(const char *what, enum cmit_fmt fmt, struct strbuf *sb,
+                 const char *line, enum date_mode dmode,
+                 const char *encoding)
 {
        char *date;
        int namelen;
@@ -621,23 +621,23 @@ static void pp_header(enum cmit_fmt fmt,
                 */
                if (!memcmp(line, "author ", 7)) {
                        strbuf_grow(sb, linelen + 80);
-                       add_user_info("Author", fmt, sb, line + 7, dmode, encoding);
+                       pp_user_info("Author", fmt, sb, line + 7, dmode, encoding);
                }
                if (!memcmp(line, "committer ", 10) &&
                    (fmt == CMIT_FMT_FULL || fmt == CMIT_FMT_FULLER)) {
                        strbuf_grow(sb, linelen + 80);
-                       add_user_info("Commit", fmt, sb, line + 10, dmode, encoding);
+                       pp_user_info("Commit", fmt, sb, line + 10, dmode, encoding);
                }
        }
 }
 
-static void pp_title_line(enum cmit_fmt fmt,
-                         const char **msg_p,
-                         struct strbuf *sb,
-                         const char *subject,
-                         const char *after_subject,
-                         const char *encoding,
-                         int plain_non_ascii)
+void pp_title_line(enum cmit_fmt fmt,
+                  const char **msg_p,
+                  struct strbuf *sb,
+                  const char *subject,
+                  const char *after_subject,
+                  const char *encoding,
+                  int plain_non_ascii)
 {
        struct strbuf title;
 
@@ -686,10 +686,10 @@ static void pp_title_line(enum cmit_fmt fmt,
        strbuf_release(&title);
 }
 
-static void pp_remainder(enum cmit_fmt fmt,
-                        const char **msg_p,
-                        struct strbuf *sb,
-                        int indent)
+void pp_remainder(enum cmit_fmt fmt,
+                 const char **msg_p,
+                 struct strbuf *sb,
+                 int indent)
 {
        int first = 1;
        for (;;) {
index b5f01f8309b6630be7169eb72f613e5deb0ebcab..c8b3b948ecc1cc8b45859a6e1ea11763addfb8b5 100644 (file)
@@ -75,7 +75,7 @@ struct rev_info {
        struct log_info *loginfo;
        int             nr, total;
        const char      *mime_boundary;
-       const char      *message_id;
+       char            *message_id;
        const char      *ref_message_id;
        const char      *add_signoff;
        const char      *extra_headers;
diff --git a/shortlog.h b/shortlog.h
new file mode 100644 (file)
index 0000000..31ff491
--- /dev/null
@@ -0,0 +1,26 @@
+#ifndef SHORTLOG_H
+#define SHORTLOG_H
+
+#include "path-list.h"
+
+struct shortlog {
+       struct path_list list;
+       int summary;
+       int wrap_lines;
+       int sort_by_number;
+       int wrap;
+       int in1;
+       int in2;
+
+       char *common_repo_prefix;
+       int email;
+       struct path_list mailmap;
+};
+
+void shortlog_init(struct shortlog *log);
+
+void shortlog_add_commit(struct shortlog *log, struct commit *commit);
+
+void shortlog_output(struct shortlog *log);
+
+#endif
index 9eec754221d85856613b01ec878ef4cb492aceb0..6b4d1c52bbf1e3edf01dd67794bf221effc581a7 100755 (executable)
@@ -245,6 +245,7 @@ format-patch --inline --stdout initial..master
 format-patch --inline --stdout --subject-prefix=TESTCASE initial..master
 config format.subjectprefix DIFFERENT_PREFIX
 format-patch --inline --stdout initial..master^^
+format-patch --stdout --cover-letter -n initial..master^
 
 diff --abbrev initial..side
 diff -r initial..side
diff --git a/t/t4013/diff.format-patch_--stdout_--cover-letter_-n_initial..master^ b/t/t4013/diff.format-patch_--stdout_--cover-letter_-n_initial..master^
new file mode 100644 (file)
index 0000000..0151453
--- /dev/null
@@ -0,0 +1,100 @@
+$ git format-patch --stdout --cover-letter -n initial..master^
+From 9a6d4949b6b76956d9d5e26f2791ec2ceff5fdc0 Mon Sep 17 00:00:00 2001
+From: C O Mitter <committer@example.com>
+Date: Mon, 26 Jun 2006 00:05:00 +0000
+Subject: [DIFFERENT_PREFIX 0/2] *** SUBJECT HERE ***
+
+*** BLURB HERE ***
+
+A U Thor (2):
+      Second
+      Third
+
+ dir/sub |    4 ++++
+ file0   |    3 +++
+ file1   |    3 +++
+ file2   |    3 ---
+ 4 files changed, 10 insertions(+), 3 deletions(-)
+ create mode 100644 file1
+ delete mode 100644 file2
+
+From 1bde4ae5f36c8d9abe3a0fce0c6aab3c4a12fe44 Mon Sep 17 00:00:00 2001
+From: A U Thor <author@example.com>
+Date: Mon, 26 Jun 2006 00:01:00 +0000
+Subject: [DIFFERENT_PREFIX 1/2] Second
+
+This is the second commit.
+---
+ dir/sub |    2 ++
+ file0   |    3 +++
+ file2   |    3 ---
+ 3 files changed, 5 insertions(+), 3 deletions(-)
+ delete mode 100644 file2
+
+diff --git a/dir/sub b/dir/sub
+index 35d242b..8422d40 100644
+--- a/dir/sub
++++ b/dir/sub
+@@ -1,2 +1,4 @@
+ A
+ B
++C
++D
+diff --git a/file0 b/file0
+index 01e79c3..b414108 100644
+--- a/file0
++++ b/file0
+@@ -1,3 +1,6 @@
+ 1
+ 2
+ 3
++4
++5
++6
+diff --git a/file2 b/file2
+deleted file mode 100644
+index 01e79c3..0000000
+--- a/file2
++++ /dev/null
+@@ -1,3 +0,0 @@
+-1
+-2
+-3
+-- 
+g-i-t--v-e-r-s-i-o-n
+
+
+From 9a6d4949b6b76956d9d5e26f2791ec2ceff5fdc0 Mon Sep 17 00:00:00 2001
+From: A U Thor <author@example.com>
+Date: Mon, 26 Jun 2006 00:02:00 +0000
+Subject: [DIFFERENT_PREFIX 2/2] Third
+
+---
+ dir/sub |    2 ++
+ file1   |    3 +++
+ 2 files changed, 5 insertions(+), 0 deletions(-)
+ create mode 100644 file1
+
+diff --git a/dir/sub b/dir/sub
+index 8422d40..cead32e 100644
+--- a/dir/sub
++++ b/dir/sub
+@@ -2,3 +2,5 @@ A
+ B
+ C
+ D
++E
++F
+diff --git a/file1 b/file1
+new file mode 100644
+index 0000000..b1e6722
+--- /dev/null
++++ b/file1
+@@ -0,0 +1,3 @@
++A
++B
++C
+-- 
+g-i-t--v-e-r-s-i-o-n
+
+$
index 0a6fe53375bb26e5c3a69d503f6f13833a2020fa..16aa99dc0dbe1fe7e09aa046dfc2dc1ec652963c 100755 (executable)
@@ -88,4 +88,117 @@ test_expect_success 'replay did not screw up the log message' '
 
 '
 
+test_expect_success 'extra headers' '
+
+       git config format.headers "To: R. E. Cipient <rcipient@example.com>
+" &&
+       git config --add format.headers "Cc: S. E. Cipient <scipient@example.com>
+" &&
+       git format-patch --stdout master..side > patch2 &&
+       sed -e "/^$/q" patch2 > hdrs2 &&
+       grep "^To: R. E. Cipient <rcipient@example.com>$" hdrs2 &&
+       grep "^Cc: S. E. Cipient <scipient@example.com>$" hdrs2
+       
+'
+
+test_expect_success 'extra headers without newlines' '
+
+       git config --replace-all format.headers "To: R. E. Cipient <rcipient@example.com>" &&
+       git config --add format.headers "Cc: S. E. Cipient <scipient@example.com>" &&
+       git format-patch --stdout master..side >patch3 &&
+       sed -e "/^$/q" patch3 > hdrs3 &&
+       grep "^To: R. E. Cipient <rcipient@example.com>$" hdrs3 &&
+       grep "^Cc: S. E. Cipient <scipient@example.com>$" hdrs3
+       
+'
+
+test_expect_success 'extra headers with multiple To:s' '
+
+       git config --replace-all format.headers "To: R. E. Cipient <rcipient@example.com>" &&
+       git config --add format.headers "To: S. E. Cipient <scipient@example.com>" &&
+       git format-patch --stdout master..side > patch4 &&
+       sed -e "/^$/q" patch4 > hdrs4 &&
+       grep "^To: R. E. Cipient <rcipient@example.com>,$" hdrs4 &&
+       grep "^ *S. E. Cipient <scipient@example.com>$" hdrs4
+'
+
+test_expect_success 'additional command line cc' '
+
+       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
+'
+
+test_expect_success 'multiple files' '
+
+       rm -rf patches/ &&
+       git checkout side &&
+       git format-patch -o patches/ master &&
+       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 'thread' '
+
+       rm -rf patches/ &&
+       git checkout side &&
+       git format-patch --thread -o patches/ master &&
+       FIRST_MID=$(grep "Message-Id:" patches/0001-* | sed "s/^[^<]*\(<[^>]*>\).*$/\1/") &&
+       for i in patches/0002-* patches/0003-*
+       do
+         grep "References: $FIRST_MID" $i &&
+         grep "In-Reply-To: $FIRST_MID" $i
+       done
+'
+
+test_expect_success 'thread in-reply-to' '
+
+       rm -rf patches/ &&
+       git checkout side &&
+       git format-patch --in-reply-to="<test.message>" --thread -o patches/ master &&
+       FIRST_MID="<test.message>" &&
+       for i in patches/*
+       do
+         grep "References: $FIRST_MID" $i &&
+         grep "In-Reply-To: $FIRST_MID" $i
+       done
+'
+
+test_expect_success 'thread cover-letter' '
+
+       rm -rf patches/ &&
+       git checkout side &&
+       git format-patch --cover-letter --thread -o patches/ master &&
+       FIRST_MID=$(grep "Message-Id:" patches/0000-* | sed "s/^[^<]*\(<[^>]*>\).*$/\1/") &&
+       for i in patches/0001-* patches/0002-* patches/0003-* 
+       do
+         grep "References: $FIRST_MID" $i &&
+         grep "In-Reply-To: $FIRST_MID" $i
+       done
+'
+
+test_expect_success 'thread cover-letter in-reply-to' '
+
+       rm -rf patches/ &&
+       git checkout side &&
+       git format-patch --cover-letter --in-reply-to="<test.message>" --thread -o patches/ master &&
+       FIRST_MID="<test.message>" &&
+       for i in patches/*
+       do
+         grep "References: $FIRST_MID" $i &&
+         grep "In-Reply-To: $FIRST_MID" $i
+       done
+'
+
+test_expect_success 'excessive subject' '
+
+       rm -rf patches/ &&
+       git checkout side &&
+       for i in 5 6 1 2 3 A 4 B C 7 8 9 10 D E F; do echo "$i"; done >>file &&
+       git update-index file &&
+       git commit -m "This is an excessively long subject line for a message due to the habit some projects have of not having a short, one-line subject at the start of the commit message, but rather sticking a whole paragraph right at the start as the only thing in the commit message. It had better not become the filename for the patch." &&
+       git format-patch -o patches/ master..side &&
+       ls patches/0004-This-is-an-excessively-long-subject-line-for-a-messa.patch
+'
+
 test_done