summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPatrick Steinhardt <ps@pks.im>2019-08-09 09:01:56 +0200
committerGitHub <noreply@github.com>2019-08-09 09:01:56 +0200
commitb0692d6b3e818b9389295d7d33a0601143cc0c16 (patch)
tree1ab0ec22115183c7215618a36b62f20e807281c2
parentf627ba6c7f4b40d533cc127f408cbce8353697ed (diff)
parent998f9c15fdca34bbfe6a3d92093afe9c7f886dcf (diff)
downloadlibgit2-b0692d6b3e818b9389295d7d33a0601143cc0c16.tar.gz
Merge pull request #4913 from implausible/feature/signing-rebase-commits
Add sign capability to git_rebase_commit
-rw-r--r--include/git2/commit.h24
-rw-r--r--include/git2/rebase.h18
-rw-r--r--src/commit.c19
-rw-r--r--src/rebase.c45
-rw-r--r--tests/rebase/sign.c243
5 files changed, 335 insertions, 14 deletions
diff --git a/include/git2/commit.h b/include/git2/commit.h
index 7e0409cc7..e6c4656a9 100644
--- a/include/git2/commit.h
+++ b/include/git2/commit.h
@@ -480,7 +480,8 @@ GIT_EXTERN(int) git_commit_create_buffer(
*
* @param out the resulting commit id
* @param commit_content the content of the unsigned commit object
- * @param signature the signature to add to the commit
+ * @param signature the signature to add to the commit. Leave `NULL`
+ * to create a commit without adding a signature field.
* @param signature_field which header field should contain this
* signature. Leave `NULL` for the default of "gpgsig"
* @return 0 or an error code
@@ -501,6 +502,27 @@ GIT_EXTERN(int) git_commit_create_with_signature(
*/
GIT_EXTERN(int) git_commit_dup(git_commit **out, git_commit *source);
+/**
+ * Commit signing callback.
+ *
+ * The callback will be called with the commit content, giving a user an
+ * opportunity to sign the commit content. The signature_field
+ * buf may be left empty to specify the default field "gpgsig".
+ *
+ * Signatures can take the form of any string, and can be created on an arbitrary
+ * header field. Signatures are most commonly used for verifying authorship of a
+ * commit using GPG or a similar cryptographically secure signing algorithm.
+ * See https://git-scm.com/book/en/v2/Git-Tools-Signing-Your-Work for more
+ * details.
+ *
+ * When the callback:
+ * - returns GIT_PASSTHROUGH, no signature will be added to the commit.
+ * - returns < 0, commit creation will be aborted.
+ * - returns GIT_OK, the signature parameter is expected to be filled.
+ */
+typedef int (*git_commit_signing_cb)(
+ git_buf *signature, git_buf *signature_field, const char *commit_content, void *payload);
+
/** @} */
GIT_END_DECL
#endif
diff --git a/include/git2/rebase.h b/include/git2/rebase.h
index 011d3e119..99a02fef9 100644
--- a/include/git2/rebase.h
+++ b/include/git2/rebase.h
@@ -13,6 +13,7 @@
#include "annotated_commit.h"
#include "merge.h"
#include "checkout.h"
+#include "commit.h"
/**
* @file git2/rebase.h
@@ -72,6 +73,21 @@ typedef struct {
* `abort` to match git semantics.
*/
git_checkout_options checkout_options;
+
+ /**
+ * If provided, this will be called with the commit content, allowing
+ * a signature to be added to the rebase commit. Can be skipped with
+ * GIT_PASSTHROUGH. If GIT_PASSTHROUGH is returned, a commit will be made
+ * without a signature.
+ * This field is only used when performing git_rebase_commit.
+ */
+ git_commit_signing_cb signing_cb;
+
+ /**
+ * This will be passed to each of the callbacks in this struct
+ * as the last parameter.
+ */
+ void *payload;
} git_rebase_options;
/**
@@ -118,7 +134,7 @@ typedef enum {
#define GIT_REBASE_OPTIONS_VERSION 1
#define GIT_REBASE_OPTIONS_INIT \
{ GIT_REBASE_OPTIONS_VERSION, 0, 0, NULL, GIT_MERGE_OPTIONS_INIT, \
- GIT_CHECKOUT_OPTIONS_INIT}
+ GIT_CHECKOUT_OPTIONS_INIT, NULL, NULL }
/** Indicates that a rebase operation is not (yet) in progress. */
#define GIT_REBASE_NO_OPERATION SIZE_MAX
diff --git a/src/commit.c b/src/commit.c
index 513fdccaf..d5f19df65 100644
--- a/src/commit.c
+++ b/src/commit.c
@@ -80,8 +80,8 @@ on_error:
}
static int validate_tree_and_parents(git_array_oid_t *parents, git_repository *repo, const git_oid *tree,
- git_commit_parent_callback parent_cb, void *parent_payload,
- const git_oid *current_id, bool validate)
+ git_commit_parent_callback parent_cb, void *parent_payload,
+ const git_oid *current_id, bool validate)
{
size_t i;
int error;
@@ -152,8 +152,8 @@ static int git_commit__create_internal(
goto cleanup;
error = git_commit__create_buffer_internal(&buf, author, committer,
- message_encoding, message, tree,
- &parents);
+ message_encoding, message, tree,
+ &parents);
if (error < 0)
goto cleanup;
@@ -582,7 +582,7 @@ const char *git_commit_body(git_commit *commit)
break;
if (*msg)
- commit->body = git__strndup(msg, end - msg + 1);
+ commit->body = git__strndup(msg, end - msg + 1);
}
return commit->body;
@@ -876,12 +876,15 @@ int git_commit_create_with_signature(
return -1;
}
- field = signature_field ? signature_field : "gpgsig";
-
/* The header ends after the first LF */
header_end++;
git_buf_put(&commit, commit_content, header_end - commit_content);
- format_header_field(&commit, field, signature);
+
+ if (signature != NULL) {
+ field = signature_field ? signature_field : "gpgsig";
+ format_header_field(&commit, field, signature);
+ }
+
git_buf_puts(&commit, header_end);
if (git_buf_oom(&commit))
diff --git a/src/rebase.c b/src/rebase.c
index fbba4bcb3..d171fa2aa 100644
--- a/src/rebase.c
+++ b/src/rebase.c
@@ -951,6 +951,10 @@ static int rebase_commit__create(
git_commit *current_commit = NULL, *commit = NULL;
git_tree *parent_tree = NULL, *tree = NULL;
git_oid tree_id, commit_id;
+ git_buf commit_content = GIT_BUF_INIT, commit_signature = GIT_BUF_INIT,
+ signature_field = GIT_BUF_INIT;
+ const char *signature_field_string = NULL,
+ *commit_signature_string = NULL;
int error;
operation = git_array_get(rebase->operations, rebase->current);
@@ -981,10 +985,40 @@ static int rebase_commit__create(
message = git_commit_message(current_commit);
}
- if ((error = git_commit_create(&commit_id, rebase->repo, NULL, author,
- committer, message_encoding, message, tree, 1,
- (const git_commit **)&parent_commit)) < 0 ||
- (error = git_commit_lookup(&commit, rebase->repo, &commit_id)) < 0)
+ if ((error = git_commit_create_buffer(&commit_content, rebase->repo, author, committer,
+ message_encoding, message, tree, 1, (const git_commit **)&parent_commit)) < 0)
+ goto done;
+
+ if (rebase->options.signing_cb) {
+ git_error_clear();
+ error = git_error_set_after_callback_function(rebase->options.signing_cb(
+ &commit_signature, &signature_field, git_buf_cstr(&commit_content),
+ rebase->options.payload), "commit signing_cb failed");
+ if (error == GIT_PASSTHROUGH) {
+ git_buf_dispose(&commit_signature);
+ git_buf_dispose(&signature_field);
+ git_error_clear();
+ error = GIT_OK;
+ } else if (error < 0)
+ goto done;
+ }
+
+ if (git_buf_is_allocated(&commit_signature)) {
+ assert(git_buf_contains_nul(&commit_signature));
+ commit_signature_string = git_buf_cstr(&commit_signature);
+ }
+
+ if (git_buf_is_allocated(&signature_field)) {
+ assert(git_buf_contains_nul(&signature_field));
+ signature_field_string = git_buf_cstr(&signature_field);
+ }
+
+ if ((error = git_commit_create_with_signature(&commit_id, rebase->repo,
+ git_buf_cstr(&commit_content), commit_signature_string,
+ signature_field_string)))
+ goto done;
+
+ if ((error = git_commit_lookup(&commit, rebase->repo, &commit_id)) < 0)
goto done;
*out = commit;
@@ -993,6 +1027,9 @@ done:
if (error < 0)
git_commit_free(commit);
+ git_buf_dispose(&commit_signature);
+ git_buf_dispose(&signature_field);
+ git_buf_dispose(&commit_content);
git_commit_free(current_commit);
git_tree_free(parent_tree);
git_tree_free(tree);
diff --git a/tests/rebase/sign.c b/tests/rebase/sign.c
new file mode 100644
index 000000000..fa4776661
--- /dev/null
+++ b/tests/rebase/sign.c
@@ -0,0 +1,243 @@
+#include "clar_libgit2.h"
+#include "git2/rebase.h"
+
+static git_repository *repo;
+static git_signature *signature;
+
+/* Fixture setup and teardown */
+void test_rebase_sign__initialize(void)
+{
+ repo = cl_git_sandbox_init("rebase");
+ cl_git_pass(git_signature_new(&signature, "Rebaser",
+ "rebaser@rebaser.rb", 1405694510, 0));
+}
+
+void test_rebase_sign__cleanup(void)
+{
+ git_signature_free(signature);
+ cl_git_sandbox_cleanup();
+}
+
+static const char *expected_commit_content = "tree cd99b26250099fc38d30bfaed7797a7275ed3366\n\
+parent f87d14a4a236582a0278a916340a793714256864\n\
+author Edward Thomson <ethomson@edwardthomson.com> 1405625055 -0400\n\
+committer Rebaser <rebaser@rebaser.rb> 1405694510 +0000\n\
+\n\
+Modification 3 to gravy\n";
+
+int signing_cb_passthrough(
+ git_buf *signature,
+ git_buf *signature_field,
+ const char *commit_content,
+ void *payload)
+{
+ cl_assert_equal_b(false, git_buf_is_allocated(signature));
+ cl_assert_equal_b(false, git_buf_is_allocated(signature_field));
+ cl_assert_equal_s(expected_commit_content, commit_content);
+ cl_assert_equal_p(NULL, payload);
+ return GIT_PASSTHROUGH;
+}
+
+/* git checkout gravy ; git rebase --merge veal */
+void test_rebase_sign__passthrough_signing_cb(void)
+{
+ git_rebase *rebase;
+ git_reference *branch_ref, *upstream_ref;
+ git_annotated_commit *branch_head, *upstream_head;
+ git_rebase_operation *rebase_operation;
+ git_oid commit_id, expected_id;
+ git_rebase_options rebase_opts = GIT_REBASE_OPTIONS_INIT;
+ git_commit *commit;
+ const char *expected_commit_raw_header = "tree cd99b26250099fc38d30bfaed7797a7275ed3366\n\
+parent f87d14a4a236582a0278a916340a793714256864\n\
+author Edward Thomson <ethomson@edwardthomson.com> 1405625055 -0400\n\
+committer Rebaser <rebaser@rebaser.rb> 1405694510 +0000\n";
+
+ rebase_opts.signing_cb = signing_cb_passthrough;
+
+ cl_git_pass(git_reference_lookup(&branch_ref, repo, "refs/heads/gravy"));
+ cl_git_pass(git_reference_lookup(&upstream_ref, repo, "refs/heads/veal"));
+
+ cl_git_pass(git_annotated_commit_from_ref(&branch_head, repo, branch_ref));
+ cl_git_pass(git_annotated_commit_from_ref(&upstream_head, repo, upstream_ref));
+
+ cl_git_pass(git_rebase_init(&rebase, repo, branch_head, upstream_head, NULL, &rebase_opts));
+
+ cl_git_pass(git_rebase_next(&rebase_operation, rebase));
+ cl_git_pass(git_rebase_commit(&commit_id, rebase, NULL, signature, NULL, NULL));
+
+ git_oid_fromstr(&expected_id, "129183968a65abd6c52da35bff43325001bfc630");
+ cl_assert_equal_oid(&expected_id, &commit_id);
+
+ cl_git_pass(git_commit_lookup(&commit, repo, &commit_id));
+ cl_assert_equal_s(expected_commit_raw_header, git_commit_raw_header(commit));
+
+ cl_git_fail_with(GIT_ITEROVER, git_rebase_next(&rebase_operation, rebase));
+
+ git_reference_free(branch_ref);
+ git_reference_free(upstream_ref);
+ git_annotated_commit_free(branch_head);
+ git_annotated_commit_free(upstream_head);
+ git_commit_free(commit);
+ git_rebase_free(rebase);
+}
+
+int signing_cb_gpg(
+ git_buf *signature,
+ git_buf *signature_field,
+ const char *commit_content,
+ void *payload)
+{
+ const char *gpg_signature = "-----BEGIN PGP SIGNATURE-----\n\
+\n\
+iQIzBAEBCgAdFiEEgVlDEfSlmKn0fvGgK++h5T2/ctIFAlwZcrAACgkQK++h5T2/\n\
+ctIPVhAA42RyZhMdKl5Bm0KtQco2scsukIg2y7tjSwhti91zDu3HQgpusjjo0fQx\n\
+ZzB+OrmlvQ9CDcGpZ0THIzXD8GRJoDMPqdrvZVrBWkGcHvw7/YPA8skzsjkauJ8W\n\
+7lzF5LCuHSS6OUmPT/+5hEHPin5PB3zhfszyC+Q7aujnIuPJMrKiMnUa+w1HWifM\n\
+km49OOygQ9S6NQoVuEQede22+c76DlDL7yFghGoo1f0sKCE/9LW6SEnwI/bWv9eo\n\
+nom5vOPrvQeJiYCQk+2DyWo8RdSxINtY+G9bPE4RXm+6ZgcXECPm9TYDIWpL36fC\n\
+jvtGLs98woWFElOziBMp5Tb630GMcSI+q5ivHfJ3WS5NKLYLHBNK4iSFN0/dgAnB\n\
+dj6GcKXKWnIBWn6ZM4o40pcM5KSRUUCLtA0ZmjJH4c4zx3X5fUxd+enwkf3e9VZO\n\
+fNKC/+xfq6NfoPUPK9+UnchHpJaJw7RG5tZS+sWCz2xpQ1y3/o49xImNyM3wnpvB\n\
+cRAZabqIHpZa9/DIUkELOtCzln6niqkjRgg3M/YCCNznwV+0RNgz87VtyTPerdef\n\
+xrqn0+ROMF6ebVqIs6PPtuPkxnAJu7TMKXVB5rFnAewS24e6cIGFzeIA7810py3l\n\
+cttVRsdOoego+fiy08eFE+aJIeYiINRGhqOBTsuqG4jIdpdKxPE=\n\
+=KbsY\n\
+-----END PGP SIGNATURE-----";
+
+ cl_assert_equal_b(false, git_buf_is_allocated(signature));
+ cl_assert_equal_b(false, git_buf_is_allocated(signature_field));
+ cl_assert_equal_s(expected_commit_content, commit_content);
+ cl_assert_equal_p(NULL, payload);
+
+ cl_git_pass(git_buf_set(signature, gpg_signature, strlen(gpg_signature) + 1));
+ return GIT_OK;
+}
+
+/* git checkout gravy ; git rebase --merge veal */
+void test_rebase_sign__gpg_with_no_field(void)
+{
+ git_rebase *rebase;
+ git_reference *branch_ref, *upstream_ref;
+ git_annotated_commit *branch_head, *upstream_head;
+ git_rebase_operation *rebase_operation;
+ git_oid commit_id, expected_id;
+ git_rebase_options rebase_opts = GIT_REBASE_OPTIONS_INIT;
+ git_commit *commit;
+ const char *expected_commit_raw_header = "tree cd99b26250099fc38d30bfaed7797a7275ed3366\n\
+parent f87d14a4a236582a0278a916340a793714256864\n\
+author Edward Thomson <ethomson@edwardthomson.com> 1405625055 -0400\n\
+committer Rebaser <rebaser@rebaser.rb> 1405694510 +0000\n\
+gpgsig -----BEGIN PGP SIGNATURE-----\n\
+ \n\
+ iQIzBAEBCgAdFiEEgVlDEfSlmKn0fvGgK++h5T2/ctIFAlwZcrAACgkQK++h5T2/\n\
+ ctIPVhAA42RyZhMdKl5Bm0KtQco2scsukIg2y7tjSwhti91zDu3HQgpusjjo0fQx\n\
+ ZzB+OrmlvQ9CDcGpZ0THIzXD8GRJoDMPqdrvZVrBWkGcHvw7/YPA8skzsjkauJ8W\n\
+ 7lzF5LCuHSS6OUmPT/+5hEHPin5PB3zhfszyC+Q7aujnIuPJMrKiMnUa+w1HWifM\n\
+ km49OOygQ9S6NQoVuEQede22+c76DlDL7yFghGoo1f0sKCE/9LW6SEnwI/bWv9eo\n\
+ nom5vOPrvQeJiYCQk+2DyWo8RdSxINtY+G9bPE4RXm+6ZgcXECPm9TYDIWpL36fC\n\
+ jvtGLs98woWFElOziBMp5Tb630GMcSI+q5ivHfJ3WS5NKLYLHBNK4iSFN0/dgAnB\n\
+ dj6GcKXKWnIBWn6ZM4o40pcM5KSRUUCLtA0ZmjJH4c4zx3X5fUxd+enwkf3e9VZO\n\
+ fNKC/+xfq6NfoPUPK9+UnchHpJaJw7RG5tZS+sWCz2xpQ1y3/o49xImNyM3wnpvB\n\
+ cRAZabqIHpZa9/DIUkELOtCzln6niqkjRgg3M/YCCNznwV+0RNgz87VtyTPerdef\n\
+ xrqn0+ROMF6ebVqIs6PPtuPkxnAJu7TMKXVB5rFnAewS24e6cIGFzeIA7810py3l\n\
+ cttVRsdOoego+fiy08eFE+aJIeYiINRGhqOBTsuqG4jIdpdKxPE=\n\
+ =KbsY\n\
+ -----END PGP SIGNATURE-----\n";
+
+ rebase_opts.signing_cb = signing_cb_gpg;
+
+ cl_git_pass(git_reference_lookup(&branch_ref, repo, "refs/heads/gravy"));
+ cl_git_pass(git_reference_lookup(&upstream_ref, repo, "refs/heads/veal"));
+
+ cl_git_pass(git_annotated_commit_from_ref(&branch_head, repo, branch_ref));
+ cl_git_pass(git_annotated_commit_from_ref(&upstream_head, repo, upstream_ref));
+
+ cl_git_pass(git_rebase_init(&rebase, repo, branch_head, upstream_head, NULL, &rebase_opts));
+
+ cl_git_pass(git_rebase_next(&rebase_operation, rebase));
+ cl_git_pass(git_rebase_commit(&commit_id, rebase, NULL, signature, NULL, NULL));
+
+ git_oid_fromstr(&expected_id, "bf78348e45c8286f52b760f1db15cb6da030f2ef");
+ cl_assert_equal_oid(&expected_id, &commit_id);
+
+ cl_git_pass(git_commit_lookup(&commit, repo, &commit_id));
+ cl_assert_equal_s(expected_commit_raw_header, git_commit_raw_header(commit));
+
+ cl_git_fail_with(GIT_ITEROVER, git_rebase_next(&rebase_operation, rebase));
+
+ git_reference_free(branch_ref);
+ git_reference_free(upstream_ref);
+ git_annotated_commit_free(branch_head);
+ git_annotated_commit_free(upstream_head);
+ git_commit_free(commit);
+ git_rebase_free(rebase);
+}
+
+
+int signing_cb_magic_field(
+ git_buf *signature,
+ git_buf *signature_field,
+ const char *commit_content,
+ void *payload)
+{
+ const char *signature_content = "magic word: pretty please";
+ const char *signature_field_content = "magicsig";
+
+ cl_assert_equal_b(false, git_buf_is_allocated(signature));
+ cl_assert_equal_b(false, git_buf_is_allocated(signature_field));
+ cl_assert_equal_s(expected_commit_content, commit_content);
+ cl_assert_equal_p(NULL, payload);
+
+ cl_git_pass(git_buf_set(signature, signature_content,
+ strlen(signature_content) + 1));
+ cl_git_pass(git_buf_set(signature_field, signature_field_content,
+ strlen(signature_field_content) + 1));
+
+ return GIT_OK;
+}
+
+/* git checkout gravy ; git rebase --merge veal */
+void test_rebase_sign__custom_signature_field(void)
+{
+ git_rebase *rebase;
+ git_reference *branch_ref, *upstream_ref;
+ git_annotated_commit *branch_head, *upstream_head;
+ git_rebase_operation *rebase_operation;
+ git_oid commit_id, expected_id;
+ git_rebase_options rebase_opts = GIT_REBASE_OPTIONS_INIT;
+ git_commit *commit;
+ const char *expected_commit_raw_header = "tree cd99b26250099fc38d30bfaed7797a7275ed3366\n\
+parent f87d14a4a236582a0278a916340a793714256864\n\
+author Edward Thomson <ethomson@edwardthomson.com> 1405625055 -0400\n\
+committer Rebaser <rebaser@rebaser.rb> 1405694510 +0000\n\
+magicsig magic word: pretty please\n";
+
+ rebase_opts.signing_cb = signing_cb_magic_field;
+
+ cl_git_pass(git_reference_lookup(&branch_ref, repo, "refs/heads/gravy"));
+ cl_git_pass(git_reference_lookup(&upstream_ref, repo, "refs/heads/veal"));
+
+ cl_git_pass(git_annotated_commit_from_ref(&branch_head, repo, branch_ref));
+ cl_git_pass(git_annotated_commit_from_ref(&upstream_head, repo, upstream_ref));
+
+ cl_git_pass(git_rebase_init(&rebase, repo, branch_head, upstream_head, NULL, &rebase_opts));
+
+ cl_git_pass(git_rebase_next(&rebase_operation, rebase));
+ cl_git_pass(git_rebase_commit(&commit_id, rebase, NULL, signature, NULL, NULL));
+
+ git_oid_fromstr(&expected_id, "f46a4a8d26ae411b02aa61b7d69576627f4a1e1c");
+ cl_assert_equal_oid(&expected_id, &commit_id);
+
+ cl_git_pass(git_commit_lookup(&commit, repo, &commit_id));
+ cl_assert_equal_s(expected_commit_raw_header, git_commit_raw_header(commit));
+
+ cl_git_fail_with(GIT_ITEROVER, git_rebase_next(&rebase_operation, rebase));
+
+ git_reference_free(branch_ref);
+ git_reference_free(upstream_ref);
+ git_annotated_commit_free(branch_head);
+ git_annotated_commit_free(upstream_head);
+ git_commit_free(commit);
+ git_rebase_free(rebase);
+}