diff --git a/include/git2/commit.h b/include/git2/commit.h index 67170cb9c..e3e5d7236 100644 --- a/include/git2/commit.h +++ b/include/git2/commit.h @@ -541,6 +541,24 @@ typedef int (*git_commit_create_cb)( const git_commit *parents[], void *payload); +/** An array of commits returned from the library */ +typedef struct git_commitarray { + git_commit **commits; + size_t count; +} git_commitarray; + +/** + * Free the commits contained in a commit array. This method should + * be called on `git_commitarray` objects that were provided by the + * library. Not doing so will result in a memory leak. + * + * This does not free the `git_commitarray` itself, since the library + * will never allocate that object directly itself. + * + * @param array The git_commitarray that contains commits to free + */ +GIT_EXTERN(void) git_commitarray_dispose(git_commitarray *array); + /** @} */ GIT_END_DECL #endif diff --git a/include/git2/repository.h b/include/git2/repository.h index 6ec2ac822..0ff085651 100644 --- a/include/git2/repository.h +++ b/include/git2/repository.h @@ -11,6 +11,7 @@ #include "types.h" #include "oid.h" #include "buffer.h" +#include "commit.h" /** * @file git2/repository.h @@ -978,6 +979,17 @@ GIT_EXTERN(int) git_repository_set_ident(git_repository *repo, const char *name, */ GIT_EXTERN(git_oid_t) git_repository_oid_type(git_repository *repo); +/** + * Gets the parents of the next commit, given the current repository state. + * Generally, this is the HEAD commit, except when performing a merge, in + * which case it is two or more commits. + * + * @param commits a `git_commitarray` that will contain the commit parents + * @param repo the repository + * @return 0 or an error code + */ +GIT_EXTERN(int) git_repository_commit_parents(git_commitarray *commits, git_repository *repo); + /** @} */ GIT_END_DECL #endif diff --git a/src/libgit2/commit.c b/src/libgit2/commit.c index f7be73acf..b2c335e81 100644 --- a/src/libgit2/commit.c +++ b/src/libgit2/commit.c @@ -1097,3 +1097,18 @@ int git_commit_author_with_mailmap( { return git_mailmap_resolve_signature(out, mailmap, commit->author); } + +void git_commitarray_dispose(git_commitarray *array) +{ + size_t i; + + if (array == NULL) + return; + + for (i = 0; i < array->count; i++) + git_commit_free(array->commits[i]); + + git__free(array->commits); + + memset(array, 0, sizeof(*array)); +} diff --git a/src/libgit2/repository.c b/src/libgit2/repository.c index 0b76e0464..c11fef403 100644 --- a/src/libgit2/repository.c +++ b/src/libgit2/repository.c @@ -3874,3 +3874,65 @@ git_oid_t git_repository_oid_type(git_repository *repo) { return repo ? repo->oid_type : 0; } + +struct mergehead_data { + git_repository *repo; + git_vector *parents; +}; + +static int insert_mergehead(const git_oid *oid, void *payload) +{ + git_commit *commit; + struct mergehead_data *data = (struct mergehead_data *)payload; + + if (git_commit_lookup(&commit, data->repo, oid) < 0) + return -1; + + return git_vector_insert(data->parents, commit); +} + +int git_repository_commit_parents(git_commitarray *out, git_repository *repo) +{ + git_commit *first_parent = NULL, *commit; + git_reference *head_ref = NULL; + git_vector parents = GIT_VECTOR_INIT; + struct mergehead_data data; + size_t i; + int error; + + GIT_ASSERT_ARG(out && repo); + + out->count = 0; + out->commits = NULL; + + error = git_revparse_ext((git_object **)&first_parent, &head_ref, repo, "HEAD"); + + if (error != 0) { + if (error == GIT_ENOTFOUND) + error = 0; + + goto done; + } + + if ((error = git_vector_insert(&parents, first_parent)) < 0) + goto done; + + data.repo = repo; + data.parents = &parents; + + error = git_repository_mergehead_foreach(repo, insert_mergehead, &data); + + if (error == GIT_ENOTFOUND) + error = 0; + else if (error != 0) + goto done; + + out->commits = (git_commit **)git_vector_detach(&out->count, NULL, &parents); + +done: + git_vector_foreach(&parents, i, commit) + git__free(commit); + + git_reference_free(head_ref); + return error; +} diff --git a/tests/libgit2/repo/getters.c b/tests/libgit2/repo/getters.c index d401bb832..8e21d35b5 100644 --- a/tests/libgit2/repo/getters.c +++ b/tests/libgit2/repo/getters.c @@ -51,3 +51,63 @@ void test_repo_getters__retrieving_the_odb_honors_the_refcount(void) git_odb_free(odb); } + +void test_repo_getters__commit_parents(void) +{ + git_repository *repo; + git_commitarray parents; + git_oid first_parent; + git_oid merge_parents[4]; + + git_oid__fromstr(&first_parent, "099fabac3a9ea935598528c27f866e34089c2eff", GIT_OID_SHA1); + + /* A commit on a new repository has no parents */ + + cl_git_pass(git_repository_init(&repo, "new_repo", false)); + cl_git_pass(git_repository_commit_parents(&parents, repo)); + + cl_assert_equal_sz(0, parents.count); + cl_assert_equal_p(NULL, parents.commits); + + git_commitarray_dispose(&parents); + git_repository_free(repo); + + /* A standard commit has one parent */ + + repo = cl_git_sandbox_init("testrepo"); + cl_git_pass(git_repository_commit_parents(&parents, repo)); + + cl_assert_equal_sz(1, parents.count); + cl_assert_equal_oid(&first_parent, git_commit_id(parents.commits[0])); + + git_commitarray_dispose(&parents); + + /* A merge commit has multiple parents */ + + cl_git_rewritefile("testrepo/.git/MERGE_HEAD", + "8496071c1b46c854b31185ea97743be6a8774479\n" + "5b5b025afb0b4c913b4c338a42934a3863bf3644\n" + "4a202b346bb0fb0db7eff3cffeb3c70babbd2045\n" + "9fd738e8f7967c078dceed8190330fc8648ee56a\n"); + + cl_git_pass(git_repository_commit_parents(&parents, repo)); + + cl_assert_equal_sz(5, parents.count); + + cl_assert_equal_oid(&first_parent, git_commit_id(parents.commits[0])); + + git_oid__fromstr(&merge_parents[0], "8496071c1b46c854b31185ea97743be6a8774479", GIT_OID_SHA1); + cl_assert_equal_oid(&merge_parents[0], git_commit_id(parents.commits[1])); + git_oid__fromstr(&merge_parents[1], "5b5b025afb0b4c913b4c338a42934a3863bf3644", GIT_OID_SHA1); + cl_assert_equal_oid(&merge_parents[1], git_commit_id(parents.commits[2])); + git_oid__fromstr(&merge_parents[2], "4a202b346bb0fb0db7eff3cffeb3c70babbd2045", GIT_OID_SHA1); + cl_assert_equal_oid(&merge_parents[2], git_commit_id(parents.commits[3])); + git_oid__fromstr(&merge_parents[3], "9fd738e8f7967c078dceed8190330fc8648ee56a", GIT_OID_SHA1); + cl_assert_equal_oid(&merge_parents[3], git_commit_id(parents.commits[4])); + + git_commitarray_dispose(&parents); + + git_repository_free(repo); + + cl_fixture_cleanup("testrepo"); +}