summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCarlos Martín Nieto <cmn@dwim.me>2015-05-04 14:59:08 +0200
committerCarlos Martín Nieto <cmn@dwim.me>2015-05-04 14:59:08 +0200
commitc3414d53df009bd684c62c8a84ff4fff0d252efd (patch)
treeb7a4b7a35524feb6b4977aaab67556441926d7d5
parentcfc2e56d59db3d0d2b58aeffc4ef4640b7f846b5 (diff)
parentcd79d99a4cd81b37f681c6f4eb6756cd8d42618c (diff)
downloadlibgit2-c3414d53df009bd684c62c8a84ff4fff0d252efd.tar.gz
Merge pull request #3070 from ethomson/checkout_icase
Case insensitive checkout improvements
-rw-r--r--include/git2/diff.h7
-rw-r--r--src/checkout.c34
-rw-r--r--src/diff.c13
-rw-r--r--tests/checkout/icase.c189
-rw-r--r--tests/checkout/tree.c15
-rw-r--r--tests/status/renames.c20
6 files changed, 253 insertions, 25 deletions
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 <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);
+}
+
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);
+ }
}
}
}