diff options
Diffstat (limited to 'tests/libgit2/merge/workdir/dirty.c')
-rw-r--r-- | tests/libgit2/merge/workdir/dirty.c | 351 |
1 files changed, 351 insertions, 0 deletions
diff --git a/tests/libgit2/merge/workdir/dirty.c b/tests/libgit2/merge/workdir/dirty.c new file mode 100644 index 000000000..b9c2ad033 --- /dev/null +++ b/tests/libgit2/merge/workdir/dirty.c @@ -0,0 +1,351 @@ +#include "clar_libgit2.h" +#include "git2/merge.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_str path = GIT_STR_INIT, content = GIT_STR_INIT; + size_t i; + + for (i = 0, filename = files[i]; filename; filename = files[++i]) { + git_str_clear(&path); + git_str_clear(&content); + + git_str_printf(&path, "%s/%s", TEST_REPO_PATH, filename); + git_str_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_str_dispose(&path); + git_str_dispose(&content); +} + +static void hack_index(char *files[]) +{ + char *filename; + struct stat statbuf; + git_str path = GIT_STR_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_str_clear(&path); + + cl_assert(entry = (git_index_entry *) + git_index_get_bypath(repo_index, filename, 0)); + + cl_git_pass(git_str_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_str_dispose(&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_str path = GIT_STR_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_OBJECT_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_str_clear(&path); + + cl_git_pass(git_str_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_str_dispose(&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_OBJECT_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_OBJECT_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_OBJECT_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); + + cl_git_pass(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)); +} |