mirror of
https://github.com/git/git
synced 2024-11-05 18:59:29 +00:00
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>
This commit is contained in:
parent
b02bd65f67
commit
a5a27c79b7
5 changed files with 317 additions and 68 deletions
|
@ -17,6 +17,7 @@ SYNOPSIS
|
||||||
[--in-reply-to=Message-Id] [--suffix=.<sfx>]
|
[--in-reply-to=Message-Id] [--suffix=.<sfx>]
|
||||||
[--ignore-if-in-upstream]
|
[--ignore-if-in-upstream]
|
||||||
[--subject-prefix=Subject-Prefix]
|
[--subject-prefix=Subject-Prefix]
|
||||||
|
[--cover-letter]
|
||||||
[ <since> | <revision range> ]
|
[ <since> | <revision range> ]
|
||||||
|
|
||||||
DESCRIPTION
|
DESCRIPTION
|
||||||
|
@ -135,6 +136,11 @@ include::diff-options.txt[]
|
||||||
allows for useful naming of a patch series, and can be
|
allows for useful naming of a patch series, and can be
|
||||||
combined with the --numbered option.
|
combined with the --numbered option.
|
||||||
|
|
||||||
|
--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>::
|
--suffix=.<sfx>::
|
||||||
Instead of using `.patch` as the suffix for generated
|
Instead of using `.patch` as the suffix for generated
|
||||||
filenames, use specified suffix. A common alternative is
|
filenames, use specified suffix. A common alternative is
|
||||||
|
|
182
builtin-log.c
182
builtin-log.c
|
@ -14,6 +14,7 @@
|
||||||
#include "reflog-walk.h"
|
#include "reflog-walk.h"
|
||||||
#include "patch-ids.h"
|
#include "patch-ids.h"
|
||||||
#include "refs.h"
|
#include "refs.h"
|
||||||
|
#include "run-command.h"
|
||||||
|
|
||||||
static int default_show_root = 1;
|
static int default_show_root = 1;
|
||||||
static const char *fmt_patch_subject_prefix = "PATCH";
|
static const char *fmt_patch_subject_prefix = "PATCH";
|
||||||
|
@ -452,38 +453,19 @@ static int git_format_config(const char *var, const char *value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
static FILE *realstdout = NULL;
|
static const char *get_oneline_for_filename(struct commit *commit,
|
||||||
static const char *output_directory = NULL;
|
int keep_subject)
|
||||||
|
|
||||||
static int reopen_stdout(struct commit *commit, int nr, int keep_subject,
|
|
||||||
int numbered_files)
|
|
||||||
{
|
{
|
||||||
char filename[PATH_MAX];
|
static char filename[PATH_MAX];
|
||||||
char *sol;
|
char *sol;
|
||||||
int len = 0;
|
int len = 0;
|
||||||
int suffix_len = strlen(fmt_patch_suffix) + 1;
|
int suffix_len = strlen(fmt_patch_suffix) + 1;
|
||||||
|
|
||||||
if (output_directory) {
|
|
||||||
if (strlen(output_directory) >=
|
|
||||||
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");
|
sol = strstr(commit->buffer, "\n\n");
|
||||||
if (sol) {
|
if (!sol)
|
||||||
int j, space = 1;
|
filename[0] = '\0';
|
||||||
|
else {
|
||||||
|
int j, space = 0;
|
||||||
|
|
||||||
sol += 2;
|
sol += 2;
|
||||||
/* strip [PATCH] or [PATCH blabla] */
|
/* strip [PATCH] or [PATCH blabla] */
|
||||||
|
@ -516,10 +498,36 @@ static int reopen_stdout(struct commit *commit, int nr, int keep_subject,
|
||||||
while (filename[len - 1] == '.'
|
while (filename[len - 1] == '.'
|
||||||
|| filename[len - 1] == '-')
|
|| filename[len - 1] == '-')
|
||||||
len--;
|
len--;
|
||||||
filename[len] = 0;
|
filename[len] = '\0';
|
||||||
}
|
}
|
||||||
if (len + suffix_len >= sizeof(filename))
|
return filename;
|
||||||
return error("Patch pathname too long");
|
}
|
||||||
|
|
||||||
|
static FILE *realstdout = NULL;
|
||||||
|
static const char *output_directory = NULL;
|
||||||
|
|
||||||
|
static int reopen_stdout(const char *oneline, int nr, int total)
|
||||||
|
{
|
||||||
|
char filename[PATH_MAX];
|
||||||
|
int len = 0;
|
||||||
|
int suffix_len = strlen(fmt_patch_suffix) + 1;
|
||||||
|
|
||||||
|
if (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");
|
||||||
|
if (filename[len - 1] != '/')
|
||||||
|
filename[len++] = '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
strcpy(filename + len, fmt_patch_suffix);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -590,6 +598,76 @@ static void gen_message_id(struct rev_info *info, char *base)
|
||||||
info->message_id = strbuf_detach(&buf, NULL);
|
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, 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 strbuf sb;
|
||||||
|
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;
|
||||||
|
|
||||||
|
origin_sha1 = sha1_to_hex(origin ? origin->object.sha1 : null_sha1);
|
||||||
|
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);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* We can only do diffstat with a unique reference point, and
|
||||||
|
* log is a bit tricky, so just skip it.
|
||||||
|
*/
|
||||||
|
if (!origin)
|
||||||
|
return;
|
||||||
|
|
||||||
|
argv[0] = "shortlog";
|
||||||
|
argv[1] = head_sha1;
|
||||||
|
argv[2] = "--not";
|
||||||
|
argv[3] = origin_sha1;
|
||||||
|
argv[4] = "--";
|
||||||
|
argv[5] = NULL;
|
||||||
|
fflush(stdout);
|
||||||
|
run_command_v_opt(argv, RUN_GIT_CMD);
|
||||||
|
|
||||||
|
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)
|
static const char *clean_message_id(const char *msg_id)
|
||||||
{
|
{
|
||||||
char ch;
|
char ch;
|
||||||
|
@ -625,6 +703,8 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)
|
||||||
int subject_prefix = 0;
|
int subject_prefix = 0;
|
||||||
int ignore_if_in_upstream = 0;
|
int ignore_if_in_upstream = 0;
|
||||||
int thread = 0;
|
int thread = 0;
|
||||||
|
int cover_letter = 0;
|
||||||
|
struct commit *origin = NULL, *head = NULL;
|
||||||
const char *in_reply_to = NULL;
|
const char *in_reply_to = NULL;
|
||||||
struct patch_ids ids;
|
struct patch_ids ids;
|
||||||
char *add_signoff = NULL;
|
char *add_signoff = NULL;
|
||||||
|
@ -724,6 +804,8 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)
|
||||||
rev.subject_prefix = argv[i] + 17;
|
rev.subject_prefix = argv[i] + 17;
|
||||||
} else if (!prefixcmp(argv[i], "--suffix="))
|
} else if (!prefixcmp(argv[i], "--suffix="))
|
||||||
fmt_patch_suffix = argv[i] + 9;
|
fmt_patch_suffix = argv[i] + 9;
|
||||||
|
else if (!strcmp(argv[i], "--cover-letter"))
|
||||||
|
cover_letter = 1;
|
||||||
else
|
else
|
||||||
argv[j++] = argv[i];
|
argv[j++] = argv[i];
|
||||||
}
|
}
|
||||||
|
@ -775,6 +857,25 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)
|
||||||
* get_revision() to do the usual traversal.
|
* get_revision() to do the usual traversal.
|
||||||
*/
|
*/
|
||||||
}
|
}
|
||||||
|
if (cover_letter) {
|
||||||
|
/* remember the range */
|
||||||
|
int negative_count = 0;
|
||||||
|
int i;
|
||||||
|
for (i = 0; i < rev.pending.nr; i++) {
|
||||||
|
struct object *o = rev.pending.objects[i].item;
|
||||||
|
if (o->flags & UNINTERESTING) {
|
||||||
|
origin = (struct commit *)o;
|
||||||
|
negative_count++;
|
||||||
|
} else
|
||||||
|
head = (struct commit *)o;
|
||||||
|
}
|
||||||
|
/* Multiple origins don't work for diffstat. */
|
||||||
|
if (negative_count > 1)
|
||||||
|
origin = NULL;
|
||||||
|
/* We can't generate a cover letter without any patches */
|
||||||
|
if (!head)
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
if (ignore_if_in_upstream)
|
if (ignore_if_in_upstream)
|
||||||
get_patch_ids(&rev, &ids, prefix);
|
get_patch_ids(&rev, &ids, prefix);
|
||||||
|
@ -801,16 +902,31 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)
|
||||||
numbered = 1;
|
numbered = 1;
|
||||||
if (numbered)
|
if (numbered)
|
||||||
rev.total = total + start_number - 1;
|
rev.total = total + start_number - 1;
|
||||||
rev.add_signoff = add_signoff;
|
|
||||||
if (in_reply_to)
|
if (in_reply_to)
|
||||||
rev.ref_message_id = clean_message_id(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, head);
|
||||||
|
total++;
|
||||||
|
start_number--;
|
||||||
|
}
|
||||||
|
rev.add_signoff = add_signoff;
|
||||||
while (0 <= --nr) {
|
while (0 <= --nr) {
|
||||||
int shown;
|
int shown;
|
||||||
commit = list[nr];
|
commit = list[nr];
|
||||||
rev.nr = total - nr + (start_number - 1);
|
rev.nr = total - nr + (start_number - 1);
|
||||||
/* Make the second and subsequent mails replies to the first */
|
/* Make the second and subsequent mails replies to the first */
|
||||||
if (thread) {
|
if (thread) {
|
||||||
|
/* Have we already had a message ID? */
|
||||||
if (rev.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)
|
if (rev.ref_message_id)
|
||||||
free(rev.message_id);
|
free(rev.message_id);
|
||||||
else
|
else
|
||||||
|
@ -818,9 +934,9 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix)
|
||||||
}
|
}
|
||||||
gen_message_id(&rev, sha1_to_hex(commit->object.sha1));
|
gen_message_id(&rev, sha1_to_hex(commit->object.sha1));
|
||||||
}
|
}
|
||||||
if (!use_stdout)
|
if (!use_stdout && reopen_stdout(numbered_files ? NULL :
|
||||||
if (reopen_stdout(commit, rev.nr, keep_subject,
|
get_oneline_for_filename(commit, keep_subject),
|
||||||
numbered_files))
|
rev.nr, rev.total))
|
||||||
die("Failed to create output files");
|
die("Failed to create output files");
|
||||||
shown = log_tree_commit(&rev, commit);
|
shown = log_tree_commit(&rev, commit);
|
||||||
free(commit->buffer);
|
free(commit->buffer);
|
||||||
|
|
|
@ -245,6 +245,7 @@ format-patch --inline --stdout initial..master
|
||||||
format-patch --inline --stdout --subject-prefix=TESTCASE initial..master
|
format-patch --inline --stdout --subject-prefix=TESTCASE initial..master
|
||||||
config format.subjectprefix DIFFERENT_PREFIX
|
config format.subjectprefix DIFFERENT_PREFIX
|
||||||
format-patch --inline --stdout initial..master^^
|
format-patch --inline --stdout initial..master^^
|
||||||
|
format-patch --stdout --cover-letter -n initial..master^
|
||||||
|
|
||||||
diff --abbrev initial..side
|
diff --abbrev initial..side
|
||||||
diff -r initial..side
|
diff -r initial..side
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
$
|
|
@ -122,6 +122,32 @@ test_expect_success 'thread in-reply-to' '
|
||||||
done
|
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' '
|
test_expect_success 'excessive subject' '
|
||||||
|
|
||||||
rm -rf patches/ &&
|
rm -rf patches/ &&
|
||||||
|
|
Loading…
Reference in a new issue