#include "clar_libgit2.h" #include "git2/merge.h" #include "buffer.h" #include "merge.h" #include "index.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; struct p_timeval times[2]; time_t now; 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. * * We set the file's timestamp to before now to pretend that * it was an old checkout so we don't trigger the racy * protections would would check the content. */ now = time(NULL); times[0].tv_sec = now - 5; times[0].tv_usec = 0; times[1].tv_sec = now - 5; times[1].tv_usec = 0; 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_utimes(path.ptr, times)); cl_git_pass(p_stat(path.ptr, &statbuf)); entry->ctime.seconds = (int32_t)statbuf.st_ctime; entry->mtime.seconds = (int32_t)statbuf.st_mtime; #if defined(GIT_USE_NSEC) entry->ctime.nanoseconds = statbuf.st_ctime_nsec; entry->mtime.nanoseconds = statbuf.st_mtime_nsec; #else entry->ctime.nanoseconds = 0; entry->mtime.nanoseconds = 0; #endif entry->dev = statbuf.st_dev; entry->ino = statbuf.st_ino; entry->uid = statbuf.st_uid; entry->gid = statbuf.st_gid; entry->file_size = (uint32_t)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)); /* Emulate checkout with a broken or misconfigured filter: modify some * files on-disk and then update the index with the updated file size * and time, as if some filter applied them. These files should not be * treated as dirty since we created them. * * (Make sure to update the index stamp to defeat racy-git protections * trying to sanity check the files in the index; those would rehash the * files, showing them as dirty, the exact mechanism we're trying to avoid.) */ 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)); }