diff --git a/include/git2/diff.h b/include/git2/diff.h index 9fcc3bb08..cac3b268a 100644 --- a/include/git2/diff.h +++ b/include/git2/diff.h @@ -124,6 +124,11 @@ typedef enum { /** Use case insensitive filename comparisons */ GIT_DIFF_IGNORE_CASE = (1u << 10), + /** May be combined with `GIT_DIFF_IGNORE_CASE` to specify that a file + * that has changed case will be returned as an add/delete pair. + */ + GIT_DIFF_INCLUDE_CASECHANGE = (1u << 11), + /** If the pathspec is set in the diff options, this flags means to * apply it as an exact match instead of as an fnmatch pattern. */ @@ -220,7 +225,7 @@ typedef struct git_diff git_diff; typedef enum { GIT_DIFF_FLAG_BINARY = (1u << 0), /**< file(s) treated as binary data */ GIT_DIFF_FLAG_NOT_BINARY = (1u << 1), /**< file(s) treated as text data */ - GIT_DIFF_FLAG_VALID_ID = (1u << 2), /**< `id` value is known correct */ + GIT_DIFF_FLAG_VALID_ID = (1u << 2), /**< `id` value is known correct */ } git_diff_flag_t; /** diff --git a/src/checkout.c b/src/checkout.c index 478130879..a647ce0b9 100644 --- a/src/checkout.c +++ b/src/checkout.c @@ -409,6 +409,14 @@ static bool submodule_is_config_only( return rval; } +static bool checkout_is_empty_dir(checkout_data *data, const char *path) +{ + git_buf_truncate(&data->path, data->workdir_len); + if (git_buf_puts(&data->path, path) < 0) + return false; + return git_path_is_empty_dir(data->path.ptr); +} + static int checkout_action_with_wd( int *action, checkout_data *data, @@ -526,6 +534,7 @@ static int checkout_action_with_wd_dir( checkout_notify(data, GIT_CHECKOUT_NOTIFY_DIRTY, delta, NULL)); GITERR_CHECK_ERROR( checkout_notify(data, GIT_CHECKOUT_NOTIFY_UNTRACKED, NULL, wd)); + *action = CHECKOUT_ACTION_IF(FORCE, REMOVE_AND_UPDATE, NONE); break; case GIT_DELTA_ADDED:/* case 4 (and 7 for dir) */ case GIT_DELTA_MODIFIED: /* case 20 (or 37 but not really) */ @@ -550,8 +559,6 @@ static int checkout_action_with_wd_dir( * dir and it will succeed if no children are left. */ *action = CHECKOUT_ACTION_IF(SAFE, UPDATE_BLOB, NONE); - if (*action != CHECKOUT_ACTION__NONE) - *action |= CHECKOUT_ACTION__DEFER_REMOVE; } else if (delta->new_file.mode != GIT_FILEMODE_TREE) /* For typechange to dir, dir is already created so no action */ @@ -564,6 +571,20 @@ static int checkout_action_with_wd_dir( return checkout_action_common(action, data, delta, wd); } +static int checkout_action_with_wd_dir_empty( + int *action, + checkout_data *data, + const git_diff_delta *delta) +{ + int error = checkout_action_no_wd(action, data, delta); + + /* We can always safely remove an empty directory. */ + if (error == 0 && *action != CHECKOUT_ACTION__NONE) + *action |= CHECKOUT_ACTION__REMOVE; + + return error; +} + static int checkout_action( int *action, checkout_data *data, @@ -653,7 +674,9 @@ static int checkout_action( } } - return checkout_action_with_wd_dir(action, data, delta, workdir, wd); + return checkout_is_empty_dir(data, wd->path) ? + checkout_action_with_wd_dir_empty(action, data, delta) : + checkout_action_with_wd_dir(action, data, delta, workdir, wd); } /* case 6 - wd is after delta */ @@ -2462,7 +2485,8 @@ int git_checkout_iterator( GIT_DIFF_INCLUDE_IGNORED | GIT_DIFF_INCLUDE_TYPECHANGE | GIT_DIFF_INCLUDE_TYPECHANGE_TREES | - GIT_DIFF_SKIP_BINARY_CHECK; + GIT_DIFF_SKIP_BINARY_CHECK | + GIT_DIFF_INCLUDE_CASECHANGE; if (data.opts.checkout_strategy & GIT_CHECKOUT_DISABLE_PATHSPEC_MATCH) diff_opts.flags |= GIT_DIFF_DISABLE_PATHSPEC_MATCH; if (data.opts.paths.count > 0) @@ -2643,7 +2667,7 @@ int git_checkout_tree( if ((error = git_repository_index(&index, repo)) < 0) return error; - if (!(error = git_iterator_for_tree(&tree_i, tree, GIT_ITERATOR_DONT_IGNORE_CASE, NULL, NULL))) + if (!(error = git_iterator_for_tree(&tree_i, tree, 0, NULL, NULL))) error = git_checkout_iterator(tree_i, index, opts); git_iterator_free(tree_i); diff --git a/src/diff.c b/src/diff.c index 08e218cce..f7e1c8ee4 100644 --- a/src/diff.c +++ b/src/diff.c @@ -822,6 +822,19 @@ static int maybe_modified( status = GIT_DELTA_UNMODIFIED; } + /* If we want case changes, then break this into a delete of the old + * and an add of the new so that consumers can act accordingly (eg, + * checkout will update the case on disk.) + */ + if (DIFF_FLAG_IS_SET(diff, GIT_DIFF_IGNORE_CASE) && + DIFF_FLAG_IS_SET(diff, GIT_DIFF_INCLUDE_CASECHANGE) && + strcmp(oitem->path, nitem->path) != 0) { + + if (!(error = diff_delta__from_one(diff, GIT_DELTA_DELETED, oitem))) + error = diff_delta__from_one(diff, GIT_DELTA_ADDED, nitem); + return error; + } + return diff_delta__from_two( diff, status, oitem, omode, nitem, nmode, git_oid_iszero(&noid) ? NULL : &noid, matched_pathspec); diff --git a/tests/checkout/icase.c b/tests/checkout/icase.c index 211738070..510f5424d 100644 --- a/tests/checkout/icase.c +++ b/tests/checkout/icase.c @@ -1,10 +1,13 @@ #include "clar_libgit2.h" #include "git2/checkout.h" +#include "refs.h" #include "path.h" #ifdef GIT_WIN32 # include +#else +# include #endif static git_repository *repo; @@ -14,14 +17,23 @@ static git_checkout_options checkout_opts; void test_checkout_icase__initialize(void) { git_oid id; + git_config *cfg; + int icase = 0; repo = cl_git_sandbox_init("testrepo"); + cl_git_pass(git_repository_config_snapshot(&cfg, repo)); + git_config_get_bool(&icase, cfg, "core.ignorecase"); + git_config_free(cfg); + + if (!icase) + cl_skip(); + cl_git_pass(git_reference_name_to_id(&id, repo, "refs/heads/dir")); cl_git_pass(git_object_lookup(&obj, repo, &id, GIT_OBJ_ANY)); git_checkout_init_options(&checkout_opts, GIT_CHECKOUT_OPTIONS_VERSION); - checkout_opts.checkout_strategy = GIT_CHECKOUT_FORCE; + checkout_opts.checkout_strategy = GIT_CHECKOUT_NONE; } void test_checkout_icase__cleanup(void) @@ -30,7 +42,7 @@ void test_checkout_icase__cleanup(void) cl_git_sandbox_cleanup(); } -static char *test_realpath(const char *in) +static char *get_filename(const char *in) { #ifdef GIT_WIN32 HANDLE fh; @@ -55,7 +67,31 @@ static char *test_realpath(const char *in) return filename; #else - return realpath(in, NULL); + char *search_dirname, *search_filename, *filename = NULL; + git_buf out = GIT_BUF_INIT; + DIR *dir; + struct dirent *de; + + cl_assert(search_dirname = git_path_dirname(in)); + cl_assert(search_filename = git_path_basename(in)); + + cl_assert(dir = opendir(search_dirname)); + + while ((de = readdir(dir))) { + if (strcasecmp(de->d_name, search_filename) == 0) { + git_buf_join(&out, '/', search_dirname, de->d_name); + filename = git_buf_detach(&out); + break; + } + } + + closedir(dir); + + git__free(search_dirname); + git__free(search_filename); + git_buf_free(&out); + + return filename; #endif } @@ -64,7 +100,7 @@ static void assert_name_is(const char *expected) char *actual; size_t actual_len, expected_len, start; - cl_assert(actual = test_realpath(expected)); + cl_assert(actual = get_filename(expected)); expected_len = strlen(expected); actual_len = strlen(actual); @@ -79,8 +115,21 @@ static void assert_name_is(const char *expected) free(actual); } -void test_checkout_icase__overwrites_files_for_files(void) +void test_checkout_icase__refuses_to_overwrite_files_for_files(void) { + checkout_opts.checkout_strategy = GIT_CHECKOUT_SAFE|GIT_CHECKOUT_RECREATE_MISSING; + + cl_git_write2file("testrepo/BRANCH_FILE.txt", "neue file\n", 10, \ + O_WRONLY | O_CREAT | O_TRUNC, 0644); + + cl_git_fail(git_checkout_tree(repo, obj, &checkout_opts)); + assert_name_is("testrepo/BRANCH_FILE.txt"); +} + +void test_checkout_icase__overwrites_files_for_files_when_forced(void) +{ + checkout_opts.checkout_strategy = GIT_CHECKOUT_FORCE; + cl_git_write2file("testrepo/NEW.txt", "neue file\n", 10, \ O_WRONLY | O_CREAT | O_TRUNC, 0644); @@ -88,8 +137,22 @@ void test_checkout_icase__overwrites_files_for_files(void) assert_name_is("testrepo/new.txt"); } -void test_checkout_icase__overwrites_links_for_files(void) +void test_checkout_icase__refuses_to_overwrite_links_for_files(void) { + checkout_opts.checkout_strategy = GIT_CHECKOUT_SAFE|GIT_CHECKOUT_RECREATE_MISSING; + + cl_must_pass(p_symlink("../tmp", "testrepo/BRANCH_FILE.txt")); + + cl_git_fail(git_checkout_tree(repo, obj, &checkout_opts)); + + cl_assert(!git_path_exists("tmp")); + assert_name_is("testrepo/BRANCH_FILE.txt"); +} + +void test_checkout_icase__overwrites_links_for_files_when_forced(void) +{ + checkout_opts.checkout_strategy = GIT_CHECKOUT_FORCE; + cl_must_pass(p_symlink("../tmp", "testrepo/NEW.txt")); cl_git_pass(git_checkout_tree(repo, obj, &checkout_opts)); @@ -98,8 +161,10 @@ void test_checkout_icase__overwrites_links_for_files(void) assert_name_is("testrepo/new.txt"); } -void test_checkout_icase__overwites_folders_for_files(void) +void test_checkout_icase__overwrites_empty_folders_for_files(void) { + checkout_opts.checkout_strategy = GIT_CHECKOUT_SAFE|GIT_CHECKOUT_RECREATE_MISSING; + cl_must_pass(p_mkdir("testrepo/NEW.txt", 0777)); cl_git_pass(git_checkout_tree(repo, obj, &checkout_opts)); @@ -108,8 +173,50 @@ void test_checkout_icase__overwites_folders_for_files(void) cl_assert(!git_path_isdir("testrepo/new.txt")); } -void test_checkout_icase__overwrites_files_for_folders(void) +void test_checkout_icase__refuses_to_overwrite_populated_folders_for_files(void) { + checkout_opts.checkout_strategy = GIT_CHECKOUT_SAFE|GIT_CHECKOUT_RECREATE_MISSING; + + cl_must_pass(p_mkdir("testrepo/BRANCH_FILE.txt", 0777)); + cl_git_write2file("testrepo/BRANCH_FILE.txt/foobar", "neue file\n", 10, \ + O_WRONLY | O_CREAT | O_TRUNC, 0644); + + cl_git_fail(git_checkout_tree(repo, obj, &checkout_opts)); + + assert_name_is("testrepo/BRANCH_FILE.txt"); + cl_assert(git_path_isdir("testrepo/BRANCH_FILE.txt")); +} + +void test_checkout_icase__overwrites_folders_for_files_when_forced(void) +{ + checkout_opts.checkout_strategy = GIT_CHECKOUT_FORCE; + + cl_must_pass(p_mkdir("testrepo/NEW.txt", 0777)); + cl_git_write2file("testrepo/NEW.txt/foobar", "neue file\n", 10, \ + O_WRONLY | O_CREAT | O_TRUNC, 0644); + + cl_git_pass(git_checkout_tree(repo, obj, &checkout_opts)); + + assert_name_is("testrepo/new.txt"); + cl_assert(!git_path_isdir("testrepo/new.txt")); +} + +void test_checkout_icase__refuses_to_overwrite_files_for_folders(void) +{ + checkout_opts.checkout_strategy = GIT_CHECKOUT_SAFE|GIT_CHECKOUT_RECREATE_MISSING; + + cl_git_write2file("testrepo/A", "neue file\n", 10, \ + O_WRONLY | O_CREAT | O_TRUNC, 0644); + + cl_git_fail(git_checkout_tree(repo, obj, &checkout_opts)); + assert_name_is("testrepo/A"); + cl_assert(!git_path_isdir("testrepo/A")); +} + +void test_checkout_icase__overwrites_files_for_folders_when_forced(void) +{ + checkout_opts.checkout_strategy = GIT_CHECKOUT_FORCE; + cl_git_write2file("testrepo/A", "neue file\n", 10, \ O_WRONLY | O_CREAT | O_TRUNC, 0644); @@ -118,8 +225,22 @@ void test_checkout_icase__overwrites_files_for_folders(void) cl_assert(git_path_isdir("testrepo/a")); } -void test_checkout_icase__overwrites_links_for_folders(void) +void test_checkout_icase__refuses_to_overwrite_links_for_folders(void) { + checkout_opts.checkout_strategy = GIT_CHECKOUT_SAFE|GIT_CHECKOUT_RECREATE_MISSING; + + cl_must_pass(p_symlink("..", "testrepo/A")); + + cl_git_fail(git_checkout_tree(repo, obj, &checkout_opts)); + + cl_assert(!git_path_exists("b.txt")); + assert_name_is("testrepo/A"); +} + +void test_checkout_icase__overwrites_links_for_folders_when_forced(void) +{ + checkout_opts.checkout_strategy = GIT_CHECKOUT_FORCE; + cl_must_pass(p_symlink("..", "testrepo/A")); cl_git_pass(git_checkout_tree(repo, obj, &checkout_opts)); @@ -128,3 +249,53 @@ void test_checkout_icase__overwrites_links_for_folders(void) assert_name_is("testrepo/a"); } +void test_checkout_icase__ignores_unstaged_casechange(void) +{ + git_reference *orig_ref, *br2_ref; + git_commit *orig, *br2; + git_checkout_options checkout_opts = GIT_CHECKOUT_OPTIONS_INIT; + + checkout_opts.checkout_strategy = GIT_CHECKOUT_SAFE; + + cl_git_pass(git_reference_lookup_resolved(&orig_ref, repo, "HEAD", 100)); + cl_git_pass(git_commit_lookup(&orig, repo, git_reference_target(orig_ref))); + cl_git_pass(git_reset(repo, (git_object *)orig, GIT_RESET_HARD, NULL)); + + cl_rename("testrepo/branch_file.txt", "testrepo/Branch_File.txt"); + + cl_git_pass(git_reference_lookup_resolved(&br2_ref, repo, "refs/heads/br2", 100)); + cl_git_pass(git_commit_lookup(&br2, repo, git_reference_target(br2_ref))); + + cl_git_pass(git_checkout_tree(repo, (const git_object *)br2, &checkout_opts)); + + git_commit_free(orig); + git_reference_free(orig_ref); +} + +void test_checkout_icase__conflicts_with_casechanged_subtrees(void) +{ + git_reference *orig_ref; + git_object *orig, *subtrees; + git_oid oid; + git_checkout_options checkout_opts = GIT_CHECKOUT_OPTIONS_INIT; + + checkout_opts.checkout_strategy = GIT_CHECKOUT_SAFE; + + cl_git_pass(git_reference_lookup_resolved(&orig_ref, repo, "HEAD", 100)); + cl_git_pass(git_object_lookup(&orig, repo, git_reference_target(orig_ref), GIT_OBJ_COMMIT)); + cl_git_pass(git_reset(repo, (git_object *)orig, GIT_RESET_HARD, NULL)); + + cl_must_pass(p_mkdir("testrepo/AB", 0777)); + cl_must_pass(p_mkdir("testrepo/AB/C", 0777)); + cl_git_write2file("testrepo/AB/C/3.txt", "Foobar!\n", 8, O_RDWR|O_CREAT, 0666); + + cl_git_pass(git_reference_name_to_id(&oid, repo, "refs/heads/subtrees")); + cl_git_pass(git_object_lookup(&subtrees, repo, &oid, GIT_OBJ_ANY)); + + cl_git_fail(git_checkout_tree(repo, subtrees, &checkout_opts)); + + git_object_free(orig); + git_object_free(subtrees); + git_reference_free(orig_ref); +} + diff --git a/tests/checkout/tree.c b/tests/checkout/tree.c index 3973d9320..50541a703 100644 --- a/tests/checkout/tree.c +++ b/tests/checkout/tree.c @@ -646,7 +646,14 @@ void test_checkout_tree__can_cancel_checkout_from_notify(void) cl_git_fail_with(git_checkout_tree(g_repo, obj, &opts), -5555); cl_assert(!git_path_exists("testrepo/new.txt")); - cl_assert_equal_i(4, ca.count); + + /* on case-insensitive FS = a/b.txt, branch_file.txt, new.txt */ + /* on case-sensitive FS = README, then above */ + + if (git_path_exists("testrepo/.git/CoNfIg")) /* case insensitive */ + cl_assert_equal_i(3, ca.count); + else + cl_assert_equal_i(4, ca.count); /* and again with a different stopping point and return code */ ca.filename = "README"; @@ -656,7 +663,11 @@ void test_checkout_tree__can_cancel_checkout_from_notify(void) cl_git_fail_with(git_checkout_tree(g_repo, obj, &opts), 123); cl_assert(!git_path_exists("testrepo/new.txt")); - cl_assert_equal_i(1, ca.count); + + if (git_path_exists("testrepo/.git/CoNfIg")) /* case insensitive */ + cl_assert_equal_i(4, ca.count); + else + cl_assert_equal_i(1, ca.count); git_object_free(obj); } diff --git a/tests/status/renames.c b/tests/status/renames.c index 24b8aca2b..f482d693a 100644 --- a/tests/status/renames.c +++ b/tests/status/renames.c @@ -73,16 +73,20 @@ static void check_status( cl_assert_equal_i_fmt(expected->status, actual->status, "%04x"); - if (oldname) - cl_assert(git__strcmp(oldname, expected->oldname) == 0); - else - cl_assert(expected->oldname == NULL); + if (expected->oldname) { + cl_assert(oldname != NULL); + cl_assert_equal_s(oldname, expected->oldname); + } else { + cl_assert(oldname == NULL); + } if (actual->status & (GIT_STATUS_INDEX_RENAMED|GIT_STATUS_WT_RENAMED)) { - if (newname) - cl_assert(git__strcmp(newname, expected->newname) == 0); - else - cl_assert(expected->newname == NULL); + if (expected->newname) { + cl_assert(newname != NULL); + cl_assert_equal_s(newname, expected->newname); + } else { + cl_assert(newname == NULL); + } } } }