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/ &&