diff options
-rw-r--r-- | include/git2.h | 1 | ||||
-rw-r--r-- | include/git2/email.h | 106 | ||||
-rw-r--r-- | src/email.c | 211 | ||||
-rw-r--r-- | tests/email/create.c | 163 |
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); +} |