mirror of
https://git.proxmox.com/git/libgit2
synced 2025-05-09 07:20:07 +00:00
Merge pull request #3070 from ethomson/checkout_icase
Case insensitive checkout improvements
This commit is contained in:
commit
c3414d53df
@ -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);
|
||||
|
13
src/diff.c
13
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);
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user