diff --git a/include/git2/commit.h b/include/git2/commit.h index 060e0085b..c8838e2c8 100644 --- a/include/git2/commit.h +++ b/include/git2/commit.h @@ -593,6 +593,25 @@ GIT_EXTERN(int) git_commit_create_v( size_t parent_count, ...); +/** + * A "batteries included" commit creation from a tree using defaults. + * + * By default, empty commits are not allowed. + * + * @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, GIT_EUNCHANGED if there were no changes to commit, or an error code + */ +GIT_EXTERN(int) git_commit_create_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 3516af884..97c8a1bde 100644 --- a/src/libgit2/commit.c +++ b/src/libgit2/commit.c @@ -1192,29 +1192,58 @@ cleanup: return error; } -int git_commit_create_from_stage( +static int check_for_empty_commit( + git_repository *repo, const git_tree *tree, git_index *index) +{ + git_diff *diff = NULL; + git_tree *head_tree = NULL; + int error = -1; + + error = git_repository_head_tree(&head_tree, repo); + + if (error && error != GIT_EUNBORNBRANCH) + goto done; + + /* + * TODO: stop comparison on first delta + * TODO: stop casting away constness + */ + if (tree) + error = git_diff_tree_to_tree(&diff, repo, head_tree, (git_tree *)tree, NULL); + else + error = git_diff_tree_to_index(&diff, repo, head_tree, index, NULL); + + if (error < 0) + goto done; + + if (git_diff_num_deltas(diff) == 0) { + git_error_set(GIT_ERROR_REPOSITORY, + "no changes are staged for commit"); + error = GIT_EUNCHANGED; + goto done; + } + +done: + git_diff_free(diff); + git_tree_free(head_tree); + + return error; +} + +static int create_from_tree( git_oid *out, git_repository *repo, + const git_tree *tree, const char *message, - const git_commit_create_options *given_opts) + const git_commit_create_options *opts) { - git_commit_create_options opts = GIT_COMMIT_CREATE_OPTIONS_INIT; git_signature *default_signature = NULL; const git_signature *author, *committer; - git_index *index = NULL; - git_diff *diff = NULL; - git_oid tree_id; - git_tree *head_tree = NULL, *tree = NULL; git_commitarray parents = { 0 }; int error = -1; - GIT_ASSERT_ARG(out && repo); - - if (given_opts) - memcpy(&opts, given_opts, sizeof(git_commit_create_options)); - - author = opts.author; - committer = opts.committer; + author = opts->author; + committer = opts->committer; if (!author || !committer) { if (git_signature_default(&default_signature, repo) < 0) @@ -1227,48 +1256,80 @@ int git_commit_create_from_stage( committer = default_signature; } - if (git_repository_index(&index, repo) < 0) - goto done; - - if (!opts.allow_empty_commit) { - error = git_repository_head_tree(&head_tree, repo); - - if (error && error != GIT_EUNBORNBRANCH) - goto done; - - error = -1; - - if (git_diff_tree_to_index(&diff, repo, head_tree, index, NULL) < 0) - goto done; - - if (git_diff_num_deltas(diff) == 0) { - git_error_set(GIT_ERROR_REPOSITORY, - "no changes are staged for commit"); - error = GIT_EUNCHANGED; - goto done; - } - } - - if (git_index_write_tree(&tree_id, index) < 0 || - git_tree_lookup(&tree, repo, &tree_id) < 0 || - git_repository_commit_parents(&parents, repo) < 0) + if (git_repository_commit_parents(&parents, repo) < 0) goto done; error = git_commit_create(out, repo, "HEAD", author, committer, - opts.message_encoding, message, + opts->message_encoding, message, tree, parents.count, (const git_commit **)parents.commits); done: git_commitarray_dispose(&parents); git_signature_free(default_signature); + + return error; +} + +int git_commit_create_from_stage( + git_oid *out, + git_repository *repo, + const char *message, + const git_commit_create_options *given_opts) +{ + git_commit_create_options opts = GIT_COMMIT_CREATE_OPTIONS_INIT; + git_index *index = NULL; + git_oid tree_id; + git_tree *tree = NULL; + int error = -1; + + GIT_ASSERT_ARG(out && repo && message); + + if (given_opts) + memcpy(&opts, given_opts, sizeof(git_commit_create_options)); + + if (git_repository_index(&index, repo) < 0) + goto done; + + if (!opts.allow_empty_commit && + (error = check_for_empty_commit(repo, NULL, index)) < 0) + goto done; + + if (git_index_write_tree(&tree_id, index) < 0 || + git_tree_lookup(&tree, repo, &tree_id) < 0) { + error = -1; + goto done; + } + + error = create_from_tree(out, repo, tree, message, &opts); + +done: git_tree_free(tree); - git_tree_free(head_tree); - git_diff_free(diff); git_index_free(index); return error; } +int git_commit_create_from_tree( + git_oid *out, + git_repository *repo, + const git_tree *tree, + const char *message, + const git_commit_create_options *given_opts) +{ + git_commit_create_options opts = GIT_COMMIT_CREATE_OPTIONS_INIT; + + GIT_ASSERT_ARG(out && repo && tree && message); + + if (given_opts) + memcpy(&opts, given_opts, sizeof(git_commit_create_options)); + + if (!opts.allow_empty_commit && + check_for_empty_commit(repo, tree, NULL) < 0) + return -1; + + return create_from_tree(out, repo, tree, message, &opts); +} + int git_commit_committer_with_mailmap( git_signature **out, const git_commit *commit, const git_mailmap *mailmap) { diff --git a/tests/libgit2/commit/create.c b/tests/libgit2/commit/create.c index 9f627dc87..7dadb6964 100644 --- a/tests/libgit2/commit/create.c +++ b/tests/libgit2/commit/create.c @@ -110,3 +110,41 @@ void test_commit_create__from_stage_newrepo(void) git_repository_free(newrepo); cl_fixture_cleanup("newrepo"); } + +void test_commit_create__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.author = g_author; + 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_create_from_tree(&commit_id, g_repo, tree, "This is the message.", &opts)); + + cl_git_pass(git_repository_head_tree(&lookedup, g_repo)); + + cl_assert_equal_oidstr("241b5b04e847bc38dd7b4b9f49f21e55da40f3a6", &commit_id); + cl_assert_equal_oidstr("b27210772d0633870b4f486d04ed3eb5ebbef5e7", git_tree_id(lookedup)); + + git_index_free(index); + git_tree_free(tree); + git_tree_free(lookedup); +}