diff --git a/include/git2/merge.h b/include/git2/merge.h index 62fd7d723..3354fbeab 100644 --- a/include/git2/merge.h +++ b/include/git2/merge.h @@ -65,6 +65,29 @@ typedef struct { #define GIT_MERGE_TREE_OPTS_INIT {GIT_MERGE_TREE_OPTS_VERSION} +/** + * Option flags for `git_merge`. + * + * GIT_MERGE_NO_FASTFORWARD - Do not fast-forward. + */ +typedef enum { + GIT_MERGE_NO_FASTFORWARD = 1, + GIT_MERGE_FASTFORWARD_ONLY = 2, +} git_merge_flags_t; + +typedef struct { + unsigned int version; + + git_merge_flags_t merge_flags; + git_merge_tree_opts merge_tree_opts; + + git_checkout_opts checkout_opts; +} git_merge_opts; + +#define GIT_MERGE_OPTS_VERSION 1 +#define GIT_MERGE_OPTS_INIT {GIT_MERGE_OPTS_VERSION, 0, GIT_MERGE_TREE_OPTS_INIT, GIT_CHECKOUT_OPTS_INIT} + + /** * Find a merge base between two commits * @@ -168,6 +191,43 @@ GIT_EXTERN(int) git_merge_trees( const git_tree *their_tree, const git_merge_tree_opts *opts); +/** + * Merges the given commits into HEAD, producing a new commit. + * + * @param out the results of the merge + * @param repo the repository to merge + * @param merge_heads the heads to merge into + * @param merge_heads_len the number of heads to merge + * @param flags merge flags + */ +GIT_EXTERN(int) git_merge( + git_merge_result **out, + git_repository *repo, + const git_merge_head **their_heads, + size_t their_heads_len, + const git_merge_opts *opts); + +/** + * Returns true if a merge is up-to-date (we were asked to merge the target + * into itself.) + */ +GIT_EXTERN(int) git_merge_result_is_uptodate(git_merge_result *merge_result); + +/** + * Returns true if a merge is eligible for fastforward + */ +GIT_EXTERN(int) git_merge_result_is_fastforward(git_merge_result *merge_result); + +/** + * Gets the fast-forward OID if the merge was a fastforward. + * + * @param out the OID of the fast-forward + * @param merge_result the results of the merge + */ +GIT_EXTERN(int) git_merge_result_fastforward_oid(git_oid *out, git_merge_result *merge_result); + +GIT_EXTERN(void) git_merge_result_free(git_merge_result *merge_result); + /** @} */ GIT_END_DECL #endif diff --git a/include/git2/types.h b/include/git2/types.h index 71d5374d3..55505b110 100644 --- a/include/git2/types.h +++ b/include/git2/types.h @@ -174,6 +174,9 @@ typedef struct git_reference_iterator git_reference_iterator; /** Merge heads, the input to merge */ typedef struct git_merge_head git_merge_head; +/** Merge result */ +typedef struct git_merge_result git_merge_result; + /** Representation of a status collection */ typedef struct git_status_list git_status_list; diff --git a/src/merge.c b/src/merge.c index ea2224d86..973d4445c 100644 --- a/src/merge.c +++ b/src/merge.c @@ -1615,16 +1615,13 @@ static int write_orig_head( { git_filebuf file = GIT_FILEBUF_INIT; git_buf file_path = GIT_BUF_INIT; - char orig_oid_str[GIT_OID_HEXSZ + 1]; int error = 0; assert(repo && our_head); - git_oid_tostr(orig_oid_str, GIT_OID_HEXSZ+1, &our_head->oid); - if ((error = git_buf_joinpath(&file_path, repo->path_repository, GIT_ORIG_HEAD_FILE)) == 0 && (error = git_filebuf_open(&file, file_path.ptr, GIT_FILEBUF_FORCE, GIT_MERGE_FILE_MODE)) == 0 && - (error = git_filebuf_printf(&file, "%s\n", orig_oid_str)) == 0) + (error = git_filebuf_printf(&file, "%s\n", our_head->oid_str)) == 0) error = git_filebuf_commit(&file); if (error < 0) @@ -1642,7 +1639,6 @@ static int write_merge_head( { git_filebuf file = GIT_FILEBUF_INIT; git_buf file_path = GIT_BUF_INIT; - char merge_oid_str[GIT_OID_HEXSZ + 1]; size_t i; int error = 0; @@ -1653,9 +1649,7 @@ static int write_merge_head( goto cleanup; for (i = 0; i < heads_len; i++) { - git_oid_tostr(merge_oid_str, GIT_OID_HEXSZ+1, &heads[i]->oid); - - if ((error = git_filebuf_printf(&file, "%s\n", merge_oid_str)) < 0) + if ((error = git_filebuf_printf(&file, "%s\n", &heads[i]->oid_str)) < 0) goto cleanup; } @@ -1685,6 +1679,17 @@ static int write_merge_mode(git_repository *repo, unsigned int flags) (error = git_filebuf_open(&file, file_path.ptr, GIT_FILEBUF_FORCE, GIT_MERGE_FILE_MODE)) < 0) goto cleanup; + /* + * no-ff is the only thing allowed here at present. One would + * presume they would be space-delimited when there are more, but + * this needs to be revisited. + */ + + if (flags & GIT_MERGE_NO_FASTFORWARD) { + if ((error = git_filebuf_write(&file, "no-ff", 5)) < 0) + goto cleanup; + } + error = git_filebuf_commit(&file); cleanup: @@ -1890,7 +1895,6 @@ static int write_merge_msg( { git_filebuf file = GIT_FILEBUF_INIT; git_buf file_path = GIT_BUF_INIT; - char oid_str[GIT_OID_HEXSZ + 1]; struct merge_msg_entry *entries; git_vector matching = GIT_VECTOR_INIT; size_t i; @@ -1930,10 +1934,9 @@ static int write_merge_msg( if (!msg_entry_is_oid(&entries[i])) break; - git_oid_fmt(oid_str, &entries[i].merge_head->oid); - oid_str[GIT_OID_HEXSZ] = '\0'; - - if ((error = git_filebuf_printf(&file, "%scommit '%s'", (i > 0) ? "; " : "", oid_str)) < 0) + if ((error = git_filebuf_printf(&file, + "%scommit '%s'", (i > 0) ? "; " : "", + entries[i].merge_head->oid_str)) < 0) goto cleanup; entries[i].written = 1; @@ -1980,10 +1983,8 @@ static int write_merge_msg( if (merge_msg_entry_written(&entries[i])) continue; - git_oid_fmt(oid_str, &entries[i].merge_head->oid); - oid_str[GIT_OID_HEXSZ] = '\0'; - - if ((error = git_filebuf_printf(&file, "; commit '%s'", oid_str)) < 0) + if ((error = git_filebuf_printf(&file, "; commit '%s'", + entries[i].merge_head->oid_str)) < 0) goto cleanup; } @@ -2003,6 +2004,474 @@ cleanup: return error; } +/* Merge branches */ + +static int merge_ancestor_head( + git_merge_head **ancestor_head, + git_repository *repo, + const git_merge_head *our_head, + const git_merge_head **their_heads, + size_t their_heads_len) +{ + git_oid *oids, ancestor_oid; + size_t i; + int error = 0; + + assert(repo && our_head && their_heads); + + oids = git__calloc(their_heads_len + 1, sizeof(git_oid)); + GITERR_CHECK_ALLOC(oids); + + git_oid_cpy(&oids[0], git_commit_id(our_head->commit)); + + for (i = 0; i < their_heads_len; i++) + git_oid_cpy(&oids[i + 1], &their_heads[i]->oid); + + if ((error = git_merge_base_many(&ancestor_oid, repo, their_heads_len + 1, oids)) < 0) + goto on_error; + + error = git_merge_head_from_oid(ancestor_head, repo, &ancestor_oid); + +on_error: + git__free(oids); + return error; +} + +GIT_INLINE(bool) merge_check_uptodate( + git_merge_result *result, + const git_merge_head *ancestor_head, + const git_merge_head *their_head) +{ + if (git_oid_cmp(&ancestor_head->oid, &their_head->oid) == 0) { + result->is_uptodate = 1; + return true; + } + + return false; +} + +GIT_INLINE(bool) merge_check_fastforward( + git_merge_result *result, + const git_merge_head *ancestor_head, + const git_merge_head *our_head, + const git_merge_head *their_head, + unsigned int flags) +{ + if ((flags & GIT_MERGE_NO_FASTFORWARD) == 0 && + git_oid_cmp(&ancestor_head->oid, &our_head->oid) == 0) { + result->is_fastforward = 1; + git_oid_cpy(&result->fastforward_oid, &their_head->oid); + + return true; + } + + return false; +} + +const char *merge_their_label(const char *branchname) +{ + const char *slash; + + if ((slash = strrchr(branchname, '/')) == NULL) + return branchname; + + if (*(slash+1) == '\0') + return "theirs"; + + return slash+1; +} + +static int merge_normalize_opts( + git_repository *repo, + git_merge_opts *opts, + const git_merge_opts *given, + size_t their_heads_len, + const git_merge_head **their_heads) +{ + int error = 0; + unsigned int default_checkout_strategy = GIT_CHECKOUT_SAFE_CREATE | + GIT_CHECKOUT_ALLOW_CONFLICTS; + + GIT_UNUSED(repo); + + if (given != NULL) + memcpy(opts, given, sizeof(git_merge_opts)); + else { + git_merge_opts default_opts = GIT_MERGE_OPTS_INIT; + memcpy(opts, &default_opts, sizeof(git_merge_opts)); + } + + if (!opts->checkout_opts.checkout_strategy) + opts->checkout_opts.checkout_strategy = default_checkout_strategy; + + if (!opts->checkout_opts.our_label) + opts->checkout_opts.our_label = "HEAD"; + + if (!opts->checkout_opts.their_label) { + if (their_heads_len == 1 && their_heads[0]->ref_name) + opts->checkout_opts.their_label = merge_their_label(their_heads[0]->ref_name); + else if (their_heads_len == 1) + opts->checkout_opts.their_label = their_heads[0]->oid_str; + else + opts->checkout_opts.their_label = "theirs"; + } + + return error; +} + +static int merge_affected_paths(git_vector *paths, git_repository *repo, git_index *index_new) +{ + git_tree *head_tree = NULL; + git_iterator *iter_head = NULL, *iter_new = NULL; + git_diff *merged_list = NULL; + git_diff_options opts = GIT_DIFF_OPTIONS_INIT; + git_diff_delta *delta; + size_t i; + const git_index_entry *e; + char *path; + int error = 0; + + if ((error = git_repository_head_tree(&head_tree, repo)) < 0 || + (error = git_iterator_for_tree(&iter_head, head_tree, GIT_ITERATOR_DONT_IGNORE_CASE, NULL, NULL)) < 0 || + (error = git_iterator_for_index(&iter_new, index_new, GIT_ITERATOR_DONT_IGNORE_CASE, NULL, NULL)) < 0 || + (error = git_diff__from_iterators(&merged_list, repo, iter_head, iter_new, &opts)) < 0) + goto done; + + git_vector_foreach(&merged_list->deltas, i, delta) { + path = git__strdup(delta->new_file.path); + GITERR_CHECK_ALLOC(path); + + if ((error = git_vector_insert(paths, path)) < 0) + goto on_error; + } + + for (i = 0; i < git_index_entrycount(index_new); i++) { + e = git_index_get_byindex(index_new, i); + + if (git_index_entry_stage(e) != 0 && + (git_vector_last(paths) == NULL || + strcmp(git_vector_last(paths), e->path) != 0)) { + + path = git__strdup(e->path); + GITERR_CHECK_ALLOC(path); + + if ((error = git_vector_insert(paths, path)) < 0) + goto on_error; + } + } + + goto done; + +on_error: + git_vector_foreach(paths, i, path) + git__free(path); + + git_vector_clear(paths); + +done: + git_tree_free(head_tree); + git_iterator_free(iter_head); + git_iterator_free(iter_new); + git_diff_free(merged_list); + + return error; +} + +static int merge_check_index(size_t *conflicts, git_repository *repo, git_index *index_new, git_vector *merged_paths) +{ + git_tree *head_tree = NULL; + git_index *index_repo = NULL; + git_iterator *iter_repo = NULL, *iter_new = NULL; + git_diff *staged_diff_list = NULL, *index_diff_list = NULL; + git_diff_delta *delta; + git_diff_options opts = GIT_DIFF_OPTIONS_INIT; + git_vector staged_paths = GIT_VECTOR_INIT; + size_t i; + int error = 0; + + GIT_UNUSED(merged_paths); + + *conflicts = 0; + + /* No staged changes may exist unless the change staged is identical to + * the result of the merge. This allows one to apply to merge manually, + * then run merge. Any other staged change would be overwritten by + * a reset merge. + */ + if ((error = git_repository_head_tree(&head_tree, repo)) < 0 || + (error = git_repository_index(&index_repo, repo)) < 0 || + (error = git_diff_tree_to_index(&staged_diff_list, repo, head_tree, index_repo, &opts)) < 0) + goto done; + + if (staged_diff_list->deltas.length == 0) + goto done; + + git_vector_foreach(&staged_diff_list->deltas, i, delta) { + if ((error = git_vector_insert(&staged_paths, (char *)delta->new_file.path)) < 0) + goto done; + } + + opts.pathspec.count = staged_paths.length; + opts.pathspec.strings = (char **)staged_paths.contents; + + if ((error = git_iterator_for_index(&iter_repo, index_repo, GIT_ITERATOR_DONT_IGNORE_CASE, NULL, NULL)) < 0 || + (error = git_iterator_for_index(&iter_new, index_new, GIT_ITERATOR_DONT_IGNORE_CASE, NULL, NULL)) < 0 || + (error = git_diff__from_iterators(&index_diff_list, repo, iter_repo, iter_new, &opts)) < 0) + goto done; + + *conflicts = index_diff_list->deltas.length; + +done: + git_tree_free(head_tree); + git_index_free(index_repo); + git_iterator_free(iter_repo); + git_iterator_free(iter_new); + git_diff_free(staged_diff_list); + git_diff_free(index_diff_list); + git_vector_free(&staged_paths); + + return error; +} + +static int merge_check_workdir(size_t *conflicts, git_repository *repo, git_index *index_new, git_vector *merged_paths) +{ + git_tree *head_tree = NULL; + git_diff *wd_diff_list = NULL; + git_diff_options opts = GIT_DIFF_OPTIONS_INIT; + int error = 0; + + *conflicts = 0; + + opts.flags |= GIT_DIFF_INCLUDE_UNTRACKED; + + if ((error = git_repository_head_tree(&head_tree, repo)) < 0) + goto done; + + /* Workdir changes may exist iff they do not conflict with changes that + * will be applied by the merge (including conflicts). Ensure that there + * are no changes in the workdir to these paths. + */ + opts.pathspec.count = merged_paths->length; + opts.pathspec.strings = (char **)merged_paths->contents; + + if ((error = git_diff_tree_to_workdir(&wd_diff_list, repo, head_tree, &opts)) < 0) + goto done; + + *conflicts = wd_diff_list->deltas.length; + +done: + git_tree_free(head_tree); + git_diff_free(wd_diff_list); + + return error; +} + +static int merge_indexes(git_repository *repo, git_index *index_new) +{ + git_index *index_repo; + unsigned int index_repo_caps; + git_vector paths = GIT_VECTOR_INIT; + size_t index_conflicts = 0, wd_conflicts = 0, conflicts, i; + char *path; + const git_index_entry *e; + const git_index_name_entry *name; + const git_index_reuc_entry *reuc; + int error = 0; + + if ((error = git_repository_index(&index_repo, repo)) < 0) + goto done; + + /* Set the index to case sensitive to handle the merge */ + index_repo_caps = git_index_caps(index_repo); + + if ((error = git_index_set_caps(index_repo, (index_repo_caps & ~GIT_INDEXCAP_IGNORE_CASE))) < 0) + goto done; + + /* Make sure the index and workdir state do not prevent merging */ + if ((error = merge_affected_paths(&paths, repo, index_new)) < 0 || + (error = merge_check_index(&index_conflicts, repo, index_new, &paths)) < 0 || + (error = merge_check_workdir(&wd_conflicts, repo, index_new, &paths)) < 0) + goto done; + + if ((conflicts = index_conflicts + wd_conflicts) > 0) { + giterr_set(GITERR_MERGE, "%d uncommitted change%s would be overwritten by merge", + conflicts, (conflicts != 1) ? "s" : ""); + error = GIT_EMERGECONFLICT; + + goto done; + } + + /* Update the new index */ + git_vector_foreach(&paths, i, path) { + if ((e = git_index_get_bypath(index_new, path, 0)) != NULL) + error = git_index_add(index_repo, e); + else + error = git_index_remove(index_repo, path, 0); + } + + /* Add conflicts */ + git_index_conflict_cleanup(index_repo); + + for (i = 0; i < git_index_entrycount(index_new); i++) { + e = git_index_get_byindex(index_new, i); + + if (git_index_entry_stage(e) != 0 && + (error = git_index_add(index_repo, e)) < 0) + goto done; + } + + /* Add name entries */ + git_index_name_clear(index_repo); + + for (i = 0; i < git_index_name_entrycount(index_new); i++) { + name = git_index_name_get_byindex(index_new, i); + + if ((error = git_index_name_add(index_repo, + name->ancestor, name->ours, name->theirs)) < 0) + goto done; + } + + /* Add the reuc */ + git_index_reuc_clear(index_repo); + + for (i = 0; i < git_index_reuc_entrycount(index_new); i++) { + reuc = (git_index_reuc_entry *)git_index_reuc_get_byindex(index_new, i); + + if ((error = git_index_reuc_add(index_repo, reuc->path, + reuc->mode[0], &reuc->oid[0], + reuc->mode[1], &reuc->oid[1], + reuc->mode[2], &reuc->oid[2])) < 0) + goto done; + } + +done: + if (index_repo != NULL) + git_index_set_caps(index_repo, index_repo_caps); + + git_index_free(index_repo); + + git_vector_foreach(&paths, i, path) + git__free(path); + + git_vector_free(&paths); + + return error; +} + +int git_merge( + git_merge_result **out, + git_repository *repo, + const git_merge_head **their_heads, + size_t their_heads_len, + const git_merge_opts *given_opts) +{ + git_merge_result *result; + git_merge_opts opts; + git_reference *our_ref = NULL; + git_merge_head *ancestor_head = NULL, *our_head = NULL; + git_tree *ancestor_tree = NULL, *our_tree = NULL, **their_trees = NULL; + git_index *index_new = NULL, *index_repo = NULL; + size_t i; + int error = 0; + + assert(out && repo && their_heads); + + *out = NULL; + + if (their_heads_len != 1) { + giterr_set(GITERR_MERGE, "Can only merge a single branch"); + return -1; + } + + result = git__calloc(1, sizeof(git_merge_result)); + GITERR_CHECK_ALLOC(result); + + their_trees = git__calloc(their_heads_len, sizeof(git_tree *)); + GITERR_CHECK_ALLOC(their_trees); + + if ((error = merge_normalize_opts(repo, &opts, given_opts, their_heads_len, their_heads)) < 0) + goto on_error; + + 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) + goto on_error; + + if (their_heads_len == 1 && + ancestor_head != NULL && + (merge_check_uptodate(result, ancestor_head, their_heads[0]) || + merge_check_fastforward(result, ancestor_head, our_head, their_heads[0], opts.merge_flags))) { + *out = result; + goto done; + } + + /* If FASTFORWARD_ONLY is specified, fail. */ + if ((opts.merge_flags & GIT_MERGE_FASTFORWARD_ONLY) == + GIT_MERGE_FASTFORWARD_ONLY) { + giterr_set(GITERR_MERGE, "Not a fast-forward."); + error = GIT_ENONFASTFORWARD; + goto on_error; + } + + /* Write the merge files to the repository. */ + if ((error = git_merge__setup(repo, our_head, their_heads, their_heads_len, opts.merge_flags)) < 0) + goto on_error; + + if (ancestor_head != NULL && + (error = git_commit_tree(&ancestor_tree, ancestor_head->commit)) < 0) + goto on_error; + + if ((error = git_commit_tree(&our_tree, our_head->commit)) < 0) + goto on_error; + + for (i = 0; i < their_heads_len; i++) { + if ((error = git_commit_tree(&their_trees[i], their_heads[i]->commit)) < 0) + goto on_error; + } + + /* TODO: recursive, octopus, etc... */ + + if ((error = git_merge_trees(&index_new, repo, ancestor_tree, our_tree, their_trees[0], &opts.merge_tree_opts)) < 0 || + (error = merge_indexes(repo, index_new)) < 0 || + (error = git_repository_index(&index_repo, repo)) < 0 || + (error = git_checkout_index(repo, index_repo, &opts.checkout_opts)) < 0) + goto on_error; + + result->index = index_new; + + *out = result; + goto done; + +on_error: + git_repository_merge_cleanup(repo); + + git__free(result); + +done: + git_index_free(index_repo); + + git_tree_free(ancestor_tree); + git_tree_free(our_tree); + + for (i = 0; i < their_heads_len; i++) + git_tree_free(their_trees[i]); + + git__free(their_trees); + + git_merge_head_free(our_head); + git_merge_head_free(ancestor_head); + + git_reference_free(our_ref); + + return error; +} + int git_merge__setup( git_repository *repo, const git_merge_head *our_head, @@ -2013,7 +2482,7 @@ int git_merge__setup( int error = 0; assert (repo && our_head && heads); - + if ((error = write_orig_head(repo, our_head)) == 0 && (error = write_merge_head(repo, heads, heads_len)) == 0 && (error = write_merge_mode(repo, flags)) == 0) { @@ -2056,6 +2525,41 @@ cleanup: return error; } +/* Merge result data */ + +int git_merge_result_is_uptodate(git_merge_result *merge_result) +{ + assert(merge_result); + + return merge_result->is_uptodate; +} + +int git_merge_result_is_fastforward(git_merge_result *merge_result) +{ + assert(merge_result); + + return merge_result->is_fastforward; +} + +int git_merge_result_fastforward_oid(git_oid *out, git_merge_result *merge_result) +{ + assert(out && merge_result); + + git_oid_cpy(out, &merge_result->fastforward_oid); + return 0; +} + +void git_merge_result_free(git_merge_result *merge_result) +{ + if (merge_result == NULL) + return; + + git_index_free(merge_result->index); + merge_result->index = NULL; + + git__free(merge_result); +} + /* Merge heads are the input to merge */ static int merge_head_init( @@ -2087,6 +2591,9 @@ static int merge_head_init( git_oid_cpy(&head->oid, oid); + git_oid_fmt(head->oid_str, oid); + head->oid_str[GIT_OID_HEXSZ] = '\0'; + if ((error = git_commit_lookup(&head->commit, repo, &head->oid)) < 0) { git_merge_head_free(head); return error; diff --git a/src/merge.h b/src/merge.h index 0ecad6e3e..d7d1c67b7 100644 --- a/src/merge.h +++ b/src/merge.h @@ -114,9 +114,20 @@ struct git_merge_head { char *remote_url; git_oid oid; + char oid_str[GIT_OID_HEXSZ+1]; git_commit *commit; }; +/** Internal structure for merge results */ +struct git_merge_result { + bool is_uptodate; + + bool is_fastforward; + git_oid fastforward_oid; + + git_index *index; +}; + int git_merge__bases_many( git_commit_list **out, git_revwalk *walk, diff --git a/tests-clar/merge/merge_helpers.c b/tests-clar/merge/merge_helpers.c index ddcb93ff6..43619be0d 100644 --- a/tests-clar/merge/merge_helpers.c +++ b/tests-clar/merge/merge_helpers.c @@ -52,6 +52,29 @@ int merge_trees_from_branches( return 0; } +int merge_branches(git_merge_result **result, git_repository *repo, const char *ours_branch, const char *theirs_branch, git_merge_opts *opts) +{ + git_reference *head_ref, *theirs_ref; + git_merge_head *theirs_head; + git_checkout_opts head_checkout_opts = GIT_CHECKOUT_OPTS_INIT; + + head_checkout_opts.checkout_strategy = GIT_CHECKOUT_FORCE; + + cl_git_pass(git_reference_symbolic_create(&head_ref, repo, "HEAD", ours_branch, 1)); + cl_git_pass(git_checkout_head(repo, &head_checkout_opts)); + + cl_git_pass(git_reference_lookup(&theirs_ref, repo, theirs_branch)); + cl_git_pass(git_merge_head_from_ref(&theirs_head, repo, theirs_ref)); + + cl_git_pass(git_merge(result, repo, (const git_merge_head **)&theirs_head, 1, opts)); + + git_reference_free(head_ref); + git_reference_free(theirs_ref); + git_merge_head_free(theirs_head); + + return 0; +} + void merge__dump_index_entries(git_vector *index_entries) { size_t i; diff --git a/tests-clar/merge/merge_helpers.h b/tests-clar/merge/merge_helpers.h index cb718e01a..ae3274437 100644 --- a/tests-clar/merge/merge_helpers.h +++ b/tests-clar/merge/merge_helpers.h @@ -44,6 +44,9 @@ int merge_trees_from_branches( const char *ours_name, const char *theirs_name, git_merge_tree_opts *opts); +int merge_branches(git_merge_result **result, git_repository *repo, + const char *ours_branch, const char *theirs_branch, git_merge_opts *opts); + int merge_test_diff_list(git_merge_diff_list *diff_list, const struct merge_index_entry expected[], size_t expected_len); int merge_test_merge_conflicts(git_vector *conflicts, const struct merge_index_conflict_data expected[], size_t expected_len); diff --git a/tests-clar/merge/workdir/fastforward.c b/tests-clar/merge/workdir/fastforward.c new file mode 100644 index 000000000..861f38354 --- /dev/null +++ b/tests-clar/merge/workdir/fastforward.c @@ -0,0 +1,148 @@ +#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_OID "fd89f8cffb663ac89095a0f9764902e93ceaca6a" + +#define THEIRS_NOFASTFORWARD_BRANCH "branch" +#define THEIRS_NOFASTFORWARD_OID "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_OID)); + + cl_assert(result = merge_fastforward_branch(0)); + cl_assert(git_merge_result_is_fastforward(result)); + cl_git_pass(git_merge_result_fastforward_oid(&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_oid(&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-clar/merge/workdir/renames.c b/tests-clar/merge/workdir/renames.c new file mode 100644 index 000000000..d38397983 --- /dev/null +++ b/tests-clar/merge/workdir/renames.c @@ -0,0 +1,156 @@ +#include "clar_libgit2.h" +#include "git2/repository.h" +#include "git2/merge.h" +#include "buffer.h" +#include "merge.h" +#include "../merge_helpers.h" +#include "fileops.h" +#include "refs.h" + +static git_repository *repo; + +#define TEST_REPO_PATH "merge-resolve" + +#define BRANCH_RENAME_OURS "rename_conflict_ours" +#define BRANCH_RENAME_THEIRS "rename_conflict_theirs" + +// Fixture setup and teardown +void test_merge_workdir_renames__initialize(void) +{ + repo = cl_git_sandbox_init(TEST_REPO_PATH); +} + +void test_merge_workdir_renames__cleanup(void) +{ + cl_git_sandbox_cleanup(); +} + +void test_merge_workdir_renames__renames(void) +{ + git_merge_result *result; + git_merge_opts opts = GIT_MERGE_OPTS_INIT; + + struct merge_index_entry merge_index_entries[] = { + { 0100644, "68c6c84b091926c7d90aa6a79b2bc3bb6adccd8e", 0, "0a-no-change.txt" }, + { 0100644, "f0ce2b8e4986084d9b308fb72709e414c23eb5e6", 0, "0b-duplicated-in-ours.txt" }, + { 0100644, "8aac75de2a34b4d340bf62a6e58197269cb55797", 0, "0b-rewritten-in-ours.txt" }, + { 0100644, "2f56120107d680129a5d9791b521cb1e73a2ed31", 0, "0c-duplicated-in-theirs.txt" }, + { 0100644, "7edc726325da726751a4195e434e4377b0f67f9a", 0, "0c-rewritten-in-theirs.txt" }, + { 0100644, "0d872f8e871a30208305978ecbf9e66d864f1638", 0, "1a-newname-in-ours-edited-in-theirs.txt" }, + { 0100644, "d0d4594e16f2e19107e3fa7ea63e7aaaff305ffb", 0, "1a-newname-in-ours.txt" }, + { 0100644, "ed9523e62e453e50dd9be1606af19399b96e397a", 0, "1b-newname-in-theirs-edited-in-ours.txt" }, + { 0100644, "2b5f1f181ee3b58ea751f5dd5d8f9b445520a136", 0, "1b-newname-in-theirs.txt" }, + { 0100644, "178940b450f238a56c0d75b7955cb57b38191982", 0, "2-newname-in-both.txt" }, + { 0100644, "18cb316b1cefa0f8a6946f0e201a8e1a6f845ab9", 0, "3a-newname-in-ours-deleted-in-theirs.txt" }, + { 0100644, "36219b49367146cb2e6a1555b5a9ebd4d0328495", 0, "3b-newname-in-theirs-deleted-in-ours.txt" }, + { 0100644, "227792b52aaa0b238bea00ec7e509b02623f168c", 0, "4a-newname-in-ours-added-in-theirs.txt~HEAD" }, + { 0100644, "8b5b53cb2aa9ceb1139f5312fcfa3cc3c5a47c9a", 0, "4a-newname-in-ours-added-in-theirs.txt~rename_conflict_theirs" }, + { 0100644, "de872ee3618b894992e9d1e18ba2ebe256a112f9", 0, "4b-newname-in-theirs-added-in-ours.txt~HEAD" }, + { 0100644, "98d52d07c0b0bbf2b46548f6aa521295c2cb55db", 0, "4b-newname-in-theirs-added-in-ours.txt~rename_conflict_theirs" }, + { 0100644, "d3719a5ae8e4d92276b5313ce976f6ee5af2b436", 0, "5a-newname-in-ours-added-in-theirs.txt~HEAD" }, + { 0100644, "98ba4205fcf31f5dd93c916d35fe3f3b3d0e6714", 0, "5a-newname-in-ours-added-in-theirs.txt~rename_conflict_theirs" }, + { 0100644, "385c8a0f26ddf79e9041e15e17dc352ed2c4cced", 0, "5b-newname-in-theirs-added-in-ours.txt~HEAD" }, + { 0100644, "63247125386de9ec90a27ad36169307bf8a11a38", 0, "5b-newname-in-theirs-added-in-ours.txt~rename_conflict_theirs" }, + { 0100644, "d8fa77b6833082c1ea36b7828a582d4c43882450", 0, "6-both-renamed-1-to-2-ours.txt" }, + { 0100644, "d8fa77b6833082c1ea36b7828a582d4c43882450", 0, "6-both-renamed-1-to-2-theirs.txt" }, + { 0100644, "b42712cfe99a1a500b2a51fe984e0b8a7702ba11", 0, "7-both-renamed.txt~HEAD" }, + { 0100644, "b69fe837e4cecfd4c9a40cdca7c138468687df07", 0, "7-both-renamed.txt~rename_conflict_theirs" }, + }; + + opts.merge_tree_opts.flags |= GIT_MERGE_TREE_FIND_RENAMES; + opts.merge_tree_opts.rename_threshold = 50; + + cl_git_pass(merge_branches(&result, repo, GIT_REFS_HEADS_DIR BRANCH_RENAME_OURS, GIT_REFS_HEADS_DIR BRANCH_RENAME_THEIRS, &opts)); + cl_assert(merge_test_workdir(repo, merge_index_entries, 24)); + + git_merge_result_free(result); +} + +void test_merge_workdir_renames__ours(void) +{ + git_index *index; + git_merge_result *result; + git_merge_opts opts = GIT_MERGE_OPTS_INIT; + + struct merge_index_entry merge_index_entries[] = { + { 0100644, "68c6c84b091926c7d90aa6a79b2bc3bb6adccd8e", 0, "0a-no-change.txt" }, + { 0100644, "f0ce2b8e4986084d9b308fb72709e414c23eb5e6", 0, "0b-duplicated-in-ours.txt" }, + { 0100644, "e376fbdd06ebf021c92724da9f26f44212734e3e", 0, "0b-rewritten-in-ours.txt" }, + { 0100644, "2f56120107d680129a5d9791b521cb1e73a2ed31", 0, "0c-duplicated-in-theirs.txt" }, + { 0100644, "efc9121fdedaf08ba180b53ebfbcf71bd488ed09", 0, "0c-rewritten-in-theirs.txt" }, + { 0100644, "0d872f8e871a30208305978ecbf9e66d864f1638", 0, "1a-newname-in-ours-edited-in-theirs.txt" }, + { 0100644, "d0d4594e16f2e19107e3fa7ea63e7aaaff305ffb", 0, "1a-newname-in-ours.txt" }, + { 0100644, "ed9523e62e453e50dd9be1606af19399b96e397a", 0, "1b-newname-in-theirs-edited-in-ours.txt" }, + { 0100644, "2b5f1f181ee3b58ea751f5dd5d8f9b445520a136", 0, "1b-newname-in-theirs.txt" }, + { 0100644, "178940b450f238a56c0d75b7955cb57b38191982", 0, "2-newname-in-both.txt" }, + { 0100644, "18cb316b1cefa0f8a6946f0e201a8e1a6f845ab9", 0, "3a-newname-in-ours-deleted-in-theirs.txt" }, + { 0100644, "36219b49367146cb2e6a1555b5a9ebd4d0328495", 0, "3b-newname-in-theirs-deleted-in-ours.txt" }, + { 0100644, "227792b52aaa0b238bea00ec7e509b02623f168c", 0, "4a-newname-in-ours-added-in-theirs.txt" }, + { 0100644, "de872ee3618b894992e9d1e18ba2ebe256a112f9", 0, "4b-newname-in-theirs-added-in-ours.txt" }, + { 0100644, "d3719a5ae8e4d92276b5313ce976f6ee5af2b436", 0, "5a-newname-in-ours-added-in-theirs.txt" }, + { 0100644, "385c8a0f26ddf79e9041e15e17dc352ed2c4cced", 0, "5b-newname-in-theirs-added-in-ours.txt" }, + { 0100644, "63247125386de9ec90a27ad36169307bf8a11a38", 0, "5b-renamed-in-theirs-added-in-ours.txt" }, + { 0100644, "d8fa77b6833082c1ea36b7828a582d4c43882450", 0, "6-both-renamed-1-to-2-ours.txt" }, + { 0100644, "b69fe837e4cecfd4c9a40cdca7c138468687df07", 0, "7-both-renamed-side-2.txt" }, + { 0100644, "b42712cfe99a1a500b2a51fe984e0b8a7702ba11", 0, "7-both-renamed.txt" }, + }; + + opts.merge_tree_opts.flags |= GIT_MERGE_TREE_FIND_RENAMES; + opts.merge_tree_opts.rename_threshold = 50; + opts.checkout_opts.checkout_strategy = GIT_CHECKOUT_SAFE | GIT_CHECKOUT_USE_OURS; + + cl_git_pass(merge_branches(&result, repo, GIT_REFS_HEADS_DIR BRANCH_RENAME_OURS, GIT_REFS_HEADS_DIR BRANCH_RENAME_THEIRS, &opts)); + cl_git_pass(git_repository_index(&index, repo)); + cl_git_pass(git_index_write(index)); + cl_assert(merge_test_workdir(repo, merge_index_entries, 20)); + + git_merge_result_free(result); + git_index_free(index); +} + +void test_merge_workdir_renames__similar(void) +{ + git_merge_result *result; + git_merge_opts opts = GIT_MERGE_OPTS_INIT; + + /* + * Note: this differs slightly from the core git merge result - there, 4a is + * tracked as a rename/delete instead of a rename/add and the theirs side + * is not placed in workdir in any form. + */ + struct merge_index_entry merge_index_entries[] = { + { 0100644, "68c6c84b091926c7d90aa6a79b2bc3bb6adccd8e", 0, "0a-no-change.txt" }, + { 0100644, "f0ce2b8e4986084d9b308fb72709e414c23eb5e6", 0, "0b-duplicated-in-ours.txt" }, + { 0100644, "8aac75de2a34b4d340bf62a6e58197269cb55797", 0, "0b-rewritten-in-ours.txt" }, + { 0100644, "2f56120107d680129a5d9791b521cb1e73a2ed31", 0, "0c-duplicated-in-theirs.txt" }, + { 0100644, "7edc726325da726751a4195e434e4377b0f67f9a", 0, "0c-rewritten-in-theirs.txt" }, + { 0100644, "0d872f8e871a30208305978ecbf9e66d864f1638", 0, "1a-newname-in-ours-edited-in-theirs.txt" }, + { 0100644, "d0d4594e16f2e19107e3fa7ea63e7aaaff305ffb", 0, "1a-newname-in-ours.txt" }, + { 0100644, "ed9523e62e453e50dd9be1606af19399b96e397a", 0, "1b-newname-in-theirs-edited-in-ours.txt" }, + { 0100644, "2b5f1f181ee3b58ea751f5dd5d8f9b445520a136", 0, "1b-newname-in-theirs.txt" }, + { 0100644, "178940b450f238a56c0d75b7955cb57b38191982", 0, "2-newname-in-both.txt" }, + { 0100644, "18cb316b1cefa0f8a6946f0e201a8e1a6f845ab9", 0, "3a-newname-in-ours-deleted-in-theirs.txt" }, + { 0100644, "36219b49367146cb2e6a1555b5a9ebd4d0328495", 0, "3b-newname-in-theirs-deleted-in-ours.txt" }, + { 0100644, "227792b52aaa0b238bea00ec7e509b02623f168c", 0, "4a-newname-in-ours-added-in-theirs.txt~HEAD" }, + { 0100644, "8b5b53cb2aa9ceb1139f5312fcfa3cc3c5a47c9a", 0, "4a-newname-in-ours-added-in-theirs.txt~rename_conflict_theirs" }, + { 0100644, "de872ee3618b894992e9d1e18ba2ebe256a112f9", 0, "4b-newname-in-theirs-added-in-ours.txt~HEAD" }, + { 0100644, "98d52d07c0b0bbf2b46548f6aa521295c2cb55db", 0, "4b-newname-in-theirs-added-in-ours.txt~rename_conflict_theirs" }, + { 0100644, "d3719a5ae8e4d92276b5313ce976f6ee5af2b436", 0, "5a-newname-in-ours-added-in-theirs.txt~HEAD" }, + { 0100644, "98ba4205fcf31f5dd93c916d35fe3f3b3d0e6714", 0, "5a-newname-in-ours-added-in-theirs.txt~rename_conflict_theirs" }, + { 0100644, "385c8a0f26ddf79e9041e15e17dc352ed2c4cced", 0, "5b-newname-in-theirs-added-in-ours.txt~HEAD" }, + { 0100644, "63247125386de9ec90a27ad36169307bf8a11a38", 0, "5b-newname-in-theirs-added-in-ours.txt~rename_conflict_theirs" }, + { 0100644, "d8fa77b6833082c1ea36b7828a582d4c43882450", 0, "6-both-renamed-1-to-2-ours.txt" }, + { 0100644, "d8fa77b6833082c1ea36b7828a582d4c43882450", 0, "6-both-renamed-1-to-2-theirs.txt" }, + { 0100644, "b42712cfe99a1a500b2a51fe984e0b8a7702ba11", 0, "7-both-renamed.txt~HEAD" }, + { 0100644, "b69fe837e4cecfd4c9a40cdca7c138468687df07", 0, "7-both-renamed.txt~rename_conflict_theirs" }, + }; + + opts.merge_tree_opts.flags |= GIT_MERGE_TREE_FIND_RENAMES; + opts.merge_tree_opts.rename_threshold = 50; + + cl_git_pass(merge_branches(&result, repo, GIT_REFS_HEADS_DIR BRANCH_RENAME_OURS, GIT_REFS_HEADS_DIR BRANCH_RENAME_THEIRS, &opts)); + cl_assert(merge_test_workdir(repo, merge_index_entries, 24)); + + git_merge_result_free(result); +} + diff --git a/tests-clar/merge/workdir/setup.c b/tests-clar/merge/workdir/setup.c index 1c8403221..511106a58 100644 --- a/tests-clar/merge/workdir/setup.c +++ b/tests-clar/merge/workdir/setup.c @@ -71,7 +71,7 @@ static void write_file_contents(const char *filename, const char *output) git_buf_free(&file_path_buf); } -/* git merge --no-ff octo1 */ +/* git merge octo1 */ void test_merge_workdir_setup__one_branch(void) { git_oid our_oid; @@ -97,7 +97,33 @@ void test_merge_workdir_setup__one_branch(void) git_merge_head_free(their_heads[0]); } -/* git merge --no-ff 16f825815cfd20a07a75c71554e82d8eede0b061 */ +/* git merge --no-ff octo1 */ +void test_merge_workdir_setup__no_fastforward(void) +{ + git_oid our_oid; + git_reference *octo1_ref; + git_merge_head *our_head, *their_heads[1]; + + cl_git_pass(git_oid_fromstr(&our_oid, ORIG_HEAD)); + cl_git_pass(git_merge_head_from_oid(&our_head, repo, &our_oid)); + + cl_git_pass(git_reference_lookup(&octo1_ref, repo, GIT_REFS_HEADS_DIR OCTO1_BRANCH)); + cl_git_pass(git_merge_head_from_ref(&their_heads[0], repo, octo1_ref)); + + cl_git_pass(git_merge__setup(repo, our_head, (const git_merge_head **)their_heads, 1, GIT_MERGE_NO_FASTFORWARD)); + + cl_assert(test_file_contents(GIT_MERGE_HEAD_FILE, OCTO1_OID "\n")); + cl_assert(test_file_contents(GIT_ORIG_HEAD_FILE, ORIG_HEAD "\n")); + cl_assert(test_file_contents(GIT_MERGE_MODE_FILE, "no-ff")); + cl_assert(test_file_contents(GIT_MERGE_MSG_FILE, "Merge branch '" OCTO1_BRANCH "'\n")); + + git_reference_free(octo1_ref); + + git_merge_head_free(our_head); + git_merge_head_free(their_heads[0]); +} + +/* git merge 16f825815cfd20a07a75c71554e82d8eede0b061 */ void test_merge_workdir_setup__one_oid(void) { git_oid our_oid; @@ -964,3 +990,65 @@ void test_merge_workdir_setup__head_foreach_octopus(void) cl_assert(cb_data.i == cb_data.len); } + +void test_merge_workdir_setup__retained_after_success(void) +{ + git_oid our_oid; + git_reference *octo1_ref; + git_merge_head *our_head, *their_heads[1]; + git_merge_result *result; + git_merge_opts opts = GIT_MERGE_OPTS_INIT; + + opts.merge_flags |= GIT_MERGE_NO_FASTFORWARD; + + cl_git_pass(git_oid_fromstr(&our_oid, ORIG_HEAD)); + cl_git_pass(git_merge_head_from_oid(&our_head, repo, &our_oid)); + + cl_git_pass(git_reference_lookup(&octo1_ref, repo, GIT_REFS_HEADS_DIR OCTO1_BRANCH)); + + cl_git_pass(git_merge_head_from_ref(&their_heads[0], repo, octo1_ref)); + + cl_git_pass(git_merge(&result, repo, (const git_merge_head **)&their_heads[0], 1, &opts)); + + cl_assert(test_file_contents(GIT_MERGE_HEAD_FILE, OCTO1_OID "\n")); + cl_assert(test_file_contents(GIT_ORIG_HEAD_FILE, ORIG_HEAD "\n")); + cl_assert(test_file_contents(GIT_MERGE_MODE_FILE, "no-ff")); + cl_assert(test_file_contents(GIT_MERGE_MSG_FILE, "Merge branch '" OCTO1_BRANCH "'\n")); + + git_reference_free(octo1_ref); + + git_merge_head_free(our_head); + git_merge_head_free(their_heads[0]); +} + +void test_merge_workdir_setup__removed_after_failure(void) +{ + git_oid our_oid; + git_reference *octo1_ref; + git_merge_head *our_head, *their_heads[1]; + git_merge_result *result; + git_merge_opts opts = GIT_MERGE_OPTS_INIT; + + opts.merge_flags |= GIT_MERGE_NO_FASTFORWARD; + + cl_git_pass(git_oid_fromstr(&our_oid, ORIG_HEAD)); + cl_git_pass(git_merge_head_from_oid(&our_head, repo, &our_oid)); + + cl_git_pass(git_reference_lookup(&octo1_ref, repo, GIT_REFS_HEADS_DIR OCTO1_BRANCH)); + cl_git_pass(git_merge_head_from_ref(&their_heads[0], repo, octo1_ref)); + + cl_git_rewritefile("merge-resolve/new-in-octo1.txt", + "Conflicting file!\n\nMerge will fail!\n"); + + cl_git_fail(git_merge(&result, repo, &their_heads[0], 1, &opts)); + + cl_assert(!git_path_exists("merge-resolve/" GIT_MERGE_HEAD_FILE)); + cl_assert(!git_path_exists("merge-resolve/" GIT_ORIG_HEAD_FILE)); + cl_assert(!git_path_exists("merge-resolve/" GIT_MERGE_MODE_FILE)); + cl_assert(!git_path_exists("merge-resolve/" GIT_MERGE_MSG_FILE)); + + git_reference_free(octo1_ref); + + git_merge_head_free(our_head); + git_merge_head_free(their_heads[0]); +} diff --git a/tests-clar/merge/workdir/simple.c b/tests-clar/merge/workdir/simple.c new file mode 100644 index 000000000..4a3b86ee4 --- /dev/null +++ b/tests-clar/merge/workdir/simple.c @@ -0,0 +1,491 @@ +#include "clar_libgit2.h" +#include "git2/repository.h" +#include "git2/merge.h" +#include "buffer.h" +#include "merge.h" +#include "../merge_helpers.h" +#include "refs.h" +#include "fileops.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_SIMPLE_BRANCH "branch" +#define THEIRS_SIMPLE_OID "7cb63eed597130ba4abb87b3e544b85021905520" + +#define THEIRS_UNRELATED_BRANCH "unrelated" +#define THEIRS_UNRELATED_OID "55b4e4687e7a0d9ca367016ed930f385d4022e6f" +#define THEIRS_UNRELATED_PARENT "d6cf6c7741b3316826af1314042550c97ded1d50" + +#define OURS_DIRECTORY_FILE "df_side1" +#define THEIRS_DIRECTORY_FILE "fc90237dc4891fa6c69827fc465632225e391618" + + +/* Non-conflicting files, index entries are common to every merge operation */ +#define ADDED_IN_MASTER_INDEX_ENTRY \ + { 0100644, "233c0919c998ed110a4b6ff36f353aec8b713487", 0, \ + "added-in-master.txt" } +#define AUTOMERGEABLE_INDEX_ENTRY \ + { 0100644, "f2e1550a0c9e53d5811175864a29536642ae3821", 0, \ + "automergeable.txt" } +#define CHANGED_IN_BRANCH_INDEX_ENTRY \ + { 0100644, "4eb04c9e79e88f6640d01ff5b25ca2a60764f216", 0, \ + "changed-in-branch.txt" } +#define CHANGED_IN_MASTER_INDEX_ENTRY \ + { 0100644, "11deab00b2d3a6f5a3073988ac050c2d7b6655e2", 0, \ + "changed-in-master.txt" } +#define UNCHANGED_INDEX_ENTRY \ + { 0100644, "c8f06f2e3bb2964174677e91f0abead0e43c9e5d", 0, \ + "unchanged.txt" } + +/* Unrelated files */ +#define UNRELATED_NEW1 \ + { 0100644, "ef58fdd8086c243bdc81f99e379acacfd21d32d6", 0, \ + "new-in-unrelated1.txt" } +#define UNRELATED_NEW2 \ + { 0100644, "948ba6e701c1edab0c2d394fb7c5538334129793", 0, \ + "new-in-unrelated2.txt" } + +/* Expected REUC entries */ +#define AUTOMERGEABLE_REUC_ENTRY \ + { "automergeable.txt", 0100644, 0100644, 0100644, \ + "6212c31dab5e482247d7977e4f0dd3601decf13b", \ + "ee3fa1b8c00aff7fe02065fdb50864bb0d932ccf", \ + "058541fc37114bfc1dddf6bd6bffc7fae5c2e6fe" } +#define CONFLICTING_REUC_ENTRY \ + { "conflicting.txt", 0100644, 0100644, 0100644, \ + "d427e0b2e138501a3d15cc376077a3631e15bd46", \ + "4e886e602529caa9ab11d71f86634bd1b6e0de10", \ + "2bd0a343aeef7a2cf0d158478966a6e587ff3863" } +#define REMOVED_IN_BRANCH_REUC_ENTRY \ + { "removed-in-branch.txt", 0100644, 0100644, 0, \ + "dfe3f22baa1f6fce5447901c3086bae368de6bdd", \ + "dfe3f22baa1f6fce5447901c3086bae368de6bdd", \ + "" } +#define REMOVED_IN_MASTER_REUC_ENTRY \ + { "removed-in-master.txt", 0100644, 0, 0100644, \ + "5c3b68a71fc4fa5d362fd3875e53137c6a5ab7a5", \ + "", \ + "5c3b68a71fc4fa5d362fd3875e53137c6a5ab7a5" } + +#define AUTOMERGEABLE_MERGED_FILE \ + "this file is changed in master\n" \ + "this file is automergeable\n" \ + "this file is automergeable\n" \ + "this file is automergeable\n" \ + "this file is automergeable\n" \ + "this file is automergeable\n" \ + "this file is automergeable\n" \ + "this file is automergeable\n" \ + "this file is changed in branch\n" + +#define AUTOMERGEABLE_MERGED_FILE_CRLF \ + "this file is changed in master\r\n" \ + "this file is automergeable\r\n" \ + "this file is automergeable\r\n" \ + "this file is automergeable\r\n" \ + "this file is automergeable\r\n" \ + "this file is automergeable\r\n" \ + "this file is automergeable\r\n" \ + "this file is automergeable\r\n" \ + "this file is changed in branch\r\n" + +#define CONFLICTING_DIFF3_FILE \ + "<<<<<<< HEAD\n" \ + "this file is changed in master and branch\n" \ + "=======\n" \ + "this file is changed in branch and master\n" \ + ">>>>>>> 7cb63eed597130ba4abb87b3e544b85021905520\n" + +// Fixture setup and teardown +void test_merge_workdir_simple__initialize(void) +{ + repo = cl_git_sandbox_init(TEST_REPO_PATH); + git_repository_index(&repo_index, repo); +} + +void test_merge_workdir_simple__cleanup(void) +{ + git_index_free(repo_index); + cl_git_sandbox_cleanup(); +} + +static git_merge_result *merge_simple_branch(int automerge_flags, int checkout_strategy) +{ + git_oid their_oids[1]; + git_merge_head *their_heads[1]; + git_merge_result *result; + git_merge_opts opts = GIT_MERGE_OPTS_INIT; + + cl_git_pass(git_oid_fromstr(&their_oids[0], THEIRS_SIMPLE_OID)); + cl_git_pass(git_merge_head_from_oid(&their_heads[0], repo, &their_oids[0])); + + opts.merge_tree_opts.automerge_flags = automerge_flags; + opts.checkout_opts.checkout_strategy = checkout_strategy; + cl_git_pass(git_merge(&result, repo, (const git_merge_head **)their_heads, 1, &opts)); + + git_merge_head_free(their_heads[0]); + + return result; +} + +static void set_core_autocrlf_to(git_repository *repo, bool value) +{ + git_config *cfg; + + cl_git_pass(git_repository_config(&cfg, repo)); + cl_git_pass(git_config_set_bool(cfg, "core.autocrlf", value)); + + git_config_free(cfg); +} + +void test_merge_workdir_simple__automerge(void) +{ + git_index *index; + const git_index_entry *entry; + git_merge_result *result; + git_buf automergeable_buf = GIT_BUF_INIT; + + struct merge_index_entry merge_index_entries[] = { + ADDED_IN_MASTER_INDEX_ENTRY, + AUTOMERGEABLE_INDEX_ENTRY, + CHANGED_IN_BRANCH_INDEX_ENTRY, + CHANGED_IN_MASTER_INDEX_ENTRY, + + { 0100644, "d427e0b2e138501a3d15cc376077a3631e15bd46", 1, "conflicting.txt" }, + { 0100644, "4e886e602529caa9ab11d71f86634bd1b6e0de10", 2, "conflicting.txt" }, + { 0100644, "2bd0a343aeef7a2cf0d158478966a6e587ff3863", 3, "conflicting.txt" }, + + UNCHANGED_INDEX_ENTRY, + }; + + struct merge_reuc_entry merge_reuc_entries[] = { + AUTOMERGEABLE_REUC_ENTRY, + REMOVED_IN_BRANCH_REUC_ENTRY, + REMOVED_IN_MASTER_REUC_ENTRY + }; + + + set_core_autocrlf_to(repo, false); + + cl_assert(result = merge_simple_branch(0, 0)); + cl_assert(!git_merge_result_is_fastforward(result)); + + cl_git_pass(git_futils_readbuffer(&automergeable_buf, + TEST_REPO_PATH "/automergeable.txt")); + cl_assert(strcmp(automergeable_buf.ptr, AUTOMERGEABLE_MERGED_FILE) == 0); + git_buf_free(&automergeable_buf); + + cl_assert(merge_test_index(repo_index, merge_index_entries, 8)); + cl_assert(merge_test_reuc(repo_index, merge_reuc_entries, 3)); + + git_merge_result_free(result); + + git_repository_index(&index, repo); + + cl_assert((entry = git_index_get_bypath(index, "automergeable.txt", 0)) != NULL); + cl_assert(entry->file_size == strlen(AUTOMERGEABLE_MERGED_FILE)); + + git_index_free(index); +} + +void test_merge_workdir_simple__automerge_crlf(void) +{ +#ifdef GIT_WIN32 + git_index *index; + const git_index_entry *entry; + + git_merge_result *result; + git_buf automergeable_buf = GIT_BUF_INIT; + + struct merge_index_entry merge_index_entries[] = { + ADDED_IN_MASTER_INDEX_ENTRY, + AUTOMERGEABLE_INDEX_ENTRY, + CHANGED_IN_BRANCH_INDEX_ENTRY, + CHANGED_IN_MASTER_INDEX_ENTRY, + + { 0100644, "d427e0b2e138501a3d15cc376077a3631e15bd46", 1, "conflicting.txt" }, + { 0100644, "4e886e602529caa9ab11d71f86634bd1b6e0de10", 2, "conflicting.txt" }, + { 0100644, "2bd0a343aeef7a2cf0d158478966a6e587ff3863", 3, "conflicting.txt" }, + + UNCHANGED_INDEX_ENTRY, + }; + + struct merge_reuc_entry merge_reuc_entries[] = { + AUTOMERGEABLE_REUC_ENTRY, + REMOVED_IN_BRANCH_REUC_ENTRY, + REMOVED_IN_MASTER_REUC_ENTRY + }; + + set_core_autocrlf_to(repo, true); + + cl_assert(result = merge_simple_branch(0, 0)); + cl_assert(!git_merge_result_is_fastforward(result)); + + cl_git_pass(git_futils_readbuffer(&automergeable_buf, + TEST_REPO_PATH "/automergeable.txt")); + cl_assert(strcmp(automergeable_buf.ptr, AUTOMERGEABLE_MERGED_FILE_CRLF) == 0); + git_buf_free(&automergeable_buf); + + cl_assert(merge_test_index(repo_index, merge_index_entries, 8)); + cl_assert(merge_test_reuc(repo_index, merge_reuc_entries, 3)); + + git_merge_result_free(result); + + git_repository_index(&index, repo); + + cl_assert((entry = git_index_get_bypath(index, "automergeable.txt", 0)) != NULL); + cl_assert(entry->file_size == strlen(AUTOMERGEABLE_MERGED_FILE_CRLF)); + + git_index_free(index); +#endif /* GIT_WIN32 */ +} + +void test_merge_workdir_simple__diff3(void) +{ + git_merge_result *result; + git_buf conflicting_buf = GIT_BUF_INIT; + + struct merge_index_entry merge_index_entries[] = { + ADDED_IN_MASTER_INDEX_ENTRY, + AUTOMERGEABLE_INDEX_ENTRY, + CHANGED_IN_BRANCH_INDEX_ENTRY, + CHANGED_IN_MASTER_INDEX_ENTRY, + + { 0100644, "d427e0b2e138501a3d15cc376077a3631e15bd46", 1, "conflicting.txt" }, + { 0100644, "4e886e602529caa9ab11d71f86634bd1b6e0de10", 2, "conflicting.txt" }, + { 0100644, "2bd0a343aeef7a2cf0d158478966a6e587ff3863", 3, "conflicting.txt" }, + + UNCHANGED_INDEX_ENTRY, + }; + + struct merge_reuc_entry merge_reuc_entries[] = { + AUTOMERGEABLE_REUC_ENTRY, + REMOVED_IN_BRANCH_REUC_ENTRY, + REMOVED_IN_MASTER_REUC_ENTRY + }; + + cl_assert(result = merge_simple_branch(0, 0)); + cl_assert(!git_merge_result_is_fastforward(result)); + + cl_git_pass(git_futils_readbuffer(&conflicting_buf, + TEST_REPO_PATH "/conflicting.txt")); + cl_assert(strcmp(conflicting_buf.ptr, CONFLICTING_DIFF3_FILE) == 0); + git_buf_free(&conflicting_buf); + + cl_assert(merge_test_index(repo_index, merge_index_entries, 8)); + cl_assert(merge_test_reuc(repo_index, merge_reuc_entries, 3)); + + git_merge_result_free(result); +} + +void test_merge_workdir_simple__checkout_ours(void) +{ + git_merge_result *result; + + struct merge_index_entry merge_index_entries[] = { + ADDED_IN_MASTER_INDEX_ENTRY, + AUTOMERGEABLE_INDEX_ENTRY, + CHANGED_IN_BRANCH_INDEX_ENTRY, + CHANGED_IN_MASTER_INDEX_ENTRY, + + { 0100644, "d427e0b2e138501a3d15cc376077a3631e15bd46", 1, "conflicting.txt" }, + { 0100644, "4e886e602529caa9ab11d71f86634bd1b6e0de10", 2, "conflicting.txt" }, + { 0100644, "2bd0a343aeef7a2cf0d158478966a6e587ff3863", 3, "conflicting.txt" }, + + UNCHANGED_INDEX_ENTRY, + }; + + struct merge_reuc_entry merge_reuc_entries[] = { + AUTOMERGEABLE_REUC_ENTRY, + REMOVED_IN_BRANCH_REUC_ENTRY, + REMOVED_IN_MASTER_REUC_ENTRY + }; + + cl_assert(result = merge_simple_branch(0, GIT_CHECKOUT_SAFE | GIT_CHECKOUT_USE_OURS)); + cl_assert(!git_merge_result_is_fastforward(result)); + + cl_assert(merge_test_index(repo_index, merge_index_entries, 8)); + cl_assert(merge_test_reuc(repo_index, merge_reuc_entries, 3)); + + cl_assert(git_path_exists(TEST_REPO_PATH "/conflicting.txt")); + + git_merge_result_free(result); +} + +void test_merge_workdir_simple__favor_ours(void) +{ + git_merge_result *result; + + struct merge_index_entry merge_index_entries[] = { + ADDED_IN_MASTER_INDEX_ENTRY, + AUTOMERGEABLE_INDEX_ENTRY, + CHANGED_IN_BRANCH_INDEX_ENTRY, + CHANGED_IN_MASTER_INDEX_ENTRY, + { 0100644, "4e886e602529caa9ab11d71f86634bd1b6e0de10", 0, "conflicting.txt" }, + UNCHANGED_INDEX_ENTRY, + }; + + struct merge_reuc_entry merge_reuc_entries[] = { + AUTOMERGEABLE_REUC_ENTRY, + CONFLICTING_REUC_ENTRY, + REMOVED_IN_BRANCH_REUC_ENTRY, + REMOVED_IN_MASTER_REUC_ENTRY, + }; + + cl_assert(result = merge_simple_branch(GIT_MERGE_AUTOMERGE_FAVOR_OURS, 0)); + cl_assert(!git_merge_result_is_fastforward(result)); + + cl_assert(merge_test_index(repo_index, merge_index_entries, 6)); + cl_assert(merge_test_reuc(repo_index, merge_reuc_entries, 4)); + + git_merge_result_free(result); +} + +void test_merge_workdir_simple__favor_theirs(void) +{ + git_merge_result *result; + + struct merge_index_entry merge_index_entries[] = { + ADDED_IN_MASTER_INDEX_ENTRY, + AUTOMERGEABLE_INDEX_ENTRY, + CHANGED_IN_BRANCH_INDEX_ENTRY, + CHANGED_IN_MASTER_INDEX_ENTRY, + { 0100644, "2bd0a343aeef7a2cf0d158478966a6e587ff3863", 0, "conflicting.txt" }, + UNCHANGED_INDEX_ENTRY, + }; + + struct merge_reuc_entry merge_reuc_entries[] = { + AUTOMERGEABLE_REUC_ENTRY, + CONFLICTING_REUC_ENTRY, + REMOVED_IN_BRANCH_REUC_ENTRY, + REMOVED_IN_MASTER_REUC_ENTRY, + }; + + cl_assert(result = merge_simple_branch(GIT_MERGE_AUTOMERGE_FAVOR_THEIRS, 0)); + cl_assert(!git_merge_result_is_fastforward(result)); + + cl_assert(merge_test_index(repo_index, merge_index_entries, 6)); + cl_assert(merge_test_reuc(repo_index, merge_reuc_entries, 4)); + + git_merge_result_free(result); +} + +void test_merge_workdir_simple__directory_file(void) +{ + git_reference *head; + git_oid their_oids[1], head_commit_id; + git_merge_head *their_heads[1]; + git_merge_result *result; + git_merge_opts opts = GIT_MERGE_OPTS_INIT; + git_commit *head_commit; + + struct merge_index_entry merge_index_entries[] = { + { 0100644, "49130a28ef567af9a6a6104c38773fedfa5f9742", 2, "dir-10" }, + { 0100644, "6c06dcd163587c2cc18be44857e0b71116382aeb", 3, "dir-10" }, + { 0100644, "43aafd43bea779ec74317dc361f45ae3f532a505", 0, "dir-6" }, + { 0100644, "a031a28ae70e33a641ce4b8a8f6317f1ab79dee4", 3, "dir-7" }, + { 0100644, "5012fd565b1393bdfda1805d4ec38ce6619e1fd1", 1, "dir-7/file.txt" }, + { 0100644, "a5563304ddf6caba25cb50323a2ea6f7dbfcadca", 2, "dir-7/file.txt" }, + { 0100644, "e9ad6ec3e38364a3d07feda7c4197d4d845c53b5", 0, "dir-8" }, + { 0100644, "3ef4d30382ca33fdeba9fda895a99e0891ba37aa", 2, "dir-9" }, + { 0100644, "fc4c636d6515e9e261f9260dbcf3cc6eca97ea08", 1, "dir-9/file.txt" }, + { 0100644, "76ab0e2868197ec158ddd6c78d8a0d2fd73d38f9", 3, "dir-9/file.txt" }, + { 0100644, "5c2411f8075f48a6b2fdb85ebc0d371747c4df15", 0, "file-1/new" }, + { 0100644, "a39a620dae5bc8b4e771cd4d251b7d080401a21e", 1, "file-2" }, + { 0100644, "d963979c237d08b6ba39062ee7bf64c7d34a27f8", 2, "file-2" }, + { 0100644, "5c341ead2ba6f2af98ce5ec3fe84f6b6d2899c0d", 0, "file-2/new" }, + { 0100644, "9efe7723802d4305142eee177e018fee1572c4f4", 0, "file-3/new" }, + { 0100644, "bacac9b3493509aa15e1730e1545fc0919d1dae0", 1, "file-4" }, + { 0100644, "7663fce0130db092936b137cabd693ec234eb060", 3, "file-4" }, + { 0100644, "e49f917b448d1340b31d76e54ba388268fd4c922", 0, "file-4/new" }, + { 0100644, "cab2cf23998b40f1af2d9d9a756dc9e285a8df4b", 2, "file-5/new" }, + { 0100644, "f5504f36e6f4eb797a56fc5bac6c6c7f32969bf2", 3, "file-5/new" }, + }; + + cl_git_pass(git_reference_symbolic_create(&head, repo, GIT_HEAD_FILE, GIT_REFS_HEADS_DIR OURS_DIRECTORY_FILE, 1)); + cl_git_pass(git_reference_name_to_id(&head_commit_id, repo, GIT_HEAD_FILE)); + cl_git_pass(git_commit_lookup(&head_commit, repo, &head_commit_id)); + cl_git_pass(git_reset(repo, (git_object *)head_commit, GIT_RESET_HARD)); + + cl_git_pass(git_oid_fromstr(&their_oids[0], THEIRS_DIRECTORY_FILE)); + cl_git_pass(git_merge_head_from_oid(&their_heads[0], repo, &their_oids[0])); + + opts.merge_tree_opts.automerge_flags = 0; + cl_git_pass(git_merge(&result, repo, (const git_merge_head **)their_heads, 1, &opts)); + + cl_assert(merge_test_index(repo_index, merge_index_entries, 20)); + + git_reference_free(head); + git_commit_free(head_commit); + git_merge_head_free(their_heads[0]); + git_merge_result_free(result); +} + +void test_merge_workdir_simple__unrelated(void) +{ + git_oid their_oids[1]; + git_merge_head *their_heads[1]; + git_merge_result *result; + git_merge_opts opts = GIT_MERGE_OPTS_INIT; + + 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, "11deab00b2d3a6f5a3073988ac050c2d7b6655e2", 0, "changed-in-master.txt" }, + { 0100644, "4e886e602529caa9ab11d71f86634bd1b6e0de10", 0, "conflicting.txt" }, + { 0100644, "ef58fdd8086c243bdc81f99e379acacfd21d32d6", 0, "new-in-unrelated1.txt" }, + { 0100644, "948ba6e701c1edab0c2d394fb7c5538334129793", 0, "new-in-unrelated2.txt" }, + { 0100644, "dfe3f22baa1f6fce5447901c3086bae368de6bdd", 0, "removed-in-branch.txt" }, + { 0100644, "c8f06f2e3bb2964174677e91f0abead0e43c9e5d", 0, "unchanged.txt" }, + }; + + cl_git_pass(git_oid_fromstr(&their_oids[0], THEIRS_UNRELATED_PARENT)); + cl_git_pass(git_merge_head_from_oid(&their_heads[0], repo, &their_oids[0])); + + opts.merge_tree_opts.automerge_flags = 0; + cl_git_pass(git_merge(&result, repo, (const git_merge_head **)their_heads, 1, &opts)); + + cl_assert(merge_test_index(repo_index, merge_index_entries, 9)); + + git_merge_head_free(their_heads[0]); + git_merge_result_free(result); +} + +void test_merge_workdir_simple__unrelated_with_conflicts(void) +{ + git_oid their_oids[1]; + git_merge_head *their_heads[1]; + git_merge_result *result; + git_merge_opts opts = GIT_MERGE_OPTS_INIT; + + struct merge_index_entry merge_index_entries[] = { + { 0100644, "233c0919c998ed110a4b6ff36f353aec8b713487", 0, "added-in-master.txt" }, + { 0100644, "ee3fa1b8c00aff7fe02065fdb50864bb0d932ccf", 2, "automergeable.txt" }, + { 0100644, "d07ec190c306ec690bac349e87d01c4358e49bb2", 3, "automergeable.txt" }, + { 0100644, "ab6c44a2e84492ad4b41bb6bac87353e9d02ac8b", 0, "changed-in-branch.txt" }, + { 0100644, "11deab00b2d3a6f5a3073988ac050c2d7b6655e2", 0, "changed-in-master.txt" }, + { 0100644, "4e886e602529caa9ab11d71f86634bd1b6e0de10", 2, "conflicting.txt" }, + { 0100644, "4b253da36a0ae8bfce63aeabd8c5b58429925594", 3, "conflicting.txt" }, + { 0100644, "ef58fdd8086c243bdc81f99e379acacfd21d32d6", 0, "new-in-unrelated1.txt" }, + { 0100644, "948ba6e701c1edab0c2d394fb7c5538334129793", 0, "new-in-unrelated2.txt" }, + { 0100644, "dfe3f22baa1f6fce5447901c3086bae368de6bdd", 0, "removed-in-branch.txt" }, + { 0100644, "c8f06f2e3bb2964174677e91f0abead0e43c9e5d", 0, "unchanged.txt" }, + }; + + cl_git_pass(git_oid_fromstr(&their_oids[0], THEIRS_UNRELATED_OID)); + cl_git_pass(git_merge_head_from_oid(&their_heads[0], repo, &their_oids[0])); + + opts.merge_tree_opts.automerge_flags = 0; + cl_git_pass(git_merge(&result, repo, (const git_merge_head **)their_heads, 1, &opts)); + + cl_assert(merge_test_index(repo_index, merge_index_entries, 11)); + + git_merge_head_free(their_heads[0]); + git_merge_result_free(result); +} + diff --git a/tests-clar/merge/workdir/trivial.c b/tests-clar/merge/workdir/trivial.c new file mode 100644 index 000000000..d20d89879 --- /dev/null +++ b/tests-clar/merge/workdir/trivial.c @@ -0,0 +1,341 @@ +#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" +#include "fileops.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" + + +// Fixture setup and teardown +void test_merge_workdir_trivial__initialize(void) +{ + repo = cl_git_sandbox_init(TEST_REPO_PATH); + git_repository_index(&repo_index, repo); +} + +void test_merge_workdir_trivial__cleanup(void) +{ + git_index_free(repo_index); + cl_git_sandbox_cleanup(); +} + + +static int merge_trivial(const char *ours, const char *theirs, bool automerge) +{ + git_buf branch_buf = GIT_BUF_INIT; + git_checkout_opts checkout_opts = GIT_CHECKOUT_OPTS_INIT; + git_reference *our_ref, *their_ref; + git_merge_head *their_heads[1]; + git_merge_opts opts = GIT_MERGE_OPTS_INIT; + git_merge_result *result; + + checkout_opts.checkout_strategy = GIT_CHECKOUT_FORCE; + + opts.merge_tree_opts.automerge_flags |= automerge ? 0 : GIT_MERGE_AUTOMERGE_NONE; + + git_buf_printf(&branch_buf, "%s%s", GIT_REFS_HEADS_DIR, ours); + cl_git_pass(git_reference_symbolic_create(&our_ref, repo, "HEAD", branch_buf.ptr, 1)); + + cl_git_pass(git_checkout_head(repo, &checkout_opts)); + + git_buf_clear(&branch_buf); + git_buf_printf(&branch_buf, "%s%s", GIT_REFS_HEADS_DIR, theirs); + cl_git_pass(git_reference_lookup(&their_ref, repo, branch_buf.ptr)); + 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_buf_free(&branch_buf); + git_reference_free(our_ref); + git_reference_free(their_ref); + git_merge_head_free(their_heads[0]); + git_merge_result_free(result); + + return 0; +} + +static int merge_trivial_conflict_entrycount(void) +{ + const git_index_entry *entry; + size_t count = 0; + size_t i; + + for (i = 0; i < git_index_entrycount(repo_index); i++) { + cl_assert(entry = git_index_get_byindex(repo_index, i)); + + if (git_index_entry_stage(entry) > 0) + count++; + } + + return count; +} + +/* 2ALT: ancest:(empty)+, head:*empty*, remote:remote = result:remote */ +void test_merge_workdir_trivial__2alt(void) +{ + const git_index_entry *entry; + + cl_git_pass(merge_trivial("trivial-2alt", "trivial-2alt-branch", 0)); + + cl_assert(entry = git_index_get_bypath(repo_index, "new-in-branch.txt", 0)); + cl_assert(git_index_reuc_entrycount(repo_index) == 0); + cl_assert(merge_trivial_conflict_entrycount() == 0); +} + +/* 3ALT: ancest:(empty)+, head:head, remote:*empty* = result:head */ +void test_merge_workdir_trivial__3alt(void) +{ + const git_index_entry *entry; + + cl_git_pass(merge_trivial("trivial-3alt", "trivial-3alt-branch", 0)); + + cl_assert(entry = git_index_get_bypath(repo_index, "new-in-3alt.txt", 0)); + cl_assert(git_index_reuc_entrycount(repo_index) == 0); + cl_assert(merge_trivial_conflict_entrycount() == 0); +} + +/* 4: ancest:(empty)^, head:head, remote:remote = result:no merge */ +void test_merge_workdir_trivial__4(void) +{ + const git_index_entry *entry; + + cl_git_pass(merge_trivial("trivial-4", "trivial-4-branch", 0)); + + cl_assert((entry = git_index_get_bypath(repo_index, "new-and-different.txt", 0)) == NULL); + cl_assert(git_index_reuc_entrycount(repo_index) == 0); + + cl_assert(merge_trivial_conflict_entrycount() == 2); + cl_assert(entry = git_index_get_bypath(repo_index, "new-and-different.txt", 2)); + cl_assert(entry = git_index_get_bypath(repo_index, "new-and-different.txt", 3)); +} + +/* 5ALT: ancest:*, head:head, remote:head = result:head */ +void test_merge_workdir_trivial__5alt_1(void) +{ + const git_index_entry *entry; + + cl_git_pass(merge_trivial("trivial-5alt-1", "trivial-5alt-1-branch", 0)); + + cl_assert(entry = git_index_get_bypath(repo_index, "new-and-same.txt", 0)); + cl_assert(git_index_reuc_entrycount(repo_index) == 0); + cl_assert(merge_trivial_conflict_entrycount() == 0); +} + +/* 5ALT: ancest:*, head:head, remote:head = result:head */ +void test_merge_workdir_trivial__5alt_2(void) +{ + const git_index_entry *entry; + + cl_git_pass(merge_trivial("trivial-5alt-2", "trivial-5alt-2-branch", 0)); + + cl_assert(entry = git_index_get_bypath(repo_index, "modified-to-same.txt", 0)); + cl_assert(git_index_reuc_entrycount(repo_index) == 0); + cl_assert(merge_trivial_conflict_entrycount() == 0); +} + +/* 6: ancest:ancest+, head:(empty), remote:(empty) = result:no merge */ +void test_merge_workdir_trivial__6(void) +{ + const git_index_entry *entry; + + cl_git_pass(merge_trivial("trivial-6", "trivial-6-branch", 0)); + + cl_assert((entry = git_index_get_bypath(repo_index, "removed-in-both.txt", 0)) == NULL); + cl_assert(git_index_reuc_entrycount(repo_index) == 0); + + cl_assert(merge_trivial_conflict_entrycount() == 1); + cl_assert(entry = git_index_get_bypath(repo_index, "removed-in-both.txt", 1)); +} + +/* 6: ancest:ancest+, head:(empty), remote:(empty) = result:no merge */ +void test_merge_workdir_trivial__6_automerge(void) +{ + const git_index_entry *entry; + const git_index_reuc_entry *reuc; + + cl_git_pass(merge_trivial("trivial-6", "trivial-6-branch", 1)); + + cl_assert((entry = git_index_get_bypath(repo_index, "removed-in-both.txt", 0)) == NULL); + cl_assert(git_index_reuc_entrycount(repo_index) == 1); + cl_assert(reuc = git_index_reuc_get_bypath(repo_index, "removed-in-both.txt")); + + cl_assert(merge_trivial_conflict_entrycount() == 0); +} + +/* 8: ancest:ancest^, head:(empty), remote:ancest = result:no merge */ +void test_merge_workdir_trivial__8(void) +{ + const git_index_entry *entry; + + cl_git_pass(merge_trivial("trivial-8", "trivial-8-branch", 0)); + + cl_assert((entry = git_index_get_bypath(repo_index, "removed-in-8.txt", 0)) == NULL); + cl_assert(git_index_reuc_entrycount(repo_index) == 0); + + cl_assert(merge_trivial_conflict_entrycount() == 2); + cl_assert(entry = git_index_get_bypath(repo_index, "removed-in-8.txt", 1)); + cl_assert(entry = git_index_get_bypath(repo_index, "removed-in-8.txt", 3)); +} + +/* 8: ancest:ancest^, head:(empty), remote:ancest = result:no merge */ +void test_merge_workdir_trivial__8_automerge(void) +{ + const git_index_entry *entry; + const git_index_reuc_entry *reuc; + + cl_git_pass(merge_trivial("trivial-8", "trivial-8-branch", 1)); + + cl_assert((entry = git_index_get_bypath(repo_index, "removed-in-8.txt", 0)) == NULL); + + cl_assert(git_index_reuc_entrycount(repo_index) == 1); + cl_assert(reuc = git_index_reuc_get_bypath(repo_index, "removed-in-8.txt")); + + cl_assert(merge_trivial_conflict_entrycount() == 0); +} + +/* 7: ancest:ancest+, head:(empty), remote:remote = result:no merge */ +void test_merge_workdir_trivial__7(void) +{ + const git_index_entry *entry; + + cl_git_pass(merge_trivial("trivial-7", "trivial-7-branch", 0)); + + cl_assert((entry = git_index_get_bypath(repo_index, "removed-in-7.txt", 0)) == NULL); + cl_assert(git_index_reuc_entrycount(repo_index) == 0); + + cl_assert(merge_trivial_conflict_entrycount() == 2); + cl_assert(entry = git_index_get_bypath(repo_index, "removed-in-7.txt", 1)); + cl_assert(entry = git_index_get_bypath(repo_index, "removed-in-7.txt", 3)); +} + +/* 7: ancest:ancest+, head:(empty), remote:remote = result:no merge */ +void test_merge_workdir_trivial__7_automerge(void) +{ + const git_index_entry *entry; + + cl_git_pass(merge_trivial("trivial-7", "trivial-7-branch", 0)); + + cl_assert((entry = git_index_get_bypath(repo_index, "removed-in-7.txt", 0)) == NULL); + cl_assert(git_index_reuc_entrycount(repo_index) == 0); + + cl_assert(merge_trivial_conflict_entrycount() == 2); + cl_assert(entry = git_index_get_bypath(repo_index, "removed-in-7.txt", 1)); + cl_assert(entry = git_index_get_bypath(repo_index, "removed-in-7.txt", 3)); +} + +/* 10: ancest:ancest^, head:ancest, remote:(empty) = result:no merge */ +void test_merge_workdir_trivial__10(void) +{ + const git_index_entry *entry; + + cl_git_pass(merge_trivial("trivial-10", "trivial-10-branch", 0)); + + cl_assert((entry = git_index_get_bypath(repo_index, "removed-in-10-branch.txt", 0)) == NULL); + cl_assert(git_index_reuc_entrycount(repo_index) == 0); + + cl_assert(merge_trivial_conflict_entrycount() == 2); + cl_assert(entry = git_index_get_bypath(repo_index, "removed-in-10-branch.txt", 1)); + cl_assert(entry = git_index_get_bypath(repo_index, "removed-in-10-branch.txt", 2)); +} + +/* 10: ancest:ancest^, head:ancest, remote:(empty) = result:no merge */ +void test_merge_workdir_trivial__10_automerge(void) +{ + const git_index_entry *entry; + const git_index_reuc_entry *reuc; + + cl_git_pass(merge_trivial("trivial-10", "trivial-10-branch", 1)); + + cl_assert((entry = git_index_get_bypath(repo_index, "removed-in-10-branch.txt", 0)) == NULL); + + cl_assert(git_index_reuc_entrycount(repo_index) == 1); + cl_assert(reuc = git_index_reuc_get_bypath(repo_index, "removed-in-10-branch.txt")); + + cl_assert(merge_trivial_conflict_entrycount() == 0); +} + +/* 9: ancest:ancest+, head:head, remote:(empty) = result:no merge */ +void test_merge_workdir_trivial__9(void) +{ + const git_index_entry *entry; + + cl_git_pass(merge_trivial("trivial-9", "trivial-9-branch", 0)); + + cl_assert((entry = git_index_get_bypath(repo_index, "removed-in-9-branch.txt", 0)) == NULL); + cl_assert(git_index_reuc_entrycount(repo_index) == 0); + + cl_assert(merge_trivial_conflict_entrycount() == 2); + cl_assert(entry = git_index_get_bypath(repo_index, "removed-in-9-branch.txt", 1)); + cl_assert(entry = git_index_get_bypath(repo_index, "removed-in-9-branch.txt", 2)); +} + +/* 9: ancest:ancest+, head:head, remote:(empty) = result:no merge */ +void test_merge_workdir_trivial__9_automerge(void) +{ + const git_index_entry *entry; + + cl_git_pass(merge_trivial("trivial-9", "trivial-9-branch", 1)); + + cl_assert((entry = git_index_get_bypath(repo_index, "removed-in-9-branch.txt", 0)) == NULL); + cl_assert(git_index_reuc_entrycount(repo_index) == 0); + + cl_assert(merge_trivial_conflict_entrycount() == 2); + cl_assert(entry = git_index_get_bypath(repo_index, "removed-in-9-branch.txt", 1)); + cl_assert(entry = git_index_get_bypath(repo_index, "removed-in-9-branch.txt", 2)); +} + +/* 13: ancest:ancest+, head:head, remote:ancest = result:head */ +void test_merge_workdir_trivial__13(void) +{ + const git_index_entry *entry; + git_oid expected_oid; + + cl_git_pass(merge_trivial("trivial-13", "trivial-13-branch", 0)); + + cl_assert(entry = git_index_get_bypath(repo_index, "modified-in-13.txt", 0)); + cl_git_pass(git_oid_fromstr(&expected_oid, "1cff9ec6a47a537380dedfdd17c9e76d74259a2b")); + cl_assert(git_oid_cmp(&entry->oid, &expected_oid) == 0); + + cl_assert(git_index_reuc_entrycount(repo_index) == 0); + cl_assert(merge_trivial_conflict_entrycount() == 0); +} + +/* 14: ancest:ancest+, head:ancest, remote:remote = result:remote */ +void test_merge_workdir_trivial__14(void) +{ + const git_index_entry *entry; + git_oid expected_oid; + + cl_git_pass(merge_trivial("trivial-14", "trivial-14-branch", 0)); + + cl_assert(entry = git_index_get_bypath(repo_index, "modified-in-14-branch.txt", 0)); + cl_git_pass(git_oid_fromstr(&expected_oid, "26153a3ff3649b6c2bb652d3f06878c6e0a172f9")); + cl_assert(git_oid_cmp(&entry->oid, &expected_oid) == 0); + + cl_assert(git_index_reuc_entrycount(repo_index) == 0); + cl_assert(merge_trivial_conflict_entrycount() == 0); +} + +/* 11: ancest:ancest+, head:head, remote:remote = result:no merge */ +void test_merge_workdir_trivial__11(void) +{ + const git_index_entry *entry; + + cl_git_pass(merge_trivial("trivial-11", "trivial-11-branch", 0)); + + cl_assert((entry = git_index_get_bypath(repo_index, "modified-in-both.txt", 0)) == NULL); + cl_assert(git_index_reuc_entrycount(repo_index) == 0); + + cl_assert(merge_trivial_conflict_entrycount() == 3); + cl_assert(entry = git_index_get_bypath(repo_index, "modified-in-both.txt", 1)); + cl_assert(entry = git_index_get_bypath(repo_index, "modified-in-both.txt", 2)); + cl_assert(entry = git_index_get_bypath(repo_index, "modified-in-both.txt", 3)); +}