diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f3c3ed6f..6ade3e3b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,11 @@ v0.23 + 1 with which to implement the transactional/atomic semantics for the configuration backend. +* `git_index_add` will now use the case as provided by the caller on + case insensitive systems. Previous versions would keep the case as + it existed in the index. This does not affect the higher-level + `git_index_add_bypath` or `git_index_add_frombuffer` functions. + v0.23 ------ diff --git a/src/index.c b/src/index.c index 20a6c934b..2e8934780 100644 --- a/src/index.c +++ b/src/index.c @@ -977,16 +977,27 @@ static int index_entry_reuc_init(git_index_reuc_entry **reuc_out, return 0; } -static void index_entry_cpy(git_index_entry *tgt, const git_index_entry *src) +static void index_entry_cpy( + git_index_entry *tgt, + git_index *index, + const git_index_entry *src, + bool update_path) { const char *tgt_path = tgt->path; memcpy(tgt, src, sizeof(*tgt)); - tgt->path = tgt_path; /* reset to existing path data */ + + /* keep the existing path buffer, but update the path to the one + * given by the caller, if we trust it. + */ + tgt->path = tgt_path; + + if (index->ignore_case && update_path) + memcpy((char *)tgt->path, src->path, strlen(tgt->path)); } static int index_entry_dup( git_index_entry **out, - git_repository *repo, + git_index *index, const git_index_entry *src) { git_index_entry *entry; @@ -996,10 +1007,10 @@ static int index_entry_dup( return 0; } - if (index_entry_create(&entry, repo, src->path) < 0) + if (index_entry_create(&entry, INDEX_OWNER(index), src->path) < 0) return -1; - index_entry_cpy(entry, src); + index_entry_cpy(entry, index, src, false); *out = entry; return 0; } @@ -1102,6 +1113,74 @@ static int check_file_directory_collision(git_index *index, return 0; } +static int canonicalize_directory_path( + git_index *index, git_index_entry *entry) +{ + const git_index_entry *match, *best = NULL; + char *search, *sep; + size_t pos, search_len, best_len; + + if (!index->ignore_case) + return 0; + + /* item already exists in the index, simply re-use the existing case */ + if ((match = git_index_get_bypath(index, entry->path, 0)) != NULL) { + memcpy((char *)entry->path, match->path, strlen(entry->path)); + return 0; + } + + /* nothing to do */ + if (strchr(entry->path, '/') == NULL) + return 0; + + if ((search = git__strdup(entry->path)) == NULL) + return -1; + + /* starting at the parent directory and descending to the root, find the + * common parent directory. + */ + while (!best && (sep = strrchr(search, '/'))) { + sep[1] = '\0'; + + search_len = strlen(search); + + git_vector_bsearch2( + &pos, &index->entries, index->entries_search_path, search); + + while ((match = git_vector_get(&index->entries, pos))) { + if (GIT_IDXENTRY_STAGE(match) != 0) { + /* conflicts do not contribute to canonical paths */ + } else if (memcmp(search, match->path, search_len) == 0) { + /* prefer an exact match to the input filename */ + best = match; + best_len = search_len; + break; + } else if (strncasecmp(search, match->path, search_len) == 0) { + /* continue walking, there may be a path with an exact + * (case sensitive) match later in the index, but use this + * as the best match until that happens. + */ + if (!best) { + best = match; + best_len = search_len; + } + } else { + break; + } + + pos++; + } + + sep[0] = '\0'; + } + + if (best) + memcpy((char *)entry->path, best->path, best_len); + + git__free(search); + return 0; +} + static int index_no_dups(void **old, void *new) { const git_index_entry *entry = new; @@ -1115,10 +1194,17 @@ static int index_no_dups(void **old, void *new) * it, then it will return an error **and also free the entry**. When * it replaces an existing entry, it will update the entry_ptr with the * actual entry in the index (and free the passed in one). + * trust_path is whether we use the given path, or whether (on case + * insensitive systems only) we try to canonicalize the given path to + * be within an existing directory. * trust_mode is whether we trust the mode in entry_ptr. */ static int index_insert( - git_index *index, git_index_entry **entry_ptr, int replace, bool trust_mode) + git_index *index, + git_index_entry **entry_ptr, + int replace, + bool trust_path, + bool trust_mode) { int error = 0; size_t path_length, position; @@ -1156,8 +1242,14 @@ static int index_insert( entry->mode = index_merge_mode(index, existing, entry->mode); } + /* canonicalize the directory name */ + if (!trust_path) + error = canonicalize_directory_path(index, entry); + /* look for tree / blob name collisions, removing conflicts if requested */ - error = check_file_directory_collision(index, entry, position, replace); + if (!error) + error = check_file_directory_collision(index, entry, position, replace); + if (error < 0) /* skip changes */; @@ -1166,7 +1258,7 @@ static int index_insert( */ else if (existing) { if (replace) - index_entry_cpy(existing, entry); + index_entry_cpy(existing, index, entry, trust_path); index_entry_free(entry); *entry_ptr = entry = existing; } @@ -1246,7 +1338,7 @@ int git_index_add_frombuffer( return -1; } - if (index_entry_dup(&entry, INDEX_OWNER(index), source_entry) < 0) + if (index_entry_dup(&entry, index, source_entry) < 0) return -1; error = git_blob_create_frombuffer(&id, INDEX_OWNER(index), buffer, len); @@ -1258,7 +1350,7 @@ int git_index_add_frombuffer( git_oid_cpy(&entry->id, &id); entry->file_size = len; - if ((error = index_insert(index, &entry, 1, true)) < 0) + if ((error = index_insert(index, &entry, 1, true, true)) < 0) return error; /* Adding implies conflict was resolved, move conflict entries to REUC */ @@ -1317,7 +1409,7 @@ int git_index_add_bypath(git_index *index, const char *path) assert(index && path); if ((ret = index_entry_init(&entry, index, path)) == 0) - ret = index_insert(index, &entry, 1, false); + ret = index_insert(index, &entry, 1, false, false); /* If we were given a directory, let's see if it's a submodule */ if (ret < 0 && ret != GIT_EDIRECTORY) @@ -1343,7 +1435,7 @@ int git_index_add_bypath(git_index *index, const char *path) if ((ret = add_repo_as_submodule(&entry, index, path)) < 0) return ret; - if ((ret = index_insert(index, &entry, 1, false)) < 0) + if ((ret = index_insert(index, &entry, 1, false, false)) < 0) return ret; } else if (ret < 0) { return ret; @@ -1393,8 +1485,8 @@ int git_index_add(git_index *index, const git_index_entry *source_entry) return -1; } - if ((ret = index_entry_dup(&entry, INDEX_OWNER(index), source_entry)) < 0 || - (ret = index_insert(index, &entry, 1, true)) < 0) + if ((ret = index_entry_dup(&entry, index, source_entry)) < 0 || + (ret = index_insert(index, &entry, 1, true, true)) < 0) return ret; git_tree_cache_invalidate_path(index->tree, entry->path); @@ -1543,9 +1635,9 @@ int git_index_conflict_add(git_index *index, assert (index); - if ((ret = index_entry_dup(&entries[0], INDEX_OWNER(index), ancestor_entry)) < 0 || - (ret = index_entry_dup(&entries[1], INDEX_OWNER(index), our_entry)) < 0 || - (ret = index_entry_dup(&entries[2], INDEX_OWNER(index), their_entry)) < 0) + if ((ret = index_entry_dup(&entries[0], index, ancestor_entry)) < 0 || + (ret = index_entry_dup(&entries[1], index, our_entry)) < 0 || + (ret = index_entry_dup(&entries[2], index, their_entry)) < 0) goto on_error; /* Validate entries */ @@ -1579,7 +1671,7 @@ int git_index_conflict_add(git_index *index, /* Make sure stage is correct */ GIT_IDXENTRY_STAGE_SET(entries[i], i + 1); - if ((ret = index_insert(index, &entries[i], 0, true)) < 0) + if ((ret = index_insert(index, &entries[i], 0, true, true)) < 0) goto on_error; entries[i] = NULL; /* don't free if later entry fails */ @@ -2153,7 +2245,7 @@ static size_t read_entry( entry.path = (char *)path_ptr; - if (index_entry_dup(out, INDEX_OWNER(index), &entry) < 0) + if (index_entry_dup(out, index, &entry) < 0) return 0; return entry_size; @@ -2670,7 +2762,7 @@ static int read_tree_cb( entry->mode == old_entry->mode && git_oid_equal(&entry->id, &old_entry->id)) { - index_entry_cpy(entry, old_entry); + index_entry_cpy(entry, data->index, old_entry, false); entry->flags_extended = 0; } @@ -2802,7 +2894,7 @@ int git_index_read_index( if (diff < 0) { git_vector_insert(&remove_entries, (git_index_entry *)old_entry); } else if (diff > 0) { - if ((error = index_entry_dup(&entry, git_index_owner(index), new_entry)) < 0) + if ((error = index_entry_dup(&entry, index, new_entry)) < 0) goto done; git_vector_insert(&new_entries, entry); @@ -2813,7 +2905,7 @@ int git_index_read_index( if (git_oid_equal(&old_entry->id, &new_entry->id)) { git_vector_insert(&new_entries, (git_index_entry *)old_entry); } else { - if ((error = index_entry_dup(&entry, git_index_owner(index), new_entry)) < 0) + if ((error = index_entry_dup(&entry, index, new_entry)) < 0) goto done; git_vector_insert(&new_entries, entry); diff --git a/tests/index/bypath.c b/tests/index/bypath.c index b607e1732..b152b0917 100644 --- a/tests/index/bypath.c +++ b/tests/index/bypath.c @@ -72,3 +72,171 @@ void test_index_bypath__add_hidden(void) cl_assert_equal_i(GIT_FILEMODE_BLOB, entry->mode); #endif } + +void test_index_bypath__add_keeps_existing_case(void) +{ + const git_index_entry *entry; + + if (!cl_repo_get_bool(g_repo, "core.ignorecase")) + clar__skip(); + + cl_git_mkfile("submod2/just_a_dir/file1.txt", "This is a file"); + cl_git_pass(git_index_add_bypath(g_idx, "just_a_dir/file1.txt")); + + cl_assert(entry = git_index_get_bypath(g_idx, "just_a_dir/file1.txt", 0)); + cl_assert_equal_s("just_a_dir/file1.txt", entry->path); + + cl_git_rewritefile("submod2/just_a_dir/file1.txt", "Updated!"); + cl_git_pass(git_index_add_bypath(g_idx, "just_a_dir/FILE1.txt")); + + cl_assert(entry = git_index_get_bypath(g_idx, "just_a_dir/FILE1.txt", 0)); + cl_assert_equal_s("just_a_dir/file1.txt", entry->path); +} + +void test_index_bypath__add_honors_existing_case(void) +{ + const git_index_entry *entry; + + if (!cl_repo_get_bool(g_repo, "core.ignorecase")) + clar__skip(); + + cl_git_mkfile("submod2/just_a_dir/file1.txt", "This is a file"); + cl_git_mkfile("submod2/just_a_dir/file2.txt", "This is another file"); + cl_git_mkfile("submod2/just_a_dir/file3.txt", "This is another file"); + cl_git_mkfile("submod2/just_a_dir/file4.txt", "And another file"); + + cl_git_pass(git_index_add_bypath(g_idx, "just_a_dir/File1.txt")); + cl_git_pass(git_index_add_bypath(g_idx, "JUST_A_DIR/file2.txt")); + cl_git_pass(git_index_add_bypath(g_idx, "Just_A_Dir/FILE3.txt")); + + cl_assert(entry = git_index_get_bypath(g_idx, "just_a_dir/File1.txt", 0)); + cl_assert_equal_s("just_a_dir/File1.txt", entry->path); + + cl_assert(entry = git_index_get_bypath(g_idx, "JUST_A_DIR/file2.txt", 0)); + cl_assert_equal_s("just_a_dir/file2.txt", entry->path); + + cl_assert(entry = git_index_get_bypath(g_idx, "Just_A_Dir/FILE3.txt", 0)); + cl_assert_equal_s("just_a_dir/FILE3.txt", entry->path); + + cl_git_rewritefile("submod2/just_a_dir/file3.txt", "Rewritten"); + cl_git_pass(git_index_add_bypath(g_idx, "Just_A_Dir/file3.txt")); + + cl_assert(entry = git_index_get_bypath(g_idx, "Just_A_Dir/file3.txt", 0)); + cl_assert_equal_s("just_a_dir/FILE3.txt", entry->path); +} + +void test_index_bypath__add_honors_existing_case_2(void) +{ + git_index_entry dummy = { { 0 } }; + const git_index_entry *entry; + + if (!cl_repo_get_bool(g_repo, "core.ignorecase")) + clar__skip(); + + dummy.mode = GIT_FILEMODE_BLOB; + + /* note that `git_index_add` does no checking to canonical directories */ + dummy.path = "Just_a_dir/file0.txt"; + cl_git_pass(git_index_add(g_idx, &dummy)); + + dummy.path = "just_a_dir/fileA.txt"; + cl_git_pass(git_index_add(g_idx, &dummy)); + + dummy.path = "Just_A_Dir/fileB.txt"; + cl_git_pass(git_index_add(g_idx, &dummy)); + + dummy.path = "JUST_A_DIR/fileC.txt"; + cl_git_pass(git_index_add(g_idx, &dummy)); + + dummy.path = "just_A_dir/fileD.txt"; + cl_git_pass(git_index_add(g_idx, &dummy)); + + dummy.path = "JUST_a_DIR/fileE.txt"; + cl_git_pass(git_index_add(g_idx, &dummy)); + + cl_git_mkfile("submod2/just_a_dir/file1.txt", "This is a file"); + cl_git_mkfile("submod2/just_a_dir/file2.txt", "This is another file"); + cl_git_mkfile("submod2/just_a_dir/file3.txt", "This is another file"); + cl_git_mkfile("submod2/just_a_dir/file4.txt", "And another file"); + + cl_git_pass(git_index_add_bypath(g_idx, "just_a_dir/File1.txt")); + cl_git_pass(git_index_add_bypath(g_idx, "JUST_A_DIR/file2.txt")); + cl_git_pass(git_index_add_bypath(g_idx, "Just_A_Dir/FILE3.txt")); + cl_git_pass(git_index_add_bypath(g_idx, "JusT_A_DIR/FILE4.txt")); + + cl_assert(entry = git_index_get_bypath(g_idx, "just_a_dir/File1.txt", 0)); + cl_assert_equal_s("just_a_dir/File1.txt", entry->path); + + cl_assert(entry = git_index_get_bypath(g_idx, "JUST_A_DIR/file2.txt", 0)); + cl_assert_equal_s("JUST_A_DIR/file2.txt", entry->path); + + cl_assert(entry = git_index_get_bypath(g_idx, "Just_A_Dir/FILE3.txt", 0)); + cl_assert_equal_s("Just_A_Dir/FILE3.txt", entry->path); + + cl_git_rewritefile("submod2/just_a_dir/file3.txt", "Rewritten"); + cl_git_pass(git_index_add_bypath(g_idx, "Just_A_Dir/file3.txt")); + + cl_assert(entry = git_index_get_bypath(g_idx, "Just_A_Dir/file3.txt", 0)); + cl_assert_equal_s("Just_A_Dir/FILE3.txt", entry->path); +} + +void test_index_bypath__add_honors_existing_case_3(void) +{ + git_index_entry dummy = { { 0 } }; + const git_index_entry *entry; + + if (!cl_repo_get_bool(g_repo, "core.ignorecase")) + clar__skip(); + + dummy.mode = GIT_FILEMODE_BLOB; + + dummy.path = "just_a_dir/filea.txt"; + cl_git_pass(git_index_add(g_idx, &dummy)); + + dummy.path = "Just_A_Dir/fileB.txt"; + cl_git_pass(git_index_add(g_idx, &dummy)); + + dummy.path = "just_A_DIR/FILEC.txt"; + cl_git_pass(git_index_add(g_idx, &dummy)); + + dummy.path = "Just_a_DIR/FileD.txt"; + cl_git_pass(git_index_add(g_idx, &dummy)); + + cl_git_mkfile("submod2/JuSt_A_DiR/fILEE.txt", "This is a file"); + + cl_git_pass(git_index_add_bypath(g_idx, "just_a_dir/fILEE.txt")); + + cl_assert(entry = git_index_get_bypath(g_idx, "JUST_A_DIR/fILEE.txt", 0)); + cl_assert_equal_s("just_a_dir/fILEE.txt", entry->path); +} + +void test_index_bypath__add_honors_existing_case_4(void) +{ + git_index_entry dummy = { { 0 } }; + const git_index_entry *entry; + + if (!cl_repo_get_bool(g_repo, "core.ignorecase")) + clar__skip(); + + dummy.mode = GIT_FILEMODE_BLOB; + + dummy.path = "just_a_dir/a/b/c/d/e/file1.txt"; + cl_git_pass(git_index_add(g_idx, &dummy)); + + dummy.path = "just_a_dir/a/B/C/D/E/file2.txt"; + cl_git_pass(git_index_add(g_idx, &dummy)); + + cl_must_pass(p_mkdir("submod2/just_a_dir/a", 0777)); + cl_must_pass(p_mkdir("submod2/just_a_dir/a/b", 0777)); + cl_must_pass(p_mkdir("submod2/just_a_dir/a/b/z", 0777)); + cl_must_pass(p_mkdir("submod2/just_a_dir/a/b/z/y", 0777)); + cl_must_pass(p_mkdir("submod2/just_a_dir/a/b/z/y/x", 0777)); + + cl_git_mkfile("submod2/just_a_dir/a/b/z/y/x/FOO.txt", "This is a file"); + + cl_git_pass(git_index_add_bypath(g_idx, "just_a_dir/A/b/Z/y/X/foo.txt")); + + cl_assert(entry = git_index_get_bypath(g_idx, "just_a_dir/A/b/Z/y/X/foo.txt", 0)); + cl_assert_equal_s("just_a_dir/a/b/Z/y/X/foo.txt", entry->path); +} + diff --git a/tests/index/rename.c b/tests/index/rename.c index dd3cfa732..ebaa9b740 100644 --- a/tests/index/rename.c +++ b/tests/index/rename.c @@ -48,3 +48,34 @@ void test_index_rename__single_file(void) cl_fixture_cleanup("rename"); } + +void test_index_rename__casechanging(void) +{ + git_repository *repo; + git_index *index; + const git_index_entry *entry; + git_index_entry new = {{0}}; + + p_mkdir("rename", 0700); + + cl_git_pass(git_repository_init(&repo, "./rename", 0)); + cl_git_pass(git_repository_index(&index, repo)); + + cl_git_mkfile("./rename/lame.name.txt", "new_file\n"); + + cl_git_pass(git_index_add_bypath(index, "lame.name.txt")); + cl_assert_equal_i(1, git_index_entrycount(index)); + cl_assert((entry = git_index_get_bypath(index, "lame.name.txt", 0))); + + memcpy(&new, entry, sizeof(git_index_entry)); + new.path = "LAME.name.TXT"; + + cl_git_pass(git_index_add(index, &new)); + cl_assert((entry = git_index_get_bypath(index, "LAME.name.TXT", 0))); + + if (cl_repo_get_bool(repo, "core.ignorecase")) + cl_assert_equal_i(1, git_index_entrycount(index)); + else + cl_assert_equal_i(2, git_index_entrycount(index)); +} +