From 05c4b5cf957ea1d6d9c9202ee7902b326cd0918d Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Wed, 13 May 2026 21:58:31 +0100 Subject: [PATCH] commit: introduce `git_commit_amend_from_...` Similar to the `git_commit_create_from_...` APIs, a simple amend function that uses smart defaults and amends HEAD from the staged changes or a given tree. --- include/git2/commit.h | 33 ++++++++++ src/libgit2/commit.c | 94 +++++++++++++++++++++++++++ tests/libgit2/commit/amend.c | 122 +++++++++++++++++++++++++++++++++++ 3 files changed, 249 insertions(+) create mode 100644 tests/libgit2/commit/amend.c diff --git a/include/git2/commit.h b/include/git2/commit.h index c8838e2c8..c5815118a 100644 --- a/include/git2/commit.h +++ b/include/git2/commit.h @@ -612,6 +612,39 @@ GIT_EXTERN(int) git_commit_create_from_tree( const char *message, const git_commit_create_options *opts); +/** + * Amends the HEAD commit in the repository using the staged changes; + * this is a near analog to `git commit --amend -m message`. + * + * @param id pointer to store the new commit's object id + * @param repo repository to commit changes in + * @param message the commit message + * @param opts options for creating the commit + * @return 0 on success or an error code + */ +GIT_EXTERN(int) git_commit_amend_from_stage( + git_oid *id, + git_repository *repo, + const char *message, + const git_commit_create_options *opts); + +/** + * Amends the HEAD commit in the repository using the given tree. + * + * @param id pointer to store the new commit's object id + * @param repo repository to commit changes in + * @param tree tree to point the commit to + * @param message the commit message + * @param opts options for creating the commit + * @return 0 on success or an error code + */ +GIT_EXTERN(int) git_commit_amend_from_tree( + git_oid *id, + git_repository *repo, + const git_tree *tree, + const char *message, + const git_commit_create_options *opts); + /** * Amend an existing commit by replacing only non-NULL values. * diff --git a/src/libgit2/commit.c b/src/libgit2/commit.c index 97c8a1bde..75786f0ea 100644 --- a/src/libgit2/commit.c +++ b/src/libgit2/commit.c @@ -1330,6 +1330,100 @@ int git_commit_create_from_tree( return create_from_tree(out, repo, tree, message, &opts); } +int git_commit_amend_from_stage( + git_oid *out, + git_repository *repo, + const char *message, + const git_commit_create_options *opts) +{ + git_index *index = NULL; + git_oid tree_id; + git_tree *tree = NULL; + int error = 0; + + GIT_ASSERT_ARG(out && repo); + + if (git_repository_index(&index, repo) < 0 || + git_index_write_tree(&tree_id, index) < 0 || + git_tree_lookup(&tree, repo, &tree_id) < 0) { + error = -1; + goto done; + } + + error = git_commit_amend_from_tree(out, repo, tree, message, opts); + +done: + git_tree_free(tree); + git_index_free(index); + return error; +} + +int git_commit_amend_from_tree( + git_oid *out, + git_repository *repo, + const git_tree *tree, + const char *given_message, + const git_commit_create_options *given_opts) +{ + git_commit_create_options opts = GIT_COMMIT_CREATE_OPTIONS_INIT; + git_commit_create_ext_options ext_opts = GIT_COMMIT_CREATE_EXT_OPTIONS_INIT; + git_reference *head_ref = NULL; + git_commit *head_commit = NULL; + git_signature *new_committer = NULL; + const git_signature *author, *committer; + const char *message; + int error; + + GIT_ASSERT_ARG(out && repo && tree); + + if (given_opts) + memcpy(&opts, given_opts, sizeof(git_commit_create_options)); + + if ((error = git_repository_head(&head_ref, repo)) < 0 || + (error = git_reference_peel((git_object **)&head_commit, head_ref, GIT_OBJECT_COMMIT)) < 0) + goto done; + + if (opts.author) { + author = opts.author; + } else { + author = git_commit_author(head_commit); + } + + if (opts.committer) { + committer = opts.committer; + } else { + if ((error = git_signature_default(&new_committer, repo)) < 0) + goto done; + + committer = new_committer; + } + + if (given_message) { + message = given_message; + ext_opts.message_encoding = opts.message_encoding; + } else { + message = git_commit_message(head_commit); + ext_opts.message_encoding = git_commit_message_encoding(head_commit); + } + + error = git_commit__create_internal( + out, repo, author, committer, message, git_tree_id(tree), + commit_parent_for_amend, (void *)head_commit, &ext_opts, + false); + + if (!error) + error = git_reference__update_for_commit( + repo, head_ref, NULL, out, "commit"); + +done: + git_signature_free(new_committer); + git_commit_free(head_commit); + git_reference_free(head_ref); + + return error; +} + + int git_commit_committer_with_mailmap( git_signature **out, const git_commit *commit, const git_mailmap *mailmap) { diff --git a/tests/libgit2/commit/amend.c b/tests/libgit2/commit/amend.c new file mode 100644 index 000000000..d75f8be62 --- /dev/null +++ b/tests/libgit2/commit/amend.c @@ -0,0 +1,122 @@ +#include "clar_libgit2.h" +#include "repository.h" + +/* Fixture setup */ +static git_repository *g_repo; +static git_signature *g_committer; + +void test_commit_amend__initialize(void) +{ + g_repo = cl_git_sandbox_init("testrepo2"); + cl_git_pass(git_signature_new(&g_committer, "libgit2 user", "nobody@noreply.libgit2.org", 987654321, 90)); +} + +void test_commit_amend__cleanup(void) +{ + git_signature_free(g_committer); + cl_git_sandbox_cleanup(); +} + + +void test_commit_amend__from_stage_simple(void) +{ + git_commit_create_options opts = GIT_COMMIT_CREATE_OPTIONS_INIT; + git_index *index; + git_oid commit_id; + git_tree *tree; + + opts.committer = g_committer; + + cl_git_rewritefile("testrepo2/newfile.txt", "This is a new file.\n"); + cl_git_rewritefile("testrepo2/newfile2.txt", "This is a new file.\n"); + cl_git_rewritefile("testrepo2/README", "hello, world.\n"); + cl_git_rewritefile("testrepo2/new.txt", "hi there.\n"); + + cl_git_pass(git_repository_index(&index, g_repo)); + cl_git_pass(git_index_add_bypath(index, "newfile2.txt")); + cl_git_pass(git_index_add_bypath(index, "README")); + cl_git_pass(git_index_write(index)); + + cl_git_pass(git_commit_amend_from_stage(&commit_id, g_repo, NULL, &opts)); + + cl_git_pass(git_repository_head_tree(&tree, g_repo)); + + cl_assert_equal_oidstr("63ec0b083fd14c22a68fd2b1794f26e4b396b6b3", &commit_id); + cl_assert_equal_oidstr("b27210772d0633870b4f486d04ed3eb5ebbef5e7", git_tree_id(tree)); + + git_index_free(index); + git_tree_free(tree); +} + +void test_commit_amend__from_stage_newmessage(void) +{ + git_commit_create_options opts = GIT_COMMIT_CREATE_OPTIONS_INIT; + git_oid commit_id; + git_tree *tree; + + opts.committer = g_committer; + + cl_git_pass(git_commit_amend_from_stage(&commit_id, g_repo, "New message goes here.", &opts)); + + cl_git_pass(git_repository_head_tree(&tree, g_repo)); + + cl_assert_equal_oidstr("8b0e1cacc8380023705192466aaef8a15ddae7b3", &commit_id); + cl_assert_equal_oidstr("c4dc1555e4d4fa0e0c9c3fc46734c7c35b3ce90b", git_tree_id(tree)); + + git_tree_free(tree); +} + +void test_commit_amend__from_stage_nochanges(void) +{ + git_commit_create_options opts = GIT_COMMIT_CREATE_OPTIONS_INIT; + git_oid commit_id; + git_tree *tree; + + opts.committer = g_committer; + + cl_git_pass(git_commit_amend_from_stage(&commit_id, g_repo, NULL, &opts)); + + cl_git_pass(git_repository_head_tree(&tree, g_repo)); + + cl_assert_equal_oidstr("da86907c6d505a92c5683bece08f23d68ac785bd", &commit_id); + cl_assert_equal_oidstr("c4dc1555e4d4fa0e0c9c3fc46734c7c35b3ce90b", git_tree_id(tree)); + + git_tree_free(tree); +} + +void test_commit_amend__from_tree(void) +{ + git_commit_create_options opts = GIT_COMMIT_CREATE_OPTIONS_INIT; + git_index *index; + git_oid commit_id; + git_oid tree_id; + git_tree *tree, *lookedup; + + opts.committer = g_committer; + + cl_git_rewritefile("testrepo2/newfile.txt", "This is a new file.\n"); + cl_git_rewritefile("testrepo2/newfile2.txt", "This is a new file.\n"); + cl_git_rewritefile("testrepo2/README", "hello, world.\n"); + cl_git_rewritefile("testrepo2/new.txt", "hi there.\n"); + + cl_git_pass(git_repository_index(&index, g_repo)); + cl_git_pass(git_index_add_bypath(index, "newfile2.txt")); + cl_git_pass(git_index_add_bypath(index, "README")); + cl_git_pass(git_index_write(index)); + cl_git_pass(git_index_write_tree(&tree_id, index)); + + cl_assert_equal_oidstr("b27210772d0633870b4f486d04ed3eb5ebbef5e7", &tree_id); + + cl_git_pass(git_tree_lookup(&tree, g_repo, &tree_id)); + + cl_git_pass(git_commit_amend_from_tree(&commit_id, g_repo, tree, NULL, &opts)); + + cl_git_pass(git_repository_head_tree(&lookedup, g_repo)); + + cl_assert_equal_oidstr("63ec0b083fd14c22a68fd2b1794f26e4b396b6b3", &commit_id); + cl_assert_equal_oidstr("b27210772d0633870b4f486d04ed3eb5ebbef5e7", git_tree_id(lookedup)); + + git_index_free(index); + git_tree_free(tree); + git_tree_free(lookedup); +}