Commit 6aa349667974a521dbe0c7e0f543f9086156689d

Edward Thomson 2021-09-13T08:17:21

email: introduce `git_email_create_from_diff` Introduce a function to create an email from a diff and multiple inputs about the source of the diff. Creating an email from a diff requires many more inputs, and should be discouraged in favor of building directly from a commit, and is thus in the `sys` namespace.

diff --git a/include/git2/email.h b/include/git2/email.h
index 6014c6c..48715fe 100644
--- a/include/git2/email.h
+++ b/include/git2/email.h
@@ -73,30 +73,39 @@ typedef struct {
 
 /**
  * Create a diff for a commit in mbox format for sending via email.
- * The commit must not be a merge commit.
  *
  * @param out buffer to store the e-mail patch in
- * @param commit commit to create a patch for
+ * @param diff the changes to include in the email
+ * @param patch_idx the patch index
+ * @param patch_count the total number of patches that will be included
+ * @param commit_id the commit id for this change
+ * @param summary the commit message for this change
+ * @param body optional text to include above the diffstat
+ * @param author the person who authored this commit
  * @param opts email creation options
  */
-GIT_EXTERN(int) git_email_create_from_commit(
+GIT_EXTERN(int) git_email_create_from_diff(
 	git_buf *out,
-	git_commit *commit,
+	git_diff *diff,
+	size_t patch_idx,
+	size_t patch_count,
+	const git_oid *commit_id,
+	const char *summary,
+	const char *body,
+	const git_signature *author,
 	const git_email_create_options *opts);
 
 /**
- * Create an mbox format diff for the given commits in the revision
- * spec, excluding merge commits.
+ * Create a diff for a commit in mbox format for sending via email.
+ * The commit must not be a merge commit.
  *
- * @param out buffer to store the e-mail patches in
- * @param commits the array of commits to create patches for
- * @param len the length of the `commits` array
+ * @param out buffer to store the e-mail patch in
+ * @param commit commit to create a patch for
  * @param opts email creation options
  */
-GIT_EXTERN(int) git_email_create_from_commits(
-	git_strarray *out,
-	git_commit **commits,
-	size_t len,
+GIT_EXTERN(int) git_email_create_from_commit(
+	git_buf *out,
+	git_commit *commit,
 	const git_email_create_options *opts);
 
 GIT_END_DECL
diff --git a/include/git2/sys/email.h b/include/git2/sys/email.h
new file mode 100644
index 0000000..6f4a286
--- /dev/null
+++ b/include/git2/sys/email.h
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) the libgit2 contributors. All rights reserved.
+ *
+ * This file is part of libgit2, distributed under the GNU GPL v2 with
+ * a Linking Exception. For full terms see the included COPYING file.
+ */
+#ifndef INCLUDE_sys_git_email_h__
+#define INCLUDE_sys_git_email_h__
+
+/**
+ * @file git2/sys/email.h
+ * @brief Advanced git email creation routines
+ * @defgroup git_email Advanced git email creation routines
+ * @ingroup Git
+ * @{
+ */
+GIT_BEGIN_DECL
+
+/**
+ * Create a diff for a commit in mbox format for sending via email.
+ *
+ * @param out buffer to store the e-mail patch in
+ * @param diff the changes to include in the email
+ * @param patch_idx the patch index
+ * @param patch_count the total number of patches that will be included
+ * @param commit_id the commit id for this change
+ * @param summary the commit message for this change
+ * @param body optional text to include above the diffstat
+ * @param author the person who authored this commit
+ * @param opts email creation options
+ */
+GIT_EXTERN(int) git_email_create_from_diff(
+	git_buf *out,
+	git_diff *diff,
+	size_t patch_idx,
+	size_t patch_count,
+	const git_oid *commit_id,
+	const char *summary,
+	const char *body,
+	const git_signature *author,
+	const git_email_create_options *opts);
+
+/** @} */
+GIT_END_DECL
+#endif
diff --git a/src/email.c b/src/email.c
index b269dee..b238f5b 100644
--- a/src/email.c
+++ b/src/email.c
@@ -38,9 +38,6 @@ static int append_prefix(
 	const char *subject_prefix = opts->subject_prefix ?
 		opts->subject_prefix : "PATCH";
 
-	if (!include_prefix(patch_count, opts))
-		return 0;
-
 	git_buf_putc(out, '[');
 
 	if (*subject_prefix)
@@ -66,47 +63,65 @@ static int append_prefix(
 		               patch_count + (start_number - 1));
 	}
 
-	git_buf_puts(out, "] ");
+	git_buf_puts(out, "]");
 
 	return git_buf_oom(out) ? -1 : 0;
 }
 
 static int append_subject(
 	git_buf *out,
-	git_commit *commit,
 	size_t patch_idx,
 	size_t patch_count,
+	const char *summary,
 	git_email_create_options *opts)
 {
+	bool prefix = include_prefix(patch_count, opts);
+	size_t summary_len = summary ? strlen(summary) : 0;
 	int error;
 
-	if ((error = git_buf_puts(out, "Subject: ")) < 0 ||
-	    (error = append_prefix(out, patch_idx, patch_count, opts)) < 0 ||
-	    (error = git_buf_puts(out, git_commit_summary(commit))) < 0 ||
-	    (error = git_buf_putc(out, '\n')) < 0)
+	if (summary_len) {
+		const char *nl = strchr(summary, '\n');
+
+		if (nl)
+			summary_len = (nl - summary);
+	}
+
+	if ((error = git_buf_puts(out, "Subject: ")) < 0)
 		return error;
 
-	return 0;
+	if (prefix &&
+	    (error = append_prefix(out, patch_idx, patch_count, opts)) < 0)
+		return error;
+
+	if (prefix && summary_len && (error = git_buf_putc(out, ' ')) < 0)
+		return error;
+
+	if (summary_len &&
+	    (error = git_buf_put(out, summary, summary_len)) < 0)
+		return error;
+
+	return git_buf_putc(out, '\n');
 }
 
 static int append_header(
 	git_buf *out,
-	git_commit *commit,
 	size_t patch_idx,
 	size_t patch_count,
+	const git_oid *commit_id,
+	const char *summary,
+	const git_signature *author,
 	git_email_create_options *opts)
 {
-	const git_signature *author = git_commit_author(commit);
 	char id[GIT_OID_HEXSZ];
 	char date[GIT_DATE_RFC2822_SZ];
 	int error;
 
-	if ((error = git_oid_fmt(id, git_commit_id(commit))) < 0 ||
+	if ((error = git_oid_fmt(id, commit_id)) < 0 ||
 	    (error = git_buf_printf(out, "From %.*s %s\n", GIT_OID_HEXSZ, id, EMAIL_TIMESTAMP)) < 0 ||
 	    (error = git_buf_printf(out, "From: %s <%s>\n", author->name, author->email)) < 0 ||
 	    (error = git__date_rfc2822_fmt(date, sizeof(date), &author->when)) < 0 ||
 	    (error = git_buf_printf(out, "Date: %s\n", date)) < 0 ||
-	    (error = append_subject(out, commit, patch_idx, patch_count, opts)) < 0)
+	    (error = append_subject(out, patch_idx, patch_count, summary, opts)) < 0)
 		return error;
 
 	if ((error = git_buf_putc(out, '\n')) < 0)
@@ -115,9 +130,8 @@ static int append_header(
 	return 0;
 }
 
-static int append_body(git_buf *out, git_commit *commit)
+static int append_body(git_buf *out, const char *body)
 {
-	const char *body = git_commit_body(commit);
 	size_t body_len;
 	int error;
 
@@ -173,18 +187,25 @@ static int append_patches(git_buf *out, git_diff *diff)
 	return error;
 }
 
-int git_email_create_from_commit(
+int git_email_create_from_diff(
 	git_buf *out,
-	git_commit *commit,
+	git_diff *diff,
+	size_t patch_idx,
+	size_t patch_count,
+	const git_oid *commit_id,
+	const char *summary,
+	const char *body,
+	const git_signature *author,
 	const git_email_create_options *given_opts)
 {
-	git_diff *diff = NULL;
 	git_email_create_options opts = GIT_EMAIL_CREATE_OPTIONS_INIT;
-	git_repository *repo;
-	int error = 0;
+	int error;
 
 	GIT_ASSERT_ARG(out);
-	GIT_ASSERT_ARG(commit);
+	GIT_ASSERT_ARG(diff);
+	GIT_ASSERT_ARG(!patch_idx || patch_idx <= patch_count);
+	GIT_ASSERT_ARG(commit_id);
+	GIT_ASSERT_ARG(author);
 
 	GIT_ERROR_CHECK_VERSION(given_opts,
 		GIT_EMAIL_CREATE_OPTIONS_VERSION,
@@ -196,16 +217,49 @@ int git_email_create_from_commit(
 	git_buf_sanitize(out);
 	git_buf_clear(out);
 
-	repo = git_commit_owner(commit);
-
-	if ((error = git_diff__commit(&diff, repo, commit, &opts.diff_opts)) == 0 &&
-	    (error = append_header(out, commit, 1, 1, &opts)) == 0 &&
-	    (error = append_body(out, commit)) == 0 &&
+	if ((error = append_header(out, patch_idx, patch_count, commit_id, summary, author, &opts)) == 0 &&
+	    (error = append_body(out, body)) == 0 &&
 	    (error = git_buf_puts(out, "---\n")) == 0 &&
 	    (error = append_diffstat(out, diff)) == 0 &&
 	    (error = append_patches(out, diff)) == 0)
 		error = git_buf_puts(out, "--\nlibgit2 " LIBGIT2_VERSION "\n\n");
 
+	return error;
+}
+
+int git_email_create_from_commit(
+	git_buf *out,
+	git_commit *commit,
+	const git_email_create_options *opts)
+{
+	const git_diff_options *diff_opts;
+	git_diff *diff = NULL;
+	git_repository *repo;
+	const git_signature *author;
+	const char *summary, *body;
+	const git_oid *commit_id;
+	int error = -1;
+
+	GIT_ASSERT_ARG(out);
+	GIT_ASSERT_ARG(commit);
+
+	GIT_ERROR_CHECK_VERSION(opts,
+		GIT_EMAIL_CREATE_OPTIONS_VERSION,
+		"git_email_create_options");
+
+	repo = git_commit_owner(commit);
+	author = git_commit_author(commit);
+	summary = git_commit_summary(commit);
+	body = git_commit_body(commit);
+	commit_id = git_commit_id(commit);
+	diff_opts = opts ? &opts->diff_opts : NULL;
+
+	if ((error = git_diff__commit(&diff, repo, commit, diff_opts)) < 0)
+		goto done;
+
+	error = git_email_create_from_diff(out, diff, 1, 1, commit_id, summary, body, author, opts);
+
+done:
 	git_diff_free(diff);
 	return error;
 }
diff --git a/tests/email/create.c b/tests/email/create.c
index 3a17ff5..2b16d1d 100644
--- a/tests/email/create.c
+++ b/tests/email/create.c
@@ -2,6 +2,7 @@
 #include "clar_libgit2.h"
 
 #include "buffer.h"
+#include "diff_generate.h"
 
 static git_repository *repo;
 
@@ -111,27 +112,60 @@ void test_email_create__commit(void)
 		email, "9264b96c6d104d0e07ae33d3007b6a48246c6f92", NULL);
 }
 
-void test_email_create__mode_change(void)
+void test_email_create__custom_summary_and_body(void)
 {
-	const char *expected =
-	"From 7ade76dd34bba4733cf9878079f9fd4a456a9189 Mon Sep 17 00:00:00 2001\n" \
-	"From: Jacques Germishuys <jacquesg@striata.com>\n" \
-	"Date: Thu, 10 Apr 2014 10:05:03 +0200\n" \
-	"Subject: [PATCH] Update permissions\n" \
+	const char *expected = "From 627e7e12d87e07a83fad5b6bfa25e86ead4a5270 Mon Sep 17 00:00:00 2001\n" \
+	"From: Patrick Steinhardt <ps@pks.im>\n" \
+	"Date: Tue, 24 Nov 2015 13:34:39 +0100\n" \
+	"Subject: [PPPPPATCH 2/4] This is a subject\n" \
+	"\n" \
+	"Modify content of file3.txt by appending a new line. Make this\n" \
+	"commit message somewhat longer to test behavior with newlines\n" \
+	"embedded in the message body.\n" \
 	"\n" \
+	"Also test if new paragraphs are included correctly.\n" \
 	"---\n" \
-	" file1.txt.renamed | 0\n" \
-	" 1 file changed, 0 insertions(+), 0 deletions(-)\n" \
-	" mode change 100644 => 100755 file1.txt.renamed\n" \
+	" file3.txt | 1 +\n" \
+	" 1 file changed, 1 insertion(+)\n" \
 	"\n" \
-	"diff --git a/file1.txt.renamed b/file1.txt.renamed\n" \
-	"old mode 100644\n" \
-	"new mode 100755\n" \
+	"diff --git a/file3.txt b/file3.txt\n" \
+	"index 9a2d780..7309653 100644\n" \
+	"--- a/file3.txt\n" \
+	"+++ b/file3.txt\n" \
+	"@@ -3,3 +3,4 @@ file3!\n" \
+	" file3\n" \
+	" file3\n" \
+	" file3\n" \
+	"+file3\n" \
 	"--\n" \
 	"libgit2 " LIBGIT2_VERSION "\n" \
 	"\n";
 
-	assert_email_match(expected, "7ade76dd34bba4733cf9878079f9fd4a456a9189", NULL);
+	const char *summary = "This is a subject\nwith\nnewlines";
+	const char *body = "Modify content of file3.txt by appending a new line. Make this\n" \
+	"commit message somewhat longer to test behavior with newlines\n" \
+	"embedded in the message body.\n" \
+	"\n" \
+	"Also test if new paragraphs are included correctly.";
+
+	git_oid oid;
+	git_commit *commit = NULL;
+	git_diff *diff = NULL;
+	git_buf buf = GIT_BUF_INIT;
+	git_email_create_options opts = GIT_EMAIL_CREATE_OPTIONS_INIT;
+
+	opts.subject_prefix = "PPPPPATCH";
+
+	git_oid_fromstr(&oid, "627e7e12d87e07a83fad5b6bfa25e86ead4a5270");
+	cl_git_pass(git_commit_lookup(&commit, repo, &oid));
+	cl_git_pass(git_diff__commit(&diff, repo, commit, NULL));
+	cl_git_pass(git_email_create_from_diff(&buf, diff, 2, 4, &oid, summary, body, git_commit_author(commit), &opts));
+
+	cl_assert_equal_s(expected, git_buf_cstr(&buf));
+
+	git_diff_free(diff);
+	git_commit_free(commit);
+	git_buf_dispose(&buf);
 }
 
 void test_email_create__commit_subjects(void)