#include "clar_libgit2.h" #include "git2/merge.h" #include "buffer.h" #include "merge.h" #include "../merge_helpers.h" #include "posix.h" #define TEST_REPO_PATH "merge-resolve" #define MERGE_BRANCH_OID "7cb63eed597130ba4abb87b3e544b85021905520" #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 CHANGED_IN_BRANCH_FILE \ "changed in branch\n" static git_repository *repo; static git_index *repo_index; static char *unaffected[][4] = { { "added-in-master.txt", NULL }, { "changed-in-master.txt", NULL }, { "unchanged.txt", NULL }, { "added-in-master.txt", "changed-in-master.txt", NULL }, { "added-in-master.txt", "unchanged.txt", NULL }, { "changed-in-master.txt", "unchanged.txt", NULL }, { "added-in-master.txt", "changed-in-master.txt", "unchanged.txt", NULL }, { "new_file.txt", NULL }, { "new_file.txt", "unchanged.txt", NULL }, { NULL }, }; static char *affected[][5] = { { "automergeable.txt", NULL }, { "changed-in-branch.txt", NULL }, { "conflicting.txt", NULL }, { "removed-in-branch.txt", NULL }, { "automergeable.txt", "changed-in-branch.txt", NULL }, { "automergeable.txt", "conflicting.txt", NULL }, { "automergeable.txt", "removed-in-branch.txt", NULL }, { "changed-in-branch.txt", "conflicting.txt", NULL }, { "changed-in-branch.txt", "removed-in-branch.txt", NULL }, { "conflicting.txt", "removed-in-branch.txt", NULL }, { "automergeable.txt", "changed-in-branch.txt", "conflicting.txt", NULL }, { "automergeable.txt", "changed-in-branch.txt", "removed-in-branch.txt", NULL }, { "automergeable.txt", "conflicting.txt", "removed-in-branch.txt", NULL }, { "changed-in-branch.txt", "conflicting.txt", "removed-in-branch.txt", NULL }, { "automergeable.txt", "changed-in-branch.txt", "conflicting.txt", "removed-in-branch.txt", NULL }, { NULL }, }; static char *result_contents[4][6] = { { "automergeable.txt", AUTOMERGEABLE_MERGED_FILE, NULL, NULL }, { "changed-in-branch.txt", CHANGED_IN_BRANCH_FILE, NULL, NULL }, { "automergeable.txt", AUTOMERGEABLE_MERGED_FILE, "changed-in-branch.txt", CHANGED_IN_BRANCH_FILE, NULL, NULL }, { NULL } }; void test_merge_workdir_dirty__initialize(void) { repo = cl_git_sandbox_init(TEST_REPO_PATH); git_repository_index(&repo_index, repo); } void test_merge_workdir_dirty__cleanup(void) { git_index_free(repo_index); cl_git_sandbox_cleanup(); } 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); } static int merge_branch(void) { git_oid their_oids[1]; git_annotated_commit *their_head; git_merge_options merge_opts = GIT_MERGE_OPTIONS_INIT; git_checkout_options checkout_opts = GIT_CHECKOUT_OPTIONS_INIT; int error; cl_git_pass(git_oid_fromstr(&their_oids[0], MERGE_BRANCH_OID)); cl_git_pass(git_annotated_commit_lookup(&their_head, repo, &their_oids[0])); checkout_opts.checkout_strategy = GIT_CHECKOUT_SAFE; error = git_merge(repo, (const git_annotated_commit **)&their_head, 1, &merge_opts, &checkout_opts); git_annotated_commit_free(their_head); return error; } static void write_files(char *files[]) { char *filename; git_buf path = GIT_BUF_INIT, content = GIT_BUF_INIT; size_t i; for (i = 0, filename = files[i]; filename; filename = files[++i]) { git_buf_clear(&path); git_buf_clear(&content); git_buf_printf(&path, "%s/%s", TEST_REPO_PATH, filename); git_buf_printf(&content, "This is a dirty file in the working directory!\n\n" "It will not be staged! Its filename is %s.\n", filename); cl_git_mkfile(path.ptr, content.ptr); } git_buf_free(&path); git_buf_free(&content); } static void hack_index(char *files[]) { char *filename; struct stat statbuf; git_buf path = GIT_BUF_INIT; git_index_entry *entry; size_t i; /* Update the index to suggest that checkout placed these files on * disk, keeping the object id but updating the cache, which will * emulate a Git implementation's different filter. */ for (i = 0, filename = files[i]; filename; filename = files[++i]) { git_buf_clear(&path); cl_assert(entry = (git_index_entry *) git_index_get_bypath(repo_index, filename, 0)); cl_git_pass(git_buf_printf(&path, "%s/%s", TEST_REPO_PATH, filename)); cl_git_pass(p_stat(path.ptr, &statbuf)); entry->ctime.seconds = (git_time_t)statbuf.st_ctime; entry->ctime.nanoseconds = 0; entry->mtime.seconds = (git_time_t)statbuf.st_mtime; entry->mtime.nanoseconds = 0; entry->dev = statbuf.st_dev; entry->ino = statbuf.st_ino; entry->uid = statbuf.st_uid; entry->gid = statbuf.st_gid; entry->file_size = statbuf.st_size; } git_buf_free(&path); } static void stage_random_files(char *files[]) { char *filename; size_t i; write_files(files); for (i = 0, filename = files[i]; filename; filename = files[++i]) cl_git_pass(git_index_add_bypath(repo_index, filename)); } static void stage_content(char *content[]) { git_reference *head; git_object *head_object; git_buf path = GIT_BUF_INIT; char *filename, *text; size_t i; cl_git_pass(git_repository_head(&head, repo)); cl_git_pass(git_reference_peel(&head_object, head, GIT_OBJ_COMMIT)); cl_git_pass(git_reset(repo, head_object, GIT_RESET_HARD, NULL)); for (i = 0, filename = content[i], text = content[++i]; filename && text; filename = content[++i], text = content[++i]) { git_buf_clear(&path); cl_git_pass(git_buf_printf(&path, "%s/%s", TEST_REPO_PATH, filename)); cl_git_mkfile(path.ptr, text); cl_git_pass(git_index_add_bypath(repo_index, filename)); } git_object_free(head_object); git_reference_free(head); git_buf_free(&path); } static int merge_dirty_files(char *dirty_files[]) { git_reference *head; git_object *head_object; int error; cl_git_pass(git_repository_head(&head, repo)); cl_git_pass(git_reference_peel(&head_object, head, GIT_OBJ_COMMIT)); cl_git_pass(git_reset(repo, head_object, GIT_RESET_HARD, NULL)); write_files(dirty_files); error = merge_branch(); git_object_free(head_object); git_reference_free(head); return error; } static int merge_differently_filtered_files(char *files[]) { git_reference *head; git_object *head_object; int error; cl_git_pass(git_repository_head(&head, repo)); cl_git_pass(git_reference_peel(&head_object, head, GIT_OBJ_COMMIT)); cl_git_pass(git_reset(repo, head_object, GIT_RESET_HARD, NULL)); write_files(files); hack_index(files); cl_git_pass(git_index_write(repo_index)); error = merge_branch(); git_object_free(head_object); git_reference_free(head); return error; } static int merge_staged_files(char *staged_files[]) { stage_random_files(staged_files); return merge_branch(); } void test_merge_workdir_dirty__unaffected_dirty_files_allowed(void) { char **files; size_t i; for (i = 0, files = unaffected[i]; files[0]; files = unaffected[++i]) cl_git_pass(merge_dirty_files(files)); } void test_merge_workdir_dirty__unstaged_deletes_maintained(void) { git_reference *head; git_object *head_object; cl_git_pass(git_repository_head(&head, repo)); cl_git_pass(git_reference_peel(&head_object, head, GIT_OBJ_COMMIT)); cl_git_pass(git_reset(repo, head_object, GIT_RESET_HARD, NULL)); cl_git_pass(p_unlink("merge-resolve/unchanged.txt")); cl_git_pass(merge_branch()); git_object_free(head_object); git_reference_free(head); } void test_merge_workdir_dirty__affected_dirty_files_disallowed(void) { char **files; size_t i; for (i = 0, files = affected[i]; files[0]; files = affected[++i]) cl_git_fail(merge_dirty_files(files)); } void test_merge_workdir_dirty__staged_files_in_index_disallowed(void) { char **files; size_t i; for (i = 0, files = unaffected[i]; files[0]; files = unaffected[++i]) cl_git_fail(merge_staged_files(files)); for (i = 0, files = affected[i]; files[0]; files = affected[++i]) cl_git_fail(merge_staged_files(files)); } void test_merge_workdir_dirty__identical_staged_files_allowed(void) { char **content; size_t i; set_core_autocrlf_to(repo, false); for (i = 0, content = result_contents[i]; content[0]; content = result_contents[++i]) { stage_content(content); git_index_write(repo_index); cl_git_pass(merge_branch()); } } void test_merge_workdir_dirty__honors_cache(void) { char **files; size_t i; for (i = 0, files = affected[i]; files[0]; files = affected[++i]) cl_git_pass(merge_differently_filtered_files(files)); }