@matheusbsbs / matheus-bagliano / commits / a5a27c79b7
Fork 0

Add a --cover-letter option to format-patch

If --cover-letter is provided, generate a cover letter message before the patches, numbered 0. Original patch thanks to Johannes Schindelin Signed-off-by: Daniel Barkalow <barkalow@iabervon.org> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Daniel Barkalow committed Feb 18, 2008 at 22:56 UTC a5a27c79b7e77e28462b6d089e827391b67d3e5f
5 files changed +314 -65
Documentation/git-format-patch.txt
+13 -7
@@ -10,13 +10,14 @@ SYNOPSIS
10 --------
11 [verse]
12 'git-format-patch' [-k] [-o <dir> | --stdout] [--thread]
13 - [--attach[=<boundary>] | --inline[=<boundary>]]
14 - [-s | --signoff] [<common diff options>]
15 - [-n | --numbered | -N | --no-numbered]
16 - [--start-number <n>] [--numbered-files]
17 - [--in-reply-to=Message-Id] [--suffix=.<sfx>]
18 - [--ignore-if-in-upstream]
19 - [--subject-prefix=Subject-Prefix]
13 + [--attach[=<boundary>] | --inline[=<boundary>]]
14 + [-s | --signoff] [<common diff options>]
15 + [-n | --numbered | -N | --no-numbered]
16 + [--start-number <n>] [--numbered-files]
17 + [--in-reply-to=Message-Id] [--suffix=.<sfx>]
18 + [--ignore-if-in-upstream]
19 + [--subject-prefix=Subject-Prefix]
20 + [--cover-letter]
21 [ <since> | <revision range> ]
22
23 DESCRIPTION
@@ -135,6 +136,11 @@ include::diff-options.txt[]
136 allows for useful naming of a patch series, and can be
137 combined with the --numbered option.
138
139 +--cover-letter::
140 + Generate a cover letter template. You still have to fill in
141 + a description, but the shortlog and the diffstat will be
142 + generated for you.
143 +
144 --suffix=.<sfx>::
145 Instead of using `.patch` as the suffix for generated
146 filenames, use specified suffix. A common alternative is
builtin-log.c
+174 -58
@@ -14,6 +14,7 @@
14 #include "reflog-walk.h"
15 #include "patch-ids.h"
16 #include "refs.h"
17 +#include "run-command.h"
18
19 static int default_show_root = 1;
20 static const char *fmt_patch_subject_prefix = "PATCH";
@@ -452,74 +453,81 @@ static int git_format_config(const char *var, const char *value)
453 }
454
455
456 +static const char *get_oneline_for_filename(struct commit *commit,
457 + int keep_subject)
458 +{
459 + static char filename[PATH_MAX];
460 + char *sol;
461 + int len = 0;
462 + int suffix_len = strlen(fmt_patch_suffix) + 1;
463 +
464 + sol = strstr(commit->buffer, "\n\n");
465 + if (!sol)
466 + filename[0] = '\0';
467 + else {
468 + int j, space = 0;
469 +
470 + sol += 2;
471 + /* strip [PATCH] or [PATCH blabla] */
472 + if (!keep_subject && !prefixcmp(sol, "[PATCH")) {
473 + char *eos = strchr(sol + 6, ']');
474 + if (eos) {
475 + while (isspace(*eos))
476 + eos++;
477 + sol = eos;
478 + }
479 + }
480 +
481 + for (j = 0;
482 + j < FORMAT_PATCH_NAME_MAX - suffix_len - 5 &&
483 + len < sizeof(filename) - suffix_len &&
484 + sol[j] && sol[j] != '\n';
485 + j++) {
486 + if (istitlechar(sol[j])) {
487 + if (space) {
488 + filename[len++] = '-';
489 + space = 0;
490 + }
491 + filename[len++] = sol[j];
492 + if (sol[j] == '.')
493 + while (sol[j + 1] == '.')
494 + j++;
495 + } else
496 + space = 1;
497 + }
498 + while (filename[len - 1] == '.'
499 + || filename[len - 1] == '-')
500 + len--;
501 + filename[len] = '\0';
502 + }
503 + return filename;
504 +}
505 +
506 static FILE *realstdout = NULL;
507 static const char *output_directory = NULL;
508
458 -static int reopen_stdout(struct commit *commit, int nr, int keep_subject,
459 - int numbered_files)
509 +static int reopen_stdout(const char *oneline, int nr, int total)
510 {
511 char filename[PATH_MAX];
462 - char *sol;
512 int len = 0;
513 int suffix_len = strlen(fmt_patch_suffix) + 1;
514
515 if (output_directory) {
467 - if (strlen(output_directory) >=
516 + len = snprintf(filename, sizeof(filename), "%s",
517 + output_directory);
518 + if (len >=
519 sizeof(filename) - FORMAT_PATCH_NAME_MAX - suffix_len)
520 return error("name of output directory is too long");
470 - strlcpy(filename, output_directory, sizeof(filename) - suffix_len);
471 - len = strlen(filename);
521 if (filename[len - 1] != '/')
522 filename[len++] = '/';
523 }
524
476 - if (numbered_files) {
477 - sprintf(filename + len, "%d", nr);
478 - len = strlen(filename);
479 -
480 - } else {
481 - sprintf(filename + len, "%04d", nr);
482 - len = strlen(filename);
483 -
484 - sol = strstr(commit->buffer, "\n\n");
485 - if (sol) {
486 - int j, space = 1;
487 -
488 - sol += 2;
489 - /* strip [PATCH] or [PATCH blabla] */
490 - if (!keep_subject && !prefixcmp(sol, "[PATCH")) {
491 - char *eos = strchr(sol + 6, ']');
492 - if (eos) {
493 - while (isspace(*eos))
494 - eos++;
495 - sol = eos;
496 - }
497 - }
498 -
499 - for (j = 0;
500 - j < FORMAT_PATCH_NAME_MAX - suffix_len - 5 &&
501 - len < sizeof(filename) - suffix_len &&
502 - sol[j] && sol[j] != '\n';
503 - j++) {
504 - if (istitlechar(sol[j])) {
505 - if (space) {
506 - filename[len++] = '-';
507 - space = 0;
508 - }
509 - filename[len++] = sol[j];
510 - if (sol[j] == '.')
511 - while (sol[j + 1] == '.')
512 - j++;
513 - } else
514 - space = 1;
515 - }
516 - while (filename[len - 1] == '.'
517 - || filename[len - 1] == '-')
518 - len--;
519 - filename[len] = 0;
520 - }
521 - if (len + suffix_len >= sizeof(filename))
522 - return error("Patch pathname too long");
525 + if (!oneline)
526 + len += sprintf(filename + len, "%d", nr);
527 + else {
528 + len += sprintf(filename + len, "%04d-", nr);
529 + len += snprintf(filename + len, sizeof(filename) - len - 1
530 + - suffix_len, "%s", oneline);
531 strcpy(filename + len, fmt_patch_suffix);
532 }
533
@@ -590,6 +598,76 @@ static void gen_message_id(struct rev_info *info, char *base)
598 info->message_id = strbuf_detach(&buf, NULL);
599 }
600
601 +static void make_cover_letter(struct rev_info *rev,
602 + int use_stdout, int numbered, int numbered_files,
603 + struct commit *origin, struct commit *head)
604 +{
605 + const char *committer;
606 + const char *origin_sha1, *head_sha1;
607 + const char *argv[7];
608 + const char *subject_start = NULL;
609 + const char *body = "*** SUBJECT HERE ***\n\n*** BLURB HERE ***\n";
610 + const char *msg;
611 + const char *extra_headers = rev->extra_headers;
612 + struct strbuf sb;
613 + const char *encoding = "utf-8";
614 +
615 + if (rev->commit_format != CMIT_FMT_EMAIL)
616 + die("Cover letter needs email format");
617 +
618 + if (!use_stdout && reopen_stdout(numbered_files ?
619 + NULL : "cover-letter", 0, rev->total))
620 + return;
621 +
622 + origin_sha1 = sha1_to_hex(origin ? origin->object.sha1 : null_sha1);
623 + head_sha1 = sha1_to_hex(head->object.sha1);
624 +
625 + log_write_email_headers(rev, head_sha1, &subject_start, &extra_headers);
626 +
627 + committer = git_committer_info(0);
628 +
629 + msg = body;
630 + strbuf_init(&sb, 0);
631 + pp_user_info(NULL, CMIT_FMT_EMAIL, &sb, committer, DATE_RFC2822,
632 + encoding);
633 + pp_title_line(CMIT_FMT_EMAIL, &msg, &sb, subject_start, extra_headers,
634 + encoding, 0);
635 + pp_remainder(CMIT_FMT_EMAIL, &msg, &sb, 0);
636 + printf("%s\n", sb.buf);
637 +
638 + strbuf_release(&sb);
639 +
640 + /*
641 + * We can only do diffstat with a unique reference point, and
642 + * log is a bit tricky, so just skip it.
643 + */
644 + if (!origin)
645 + return;
646 +
647 + argv[0] = "shortlog";
648 + argv[1] = head_sha1;
649 + argv[2] = "--not";
650 + argv[3] = origin_sha1;
651 + argv[4] = "--";
652 + argv[5] = NULL;
653 + fflush(stdout);
654 + run_command_v_opt(argv, RUN_GIT_CMD);
655 +
656 + argv[0] = "diff";
657 + argv[1] = "--stat";
658 + argv[2] = "--summary";
659 + argv[3] = head_sha1;
660 + argv[4] = "--not";
661 + argv[5] = origin_sha1;
662 + argv[6] = "--";
663 + argv[7] = NULL;
664 + fflush(stdout);
665 + run_command_v_opt(argv, RUN_GIT_CMD);
666 +
667 + fflush(stdout);
668 + printf("\n");
669 +}
670 +
671 static const char *clean_message_id(const char *msg_id)
672 {
673 char ch;
@@ -625,6 +703,8 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)
703 int subject_prefix = 0;
704 int ignore_if_in_upstream = 0;
705 int thread = 0;
706 + int cover_letter = 0;
707 + struct commit *origin = NULL, *head = NULL;
708 const char *in_reply_to = NULL;
709 struct patch_ids ids;
710 char *add_signoff = NULL;
@@ -724,6 +804,8 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)
804 rev.subject_prefix = argv[i] + 17;
805 } else if (!prefixcmp(argv[i], "--suffix="))
806 fmt_patch_suffix = argv[i] + 9;
807 + else if (!strcmp(argv[i], "--cover-letter"))
808 + cover_letter = 1;
809 else
810 argv[j++] = argv[i];
811 }
@@ -775,6 +857,25 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)
857 * get_revision() to do the usual traversal.
858 */
859 }
860 + if (cover_letter) {
861 + /* remember the range */
862 + int negative_count = 0;
863 + int i;
864 + for (i = 0; i < rev.pending.nr; i++) {
865 + struct object *o = rev.pending.objects[i].item;
866 + if (o->flags & UNINTERESTING) {
867 + origin = (struct commit *)o;
868 + negative_count++;
869 + } else
870 + head = (struct commit *)o;
871 + }
872 + /* Multiple origins don't work for diffstat. */
873 + if (negative_count > 1)
874 + origin = NULL;
875 + /* We can't generate a cover letter without any patches */
876 + if (!head)
877 + return 0;
878 + }
879
880 if (ignore_if_in_upstream)
881 get_patch_ids(&rev, &ids, prefix);
@@ -801,16 +902,31 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)
902 numbered = 1;
903 if (numbered)
904 rev.total = total + start_number - 1;
804 - rev.add_signoff = add_signoff;
905 if (in_reply_to)
906 rev.ref_message_id = clean_message_id(in_reply_to);
907 + if (cover_letter) {
908 + if (thread)
909 + gen_message_id(&rev, "cover");
910 + make_cover_letter(&rev, use_stdout, numbered, numbered_files,
911 + origin, head);
912 + total++;
913 + start_number--;
914 + }
915 + rev.add_signoff = add_signoff;
916 while (0 <= --nr) {
917 int shown;
918 commit = list[nr];
919 rev.nr = total - nr + (start_number - 1);
920 /* Make the second and subsequent mails replies to the first */
921 if (thread) {
922 + /* Have we already had a message ID? */
923 if (rev.message_id) {
924 + /*
925 + * If we've got the ID to be a reply
926 + * to, discard the current ID;
927 + * otherwise, make everything a reply
928 + * to that.
929 + */
930 if (rev.ref_message_id)
931 free(rev.message_id);
932 else
@@ -818,10 +934,10 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)
934 }
935 gen_message_id(&rev, sha1_to_hex(commit->object.sha1));
936 }
821 - if (!use_stdout)
822 - if (reopen_stdout(commit, rev.nr, keep_subject,
823 - numbered_files))
824 - die("Failed to create output files");
937 + if (!use_stdout && reopen_stdout(numbered_files ? NULL :
938 + get_oneline_for_filename(commit, keep_subject),
939 + rev.nr, rev.total))
940 + die("Failed to create output files");
941 shown = log_tree_commit(&rev, commit);
942 free(commit->buffer);
943 commit->buffer = NULL;
t/t4013-diff-various.sh
+1
@@ -245,6 +245,7 @@ format-patch --inline --stdout initial..master
245 format-patch --inline --stdout --subject-prefix=TESTCASE initial..master
246 config format.subjectprefix DIFFERENT_PREFIX
247 format-patch --inline --stdout initial..master^^
248 +format-patch --stdout --cover-letter -n initial..master^
249
250 diff --abbrev initial..side
251 diff -r initial..side
t/t4013/diff.format-patch_--stdout_--cover-letter_-n_initial..master^ new
+100
@@ -0,0 +1,100 @@
1 +$ git format-patch --stdout --cover-letter -n initial..master^
2 +From 9a6d4949b6b76956d9d5e26f2791ec2ceff5fdc0 Mon Sep 17 00:00:00 2001
3 +From: C O Mitter <committer@example.com>
4 +Date: Mon, 26 Jun 2006 00:05:00 +0000
5 +Subject: [DIFFERENT_PREFIX 0/2] *** SUBJECT HERE ***
6 +
7 +*** BLURB HERE ***
8 +
9 +A U Thor (2):
10 + Second
11 + Third
12 +
13 + dir/sub | 4 ++++
14 + file0 | 3 +++
15 + file1 | 3 +++
16 + file2 | 3 ---
17 + 4 files changed, 10 insertions(+), 3 deletions(-)
18 + create mode 100644 file1
19 + delete mode 100644 file2
20 +
21 +From 1bde4ae5f36c8d9abe3a0fce0c6aab3c4a12fe44 Mon Sep 17 00:00:00 2001
22 +From: A U Thor <author@example.com>
23 +Date: Mon, 26 Jun 2006 00:01:00 +0000
24 +Subject: [DIFFERENT_PREFIX 1/2] Second
25 +
26 +This is the second commit.
27 +---
28 + dir/sub | 2 ++
29 + file0 | 3 +++
30 + file2 | 3 ---
31 + 3 files changed, 5 insertions(+), 3 deletions(-)
32 + delete mode 100644 file2
33 +
34 +diff --git a/dir/sub b/dir/sub
35 +index 35d242b..8422d40 100644
36 +--- a/dir/sub
37 ++++ b/dir/sub
38 +@@ -1,2 +1,4 @@
39 + A
40 + B
41 ++C
42 ++D
43 +diff --git a/file0 b/file0
44 +index 01e79c3..b414108 100644
45 +--- a/file0
46 ++++ b/file0
47 +@@ -1,3 +1,6 @@
48 + 1
49 + 2
50 + 3
51 ++4
52 ++5
53 ++6
54 +diff --git a/file2 b/file2
55 +deleted file mode 100644
56 +index 01e79c3..0000000
57 +--- a/file2
58 ++++ /dev/null
59 +@@ -1,3 +0,0 @@
60 +-1
61 +-2
62 +-3
63 +--
64 +g-i-t--v-e-r-s-i-o-n
65 +
66 +
67 +From 9a6d4949b6b76956d9d5e26f2791ec2ceff5fdc0 Mon Sep 17 00:00:00 2001
68 +From: A U Thor <author@example.com>
69 +Date: Mon, 26 Jun 2006 00:02:00 +0000
70 +Subject: [DIFFERENT_PREFIX 2/2] Third
71 +
72 +---
73 + dir/sub | 2 ++
74 + file1 | 3 +++
75 + 2 files changed, 5 insertions(+), 0 deletions(-)
76 + create mode 100644 file1
77 +
78 +diff --git a/dir/sub b/dir/sub
79 +index 8422d40..cead32e 100644
80 +--- a/dir/sub
81 ++++ b/dir/sub
82 +@@ -2,3 +2,5 @@ A
83 + B
84 + C
85 + D
86 ++E
87 ++F
88 +diff --git a/file1 b/file1
89 +new file mode 100644
90 +index 0000000..b1e6722
91 +--- /dev/null
92 ++++ b/file1
93 +@@ -0,0 +1,3 @@
94 ++A
95 ++B
96 ++C
97 +--
98 +g-i-t--v-e-r-s-i-o-n
99 +
100 +$
t/t4014-format-patch.sh
+26
@@ -122,6 +122,32 @@ test_expect_success 'thread in-reply-to' '
122 done
123 '
124
125 +test_expect_success 'thread cover-letter' '
126 +
127 + rm -rf patches/ &&
128 + git checkout side &&
129 + git format-patch --cover-letter --thread -o patches/ master &&
130 + FIRST_MID=$(grep "Message-Id:" patches/0000-* | sed "s/^[^<]*\(<[^>]*>\).*$/\1/") &&
131 + for i in patches/0001-* patches/0002-* patches/0003-*
132 + do
133 + grep "References: $FIRST_MID" $i &&
134 + grep "In-Reply-To: $FIRST_MID" $i
135 + done
136 +'
137 +
138 +test_expect_success 'thread cover-letter in-reply-to' '
139 +
140 + rm -rf patches/ &&
141 + git checkout side &&
142 + git format-patch --cover-letter --in-reply-to="<test.message>" --thread -o patches/ master &&
143 + FIRST_MID="<test.message>" &&
144 + for i in patches/*
145 + do
146 + grep "References: $FIRST_MID" $i &&
147 + grep "In-Reply-To: $FIRST_MID" $i
148 + done
149 +'
150 +
151 test_expect_success 'excessive subject' '
152
153 rm -rf patches/ &&