summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEdward Thomson <ethomson@edwardthomson.com>2021-09-12 17:06:14 -0400
committerEdward Thomson <ethomson@edwardthomson.com>2021-09-18 08:32:41 -0400
commit75d4676a64a73be937916361d2f0471055dec0e7 (patch)
tree483b634cec5e6e003d8aa0443cdd15e4ff1ee182
parentaa993f76ca556b06194e7da0f56afcf30cb5ee5e (diff)
downloadlibgit2-75d4676a64a73be937916361d2f0471055dec0e7.tar.gz
email: introduce `git_email_create_from_commit`
Create `git_email_*` which will encapsulate email creation and application, and `git_email_create_from_commit` in particular, which creates an email for a single commit.
-rw-r--r--include/git2.h1
-rw-r--r--include/git2/email.h106
-rw-r--r--src/email.c211
-rw-r--r--tests/email/create.c163
4 files changed, 481 insertions, 0 deletions
diff --git a/include/git2.h b/include/git2.h
index f39d7fbe2..2961cc3e5 100644
--- a/include/git2.h
+++ b/include/git2.h
@@ -26,6 +26,7 @@
#include "git2/deprecated.h"
#include "git2/describe.h"
#include "git2/diff.h"
+#include "git2/email.h"
#include "git2/errors.h"
#include "git2/filter.h"
#include "git2/global.h"
diff --git a/include/git2/email.h b/include/git2/email.h
new file mode 100644
index 000000000..6014c6c7c
--- /dev/null
+++ b/include/git2/email.h
@@ -0,0 +1,106 @@
+/*
+ * 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_git_email_h__
+#define INCLUDE_git_email_h__
+
+#include "common.h"
+
+/**
+ * @file git2/email.h
+ * @brief Git email formatting and application routines.
+ * @ingroup Git
+ * @{
+ */
+GIT_BEGIN_DECL
+
+/**
+ * Formatting options for diff e-mail generation
+ */
+typedef enum {
+ /** Normal patch, the default */
+ GIT_EMAIL_CREATE_DEFAULT = 0,
+
+ /** Do not include patch numbers in the subject prefix. */
+ GIT_EMAIL_CREATE_OMIT_NUMBERS = (1u << 0),
+
+ /**
+ * Include numbers in the subject prefix even when the
+ * patch is for a single commit (1/1).
+ */
+ GIT_EMAIL_CREATE_ALWAYS_NUMBER = (1u << 1),
+} git_email_create_flags_t;
+
+/**
+ * Options for controlling the formatting of the generated e-mail.
+ */
+typedef struct {
+ unsigned int version;
+
+ /** see `git_email_create_flags_t` above */
+ uint32_t flags;
+
+ /** Options to use when creating diffs */
+ git_diff_options diff_opts;
+
+ /**
+ * The subject prefix, by default "PATCH". If set to an empty
+ * string ("") then only the patch numbers will be shown in the
+ * prefix. If the subject_prefix is empty and patch numbers
+ * are not being shown, the prefix will be omitted entirely.
+ */
+ const char *subject_prefix;
+
+ /**
+ * The starting patch number; this cannot be 0. By default,
+ * this is 1.
+ */
+ size_t start_number;
+
+ /** The "re-roll" number. By default, there is no re-roll. */
+ size_t reroll_number;
+} git_email_create_options;
+
+#define GIT_EMAIL_CREATE_OPTIONS_VERSION 1
+#define GIT_EMAIL_CREATE_OPTIONS_INIT { \
+ GIT_EMAIL_CREATE_OPTIONS_VERSION, \
+ GIT_EMAIL_CREATE_DEFAULT, \
+ GIT_DIFF_OPTIONS_INIT \
+ }
+
+/**
+ * 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 opts email creation options
+ */
+GIT_EXTERN(int) git_email_create_from_commit(
+ git_buf *out,
+ git_commit *commit,
+ const git_email_create_options *opts);
+
+/**
+ * Create an mbox format diff for the given commits in the revision
+ * spec, excluding merge commits.
+ *
+ * @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 opts email creation options
+ */
+GIT_EXTERN(int) git_email_create_from_commits(
+ git_strarray *out,
+ git_commit **commits,
+ size_t len,
+ const git_email_create_options *opts);
+
+GIT_END_DECL
+
+/** @} */
+
+#endif
diff --git a/src/email.c b/src/email.c
new file mode 100644
index 000000000..b269dee67
--- /dev/null
+++ b/src/email.c
@@ -0,0 +1,211 @@
+/*
+ * 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.
+ */
+
+#include "buffer.h"
+#include "common.h"
+#include "diff_generate.h"
+
+#include "git2/email.h"
+#include "git2/patch.h"
+#include "git2/version.h"
+
+/*
+ * Git uses a "magic" timestamp to indicate that an email message
+ * is from `git format-patch` (or our equivalent).
+ */
+#define EMAIL_TIMESTAMP "Mon Sep 17 00:00:00 2001"
+
+GIT_INLINE(int) include_prefix(
+ size_t patch_count,
+ git_email_create_options *opts)
+{
+ return ((!opts->subject_prefix || *opts->subject_prefix) ||
+ (opts->flags & GIT_EMAIL_CREATE_ALWAYS_NUMBER) != 0 ||
+ opts->reroll_number ||
+ (patch_count > 1 && !(opts->flags & GIT_EMAIL_CREATE_OMIT_NUMBERS)));
+}
+
+static int append_prefix(
+ git_buf *out,
+ size_t patch_idx,
+ size_t patch_count,
+ git_email_create_options *opts)
+{
+ 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)
+ git_buf_puts(out, subject_prefix);
+
+ if (opts->reroll_number) {
+ if (*subject_prefix)
+ git_buf_putc(out, ' ');
+
+ git_buf_printf(out, "v%" PRIuZ, opts->reroll_number);
+ }
+
+ if ((opts->flags & GIT_EMAIL_CREATE_ALWAYS_NUMBER) != 0 ||
+ (patch_count > 1 && !(opts->flags & GIT_EMAIL_CREATE_OMIT_NUMBERS))) {
+ size_t start_number = opts->start_number ?
+ opts->start_number : 1;
+
+ if (*subject_prefix || opts->reroll_number)
+ git_buf_putc(out, ' ');
+
+ git_buf_printf(out, "%" PRIuZ "/%" PRIuZ,
+ patch_idx + (start_number - 1),
+ patch_count + (start_number - 1));
+ }
+
+ 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,
+ git_email_create_options *opts)
+{
+ 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)
+ return error;
+
+ return 0;
+}
+
+static int append_header(
+ git_buf *out,
+ git_commit *commit,
+ size_t patch_idx,
+ size_t patch_count,
+ 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 ||
+ (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)
+ return error;
+
+ if ((error = git_buf_putc(out, '\n')) < 0)
+ return error;
+
+ return 0;
+}
+
+static int append_body(git_buf *out, git_commit *commit)
+{
+ const char *body = git_commit_body(commit);
+ size_t body_len;
+ int error;
+
+ if (!body)
+ return 0;
+
+ body_len = strlen(body);
+
+ if ((error = git_buf_puts(out, body)) < 0)
+ return error;
+
+ if (body_len && body[body_len - 1] != '\n')
+ error = git_buf_putc(out, '\n');
+
+ return error;
+}
+
+static int append_diffstat(git_buf *out, git_diff *diff)
+{
+ git_diff_stats *stats = NULL;
+ unsigned int format_flags;
+ int error;
+
+ format_flags = GIT_DIFF_STATS_FULL | GIT_DIFF_STATS_INCLUDE_SUMMARY;
+
+ if ((error = git_diff_get_stats(&stats, diff)) == 0 &&
+ (error = git_diff_stats_to_buf(out, stats, format_flags, 0)) == 0)
+ error = git_buf_putc(out, '\n');
+
+ git_diff_stats_free(stats);
+ return error;
+}
+
+static int append_patches(git_buf *out, git_diff *diff)
+{
+ size_t i, deltas;
+ int error = 0;
+
+ deltas = git_diff_num_deltas(diff);
+
+ for (i = 0; i < deltas; ++i) {
+ git_patch *patch = NULL;
+
+ if ((error = git_patch_from_diff(&patch, diff, i)) >= 0)
+ error = git_patch_to_buf(out, patch);
+
+ git_patch_free(patch);
+
+ if (error < 0)
+ break;
+ }
+
+ return error;
+}
+
+int git_email_create_from_commit(
+ git_buf *out,
+ git_commit *commit,
+ 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;
+
+ GIT_ASSERT_ARG(out);
+ GIT_ASSERT_ARG(commit);
+
+ GIT_ERROR_CHECK_VERSION(given_opts,
+ GIT_EMAIL_CREATE_OPTIONS_VERSION,
+ "git_email_create_options");
+
+ if (given_opts)
+ memcpy(&opts, given_opts, sizeof(git_email_create_options));
+
+ 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 &&
+ (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");
+
+ git_diff_free(diff);
+ return error;
+}
diff --git a/tests/email/create.c b/tests/email/create.c
new file mode 100644
index 000000000..3a17ff59f
--- /dev/null
+++ b/tests/email/create.c
@@ -0,0 +1,163 @@
+#include "clar.h"
+#include "clar_libgit2.h"
+
+#include "buffer.h"
+
+static git_repository *repo;
+
+void test_email_create__initialize(void)
+{
+ repo = cl_git_sandbox_init("diff_format_email");
+}
+
+void test_email_create__cleanup(void)
+{
+ cl_git_sandbox_cleanup();
+}
+
+static void email_for_commit(
+ git_buf *out,
+ const char *commit_id,
+ git_email_create_options *opts)
+{
+ git_oid oid;
+ git_commit *commit = NULL;
+ git_diff *diff = NULL;
+
+ git_oid_fromstr(&oid, commit_id);
+
+ cl_git_pass(git_commit_lookup(&commit, repo, &oid));
+
+ cl_git_pass(git_email_create_from_commit(out, commit, opts));
+
+ git_diff_free(diff);
+ git_commit_free(commit);
+}
+
+static void assert_email_match(
+ const char *expected,
+ const char *commit_id,
+ git_email_create_options *opts)
+{
+ git_buf buf = GIT_BUF_INIT;
+
+ email_for_commit(&buf, commit_id, opts);
+ cl_assert_equal_s(expected, git_buf_cstr(&buf));
+
+ git_buf_dispose(&buf);
+}
+
+static void assert_subject_match(
+ const char *expected,
+ const char *commit_id,
+ git_email_create_options *opts)
+{
+ git_buf buf = GIT_BUF_INIT;
+ const char *loc;
+
+ email_for_commit(&buf, commit_id, opts);
+
+ cl_assert((loc = strstr(buf.ptr, "\nSubject: ")) != NULL);
+ git_buf_consume(&buf, (loc + 10));
+ git_buf_truncate_at_char(&buf, '\n');
+
+ cl_assert_equal_s(expected, git_buf_cstr(&buf));
+
+ git_buf_dispose(&buf);
+}
+
+void test_email_create__commit(void)
+{
+ const char *email =
+ "From 9264b96c6d104d0e07ae33d3007b6a48246c6f92 Mon Sep 17 00:00:00 2001\n" \
+ "From: Jacques Germishuys <jacquesg@striata.com>\n" \
+ "Date: Wed, 9 Apr 2014 20:57:01 +0200\n" \
+ "Subject: [PATCH] Modify some content\n" \
+ "\n" \
+ "---\n" \
+ " file1.txt | 8 +++++---\n" \
+ " 1 file changed, 5 insertions(+), 3 deletions(-)\n" \
+ "\n" \
+ "diff --git a/file1.txt b/file1.txt\n" \
+ "index 94aaae8..af8f41d 100644\n" \
+ "--- a/file1.txt\n" \
+ "+++ b/file1.txt\n" \
+ "@@ -1,15 +1,17 @@\n" \
+ " file1.txt\n" \
+ " file1.txt\n" \
+ "+_file1.txt_\n" \
+ " file1.txt\n" \
+ " file1.txt\n" \
+ " file1.txt\n" \
+ " file1.txt\n" \
+ "+\n" \
+ "+\n" \
+ " file1.txt\n" \
+ " file1.txt\n" \
+ " file1.txt\n" \
+ " file1.txt\n" \
+ " file1.txt\n" \
+ "-file1.txt\n" \
+ "-file1.txt\n" \
+ "-file1.txt\n" \
+ "+_file1.txt_\n" \
+ "+_file1.txt_\n" \
+ " file1.txt\n" \
+ "--\n" \
+ "libgit2 " LIBGIT2_VERSION "\n" \
+ "\n";
+
+ assert_email_match(
+ email, "9264b96c6d104d0e07ae33d3007b6a48246c6f92", NULL);
+}
+
+void test_email_create__mode_change(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" \
+ "\n" \
+ "---\n" \
+ " file1.txt.renamed | 0\n" \
+ " 1 file changed, 0 insertions(+), 0 deletions(-)\n" \
+ " mode change 100644 => 100755 file1.txt.renamed\n" \
+ "\n" \
+ "diff --git a/file1.txt.renamed b/file1.txt.renamed\n" \
+ "old mode 100644\n" \
+ "new mode 100755\n" \
+ "--\n" \
+ "libgit2 " LIBGIT2_VERSION "\n" \
+ "\n";
+
+ assert_email_match(expected, "7ade76dd34bba4733cf9878079f9fd4a456a9189", NULL);
+}
+
+void test_email_create__commit_subjects(void)
+{
+ git_email_create_options opts = GIT_EMAIL_CREATE_OPTIONS_INIT;
+
+ assert_subject_match("[PATCH] Modify some content", "9264b96c6d104d0e07ae33d3007b6a48246c6f92", &opts);
+
+ opts.reroll_number = 42;
+ assert_subject_match("[PATCH v42] Modify some content", "9264b96c6d104d0e07ae33d3007b6a48246c6f92", &opts);
+
+ opts.flags |= GIT_EMAIL_CREATE_ALWAYS_NUMBER;
+ assert_subject_match("[PATCH v42 1/1] Modify some content", "9264b96c6d104d0e07ae33d3007b6a48246c6f92", &opts);
+
+ opts.start_number = 9;
+ assert_subject_match("[PATCH v42 9/9] Modify some content", "9264b96c6d104d0e07ae33d3007b6a48246c6f92", &opts);
+
+ opts.subject_prefix = "";
+ assert_subject_match("[v42 9/9] Modify some content", "9264b96c6d104d0e07ae33d3007b6a48246c6f92", &opts);
+
+ opts.reroll_number = 0;
+ assert_subject_match("[9/9] Modify some content", "9264b96c6d104d0e07ae33d3007b6a48246c6f92", &opts);
+
+ opts.start_number = 0;
+ assert_subject_match("[1/1] Modify some content", "9264b96c6d104d0e07ae33d3007b6a48246c6f92", &opts);
+
+ opts.flags = GIT_EMAIL_CREATE_OMIT_NUMBERS;
+ assert_subject_match("Modify some content", "9264b96c6d104d0e07ae33d3007b6a48246c6f92", &opts);
+}