diff --git a/include/git2/merge.h b/include/git2/merge.h index 38e3408cc..1d30a5a16 100644 --- a/include/git2/merge.h +++ b/include/git2/merge.h @@ -234,6 +234,46 @@ GIT_EXTERN(int) git_merge_tree_init_opts( git_merge_tree_opts* opts, int version); +/** + * The results of `git_merge_status` indicate the state of a merge scenario. + */ +typedef enum { + /** + * A "normal" merge; both HEAD and the given merge input have diverged + * from their common ancestor. The divergent commits must be merged. + */ + GIT_MERGE_STATUS_NORMAL = 0, + + /** + * The repository is already up-to-date and no merge needs to be + * performed. The given merge input already exists as a parent of HEAD. + */ + GIT_MERGE_STATUS_UP_TO_DATE = (1 << 0), + + /** + * The given merge input is a fast-forward from HEAD and no merge + * needs to be performed. Instead, the client can check out the + * given merge input. + */ + GIT_MERGE_STATUS_FASTFORWARD = (1 << 1), +} git_merge_status_t; + +/** + * Determine the status of the merge between the given branch(es) and the + * HEAD of the repository. + * + * @param status_out status enumeration that the result is written into + * @param repo the repository to merge + * @param their_heads the heads to merge into + * @param their_heads_len the number of heads to merge + * @return 0 on success or error code + */ +GIT_EXTERN(int) git_merge_status( + git_merge_status_t *status_out, + git_repository *repo, + const git_merge_head **their_heads, + size_t their_heads_len); + /** * Option flags for `git_merge`. */ diff --git a/src/merge.c b/src/merge.c index 8a33edb13..cdc1921ce 100644 --- a/src/merge.c +++ b/src/merge.c @@ -2497,6 +2497,79 @@ static int merge_state_cleanup(git_repository *repo) return git_repository__cleanup_files(repo, state_files, ARRAY_SIZE(state_files)); } +static int merge_heads( + git_merge_head **ancestor_head_out, + git_merge_head **our_head_out, + git_repository *repo, + const git_merge_head **their_heads, + size_t their_heads_len) +{ + git_merge_head *ancestor_head = NULL, *our_head = NULL; + git_reference *our_ref = NULL; + int error = 0; + + *ancestor_head_out = NULL; + *our_head_out = NULL; + + if ((error = git_repository__ensure_not_bare(repo, "merge")) < 0) + goto done; + + if ((error = git_reference_lookup(&our_ref, repo, GIT_HEAD_FILE)) < 0 || + (error = git_merge_head_from_ref(&our_head, repo, our_ref)) < 0) + goto done; + + if ((error = merge_ancestor_head(&ancestor_head, repo, our_head, their_heads, their_heads_len)) < 0) { + if (error != GIT_ENOTFOUND) + goto done; + + giterr_clear(); + error = 0; + } + + *ancestor_head_out = ancestor_head; + *our_head_out = our_head; + +done: + if (error < 0) { + git_merge_head_free(ancestor_head); + git_merge_head_free(our_head); + } + + git_reference_free(our_ref); + + return error; +} + +int git_merge_status( + git_merge_status_t *out, + git_repository *repo, + const git_merge_head **their_heads, + size_t their_heads_len) +{ + git_merge_head *ancestor_head = NULL, *our_head = NULL; + int error; + + assert(out && repo && their_heads); + + *out = GIT_MERGE_STATUS_NORMAL; + + if ((error = merge_heads(&ancestor_head, &our_head, repo, their_heads, their_heads_len)) < 0) + goto done; + + if (their_heads_len == 1 && ancestor_head != NULL) { + /* We're up-to-date if we're trying to merge our own common ancestor. */ + if (git_oid_equal(&ancestor_head->oid, &their_heads[0]->oid)) + *out = GIT_MERGE_STATUS_UP_TO_DATE; + + /* We're fastforwardable if we're our own common ancestor. */ + else if (git_oid_equal(&ancestor_head->oid, &our_head->oid)) + *out = GIT_MERGE_STATUS_FASTFORWARD; + } + +done: + return error; +} + int git_merge( git_merge_result **out, git_repository *repo, @@ -2530,15 +2603,7 @@ int git_merge( their_trees = git__calloc(their_heads_len, sizeof(git_tree *)); GITERR_CHECK_ALLOC(their_trees); - if ((error = git_repository__ensure_not_bare(repo, "merge")) < 0) - goto on_error; - - if ((error = git_reference_lookup(&our_ref, repo, GIT_HEAD_FILE)) < 0 || - (error = git_merge_head_from_ref(&our_head, repo, our_ref)) < 0) - goto on_error; - - if ((error = merge_ancestor_head(&ancestor_head, repo, our_head, their_heads, their_heads_len)) < 0 && - error != GIT_ENOTFOUND) + if ((error = merge_heads(&ancestor_head, &our_head, repo, their_heads, their_heads_len)) < 0) goto on_error; if ((error = merge_normalize_opts(repo, &opts, given_opts, ancestor_head, our_head, their_heads_len, their_heads)) < 0) diff --git a/tests/merge/workdir/fastforward.c b/tests/merge/workdir/fastforward.c deleted file mode 100644 index d6b31481f..000000000 --- a/tests/merge/workdir/fastforward.c +++ /dev/null @@ -1,148 +0,0 @@ -#include "clar_libgit2.h" -#include "git2/repository.h" -#include "git2/merge.h" -#include "git2/sys/index.h" -#include "merge.h" -#include "../merge_helpers.h" -#include "refs.h" - -static git_repository *repo; -static git_index *repo_index; - -#define TEST_REPO_PATH "merge-resolve" -#define TEST_INDEX_PATH TEST_REPO_PATH "/.git/index" - -#define THEIRS_FASTFORWARD_BRANCH "ff_branch" -#define THEIRS_FASTFORWARD_ID "fd89f8cffb663ac89095a0f9764902e93ceaca6a" - -#define THEIRS_NOFASTFORWARD_BRANCH "branch" -#define THEIRS_NOFASTFORWARD_ID "7cb63eed597130ba4abb87b3e544b85021905520" - - -// Fixture setup and teardown -void test_merge_workdir_fastforward__initialize(void) -{ - repo = cl_git_sandbox_init(TEST_REPO_PATH); - git_repository_index(&repo_index, repo); -} - -void test_merge_workdir_fastforward__cleanup(void) -{ - git_index_free(repo_index); - cl_git_sandbox_cleanup(); -} - -static git_merge_result *merge_fastforward_branch(int flags) -{ - git_reference *their_ref; - git_merge_head *their_heads[1]; - git_merge_result *result; - git_merge_opts opts = GIT_MERGE_OPTS_INIT; - - opts.merge_flags = flags; - - cl_git_pass(git_reference_lookup(&their_ref, repo, GIT_REFS_HEADS_DIR THEIRS_FASTFORWARD_BRANCH)); - cl_git_pass(git_merge_head_from_ref(&their_heads[0], repo, their_ref)); - - cl_git_pass(git_merge(&result, repo, (const git_merge_head **)their_heads, 1, &opts)); - - git_merge_head_free(their_heads[0]); - git_reference_free(their_ref); - - return result; -} - -void test_merge_workdir_fastforward__fastforward(void) -{ - git_merge_result *result; - git_oid expected, ff_oid; - - cl_git_pass(git_oid_fromstr(&expected, THEIRS_FASTFORWARD_ID)); - - cl_assert(result = merge_fastforward_branch(0)); - cl_assert(git_merge_result_is_fastforward(result)); - cl_git_pass(git_merge_result_fastforward_id(&ff_oid, result)); - cl_assert(git_oid_cmp(&ff_oid, &expected) == 0); - - git_merge_result_free(result); -} - -void test_merge_workdir_fastforward__fastforward_only(void) -{ - git_merge_result *result; - git_merge_opts opts = GIT_MERGE_OPTS_INIT; - git_reference *their_ref; - git_merge_head *their_head; - int error; - - opts.merge_flags = GIT_MERGE_FASTFORWARD_ONLY; - - cl_git_pass(git_reference_lookup(&their_ref, repo, GIT_REFS_HEADS_DIR THEIRS_NOFASTFORWARD_BRANCH)); - cl_git_pass(git_merge_head_from_ref(&their_head, repo, their_ref)); - - cl_git_fail((error = git_merge(&result, repo, (const git_merge_head **)&their_head, 1, &opts))); - cl_assert(error == GIT_ENONFASTFORWARD); - - git_merge_head_free(their_head); - git_reference_free(their_ref); -} - -void test_merge_workdir_fastforward__no_fastforward(void) -{ - git_merge_result *result; - - struct merge_index_entry merge_index_entries[] = { - { 0100644, "233c0919c998ed110a4b6ff36f353aec8b713487", 0, "added-in-master.txt" }, - { 0100644, "ee3fa1b8c00aff7fe02065fdb50864bb0d932ccf", 0, "automergeable.txt" }, - { 0100644, "ab6c44a2e84492ad4b41bb6bac87353e9d02ac8b", 0, "changed-in-branch.txt" }, - { 0100644, "bd9cb4cd0a770cb9adcb5fce212142ef40ea1c35", 0, "changed-in-master.txt" }, - { 0100644, "4e886e602529caa9ab11d71f86634bd1b6e0de10", 0, "conflicting.txt" }, - { 0100644, "364bbe4ce80c7bd31e6307dce77d46e3e1759fb3", 0, "new-in-ff.txt" }, - { 0100644, "dfe3f22baa1f6fce5447901c3086bae368de6bdd", 0, "removed-in-branch.txt" }, - { 0100644, "c8f06f2e3bb2964174677e91f0abead0e43c9e5d", 0, "unchanged.txt" }, - }; - - cl_assert(result = merge_fastforward_branch(GIT_MERGE_NO_FASTFORWARD)); - cl_assert(!git_merge_result_is_fastforward(result)); - - cl_assert(merge_test_index(repo_index, merge_index_entries, 8)); - cl_assert(git_index_reuc_entrycount(repo_index) == 0); - - git_merge_result_free(result); -} - -void test_merge_workdir_fastforward__uptodate(void) -{ - git_reference *their_ref; - git_merge_head *their_heads[1]; - git_merge_result *result; - - cl_git_pass(git_reference_lookup(&their_ref, repo, GIT_HEAD_FILE)); - cl_git_pass(git_merge_head_from_ref(&their_heads[0], repo, their_ref)); - - cl_git_pass(git_merge(&result, repo, (const git_merge_head **)their_heads, 1, NULL)); - - cl_assert(git_merge_result_is_uptodate(result)); - - git_merge_head_free(their_heads[0]); - git_reference_free(their_ref); - git_merge_result_free(result); -} - -void test_merge_workdir_fastforward__uptodate_merging_prev_commit(void) -{ - git_oid their_oid; - git_merge_head *their_heads[1]; - git_merge_result *result; - - cl_git_pass(git_oid_fromstr(&their_oid, "c607fc30883e335def28cd686b51f6cfa02b06ec")); - cl_git_pass(git_merge_head_from_id(&their_heads[0], repo, &their_oid)); - - cl_git_pass(git_merge(&result, repo, (const git_merge_head **)their_heads, 1, NULL)); - - cl_assert(git_merge_result_is_uptodate(result)); - - git_merge_head_free(their_heads[0]); - git_merge_result_free(result); -} - diff --git a/tests/merge/workdir/status.c b/tests/merge/workdir/status.c new file mode 100644 index 000000000..589299eff --- /dev/null +++ b/tests/merge/workdir/status.c @@ -0,0 +1,89 @@ +#include "clar_libgit2.h" +#include "git2/repository.h" +#include "git2/merge.h" +#include "git2/sys/index.h" +#include "merge.h" +#include "../merge_helpers.h" +#include "refs.h" + +static git_repository *repo; +static git_index *repo_index; + +#define TEST_REPO_PATH "merge-resolve" +#define TEST_INDEX_PATH TEST_REPO_PATH "/.git/index" + +#define UPTODATE_BRANCH "master" +#define PREVIOUS_BRANCH "previous" + +#define FASTFORWARD_BRANCH "ff_branch" +#define FASTFORWARD_ID "fd89f8cffb663ac89095a0f9764902e93ceaca6a" + +#define NOFASTFORWARD_BRANCH "branch" +#define NOFASTFORWARD_ID "7cb63eed597130ba4abb87b3e544b85021905520" + + +// Fixture setup and teardown +void test_merge_workdir_status__initialize(void) +{ + repo = cl_git_sandbox_init(TEST_REPO_PATH); + git_repository_index(&repo_index, repo); +} + +void test_merge_workdir_status__cleanup(void) +{ + git_index_free(repo_index); + cl_git_sandbox_cleanup(); +} + +static git_status_t status_from_branch(const char *branchname) +{ + git_buf refname = GIT_BUF_INIT; + git_reference *their_ref; + git_merge_head *their_heads[1]; + git_status_t status; + + git_buf_printf(&refname, "%s%s", GIT_REFS_HEADS_DIR, branchname); + + cl_git_pass(git_reference_lookup(&their_ref, repo, git_buf_cstr(&refname))); + cl_git_pass(git_merge_head_from_ref(&their_heads[0], repo, their_ref)); + + cl_git_pass(git_merge_status(&status, repo, their_heads, 1)); + + git_buf_free(&refname); + git_merge_head_free(their_heads[0]); + git_reference_free(their_ref); + + return status; +} + +void test_merge_workdir_status__fastforward(void) +{ + git_merge_status_t status; + + status = status_from_branch(FASTFORWARD_BRANCH); + cl_assert_equal_i(GIT_MERGE_STATUS_FASTFORWARD, status); +} + +void test_merge_workdir_status__no_fastforward(void) +{ + git_merge_status_t status; + + status = status_from_branch(NOFASTFORWARD_BRANCH); + cl_assert_equal_i(GIT_MERGE_STATUS_NORMAL, status); +} + +void test_merge_workdir_status__uptodate(void) +{ + git_merge_status_t status; + + status = status_from_branch(UPTODATE_BRANCH); + cl_assert_equal_i(GIT_MERGE_STATUS_UP_TO_DATE, status); +} + +void test_merge_workdir_status__uptodate_merging_prev_commit(void) +{ + git_merge_status_t status; + + status = status_from_branch(PREVIOUS_BRANCH); + cl_assert_equal_i(GIT_MERGE_STATUS_UP_TO_DATE, status); +} diff --git a/tests/resources/merge-resolve/.gitted/refs/heads/previous b/tests/resources/merge-resolve/.gitted/refs/heads/previous new file mode 100644 index 000000000..7bc1a8d15 Binary files /dev/null and b/tests/resources/merge-resolve/.gitted/refs/heads/previous differ