Commit c3414d53 by Carlos Martín Nieto

Merge pull request #3070 from ethomson/checkout_icase

Case insensitive checkout improvements
parents cfc2e56d cd79d99a
......@@ -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;
/**
......
......@@ -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);
......
......@@ -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);
......
#include "clar_libgit2.h"
#include "git2/checkout.h"
#include "refs.h"
#include "path.h"
#ifdef GIT_WIN32
# include <windows.h>
#else
# include <dirent.h>
#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);
}
......@@ -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);
}
......
......@@ -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);
}
}
}
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment