diff options
author | Patrick Steinhardt <ps@pks.im> | 2019-08-09 09:01:56 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-08-09 09:01:56 +0200 |
commit | b0692d6b3e818b9389295d7d33a0601143cc0c16 (patch) | |
tree | 1ab0ec22115183c7215618a36b62f20e807281c2 | |
parent | f627ba6c7f4b40d533cc127f408cbce8353697ed (diff) | |
parent | 998f9c15fdca34bbfe6a3d92093afe9c7f886dcf (diff) | |
download | libgit2-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.h | 24 | ||||
-rw-r--r-- | include/git2/rebase.h | 18 | ||||
-rw-r--r-- | src/commit.c | 19 | ||||
-rw-r--r-- | src/rebase.c | 45 | ||||
-rw-r--r-- | tests/rebase/sign.c | 243 |
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); +} |