зеркало из https://github.com/microsoft/git.git
Merge branch 'kb/name-hash'
The code to keep track of what directory names are known to Git on platforms with case insensitive filesystems can get confused upon a hash collision between these pathnames and looped forever. * kb/name-hash: name-hash.c: fix endless loop with core.ignorecase=true
This commit is contained in:
Коммит
c044bed8f0
17
cache.h
17
cache.h
|
@ -132,7 +132,6 @@ struct cache_entry {
|
||||||
unsigned int ce_namelen;
|
unsigned int ce_namelen;
|
||||||
unsigned char sha1[20];
|
unsigned char sha1[20];
|
||||||
struct cache_entry *next;
|
struct cache_entry *next;
|
||||||
struct cache_entry *dir_next;
|
|
||||||
char name[FLEX_ARRAY]; /* more */
|
char name[FLEX_ARRAY]; /* more */
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -268,25 +267,15 @@ struct index_state {
|
||||||
unsigned name_hash_initialized : 1,
|
unsigned name_hash_initialized : 1,
|
||||||
initialized : 1;
|
initialized : 1;
|
||||||
struct hash_table name_hash;
|
struct hash_table name_hash;
|
||||||
|
struct hash_table dir_hash;
|
||||||
};
|
};
|
||||||
|
|
||||||
extern struct index_state the_index;
|
extern struct index_state the_index;
|
||||||
|
|
||||||
/* Name hashing */
|
/* Name hashing */
|
||||||
extern void add_name_hash(struct index_state *istate, struct cache_entry *ce);
|
extern void add_name_hash(struct index_state *istate, struct cache_entry *ce);
|
||||||
/*
|
extern void remove_name_hash(struct index_state *istate, struct cache_entry *ce);
|
||||||
* We don't actually *remove* it, we can just mark it invalid so that
|
extern void free_name_hash(struct index_state *istate);
|
||||||
* we won't find it in lookups.
|
|
||||||
*
|
|
||||||
* Not only would we have to search the lists (simple enough), but
|
|
||||||
* we'd also have to rehash other hash buckets in case this makes the
|
|
||||||
* hash bucket empty (common). So it's much better to just mark
|
|
||||||
* it.
|
|
||||||
*/
|
|
||||||
static inline void remove_name_hash(struct cache_entry *ce)
|
|
||||||
{
|
|
||||||
ce->ce_flags |= CE_UNHASHED;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#ifndef NO_THE_INDEX_COMPATIBILITY_MACROS
|
#ifndef NO_THE_INDEX_COMPATIBILITY_MACROS
|
||||||
|
|
182
name-hash.c
182
name-hash.c
|
@ -32,38 +32,96 @@ static unsigned int hash_name(const char *name, int namelen)
|
||||||
return hash;
|
return hash;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void hash_index_entry_directories(struct index_state *istate, struct cache_entry *ce)
|
struct dir_entry {
|
||||||
|
struct dir_entry *next;
|
||||||
|
struct dir_entry *parent;
|
||||||
|
struct cache_entry *ce;
|
||||||
|
int nr;
|
||||||
|
unsigned int namelen;
|
||||||
|
};
|
||||||
|
|
||||||
|
static struct dir_entry *find_dir_entry(struct index_state *istate,
|
||||||
|
const char *name, unsigned int namelen)
|
||||||
|
{
|
||||||
|
unsigned int hash = hash_name(name, namelen);
|
||||||
|
struct dir_entry *dir;
|
||||||
|
|
||||||
|
for (dir = lookup_hash(hash, &istate->dir_hash); dir; dir = dir->next)
|
||||||
|
if (dir->namelen == namelen &&
|
||||||
|
!strncasecmp(dir->ce->name, name, namelen))
|
||||||
|
return dir;
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
static struct dir_entry *hash_dir_entry(struct index_state *istate,
|
||||||
|
struct cache_entry *ce, int namelen)
|
||||||
{
|
{
|
||||||
/*
|
/*
|
||||||
* Throw each directory component in the hash for quick lookup
|
* Throw each directory component in the hash for quick lookup
|
||||||
* during a git status. Directory components are stored with their
|
* during a git status. Directory components are stored with their
|
||||||
* closing slash. Despite submodules being a directory, they never
|
* closing slash. Despite submodules being a directory, they never
|
||||||
* reach this point, because they are stored without a closing slash
|
* reach this point, because they are stored without a closing slash
|
||||||
* in the cache.
|
* in index_state.name_hash (as ordinary cache_entries).
|
||||||
*
|
*
|
||||||
* Note that the cache_entry stored with the directory does not
|
* Note that the cache_entry stored with the dir_entry merely
|
||||||
* represent the directory itself. It is a pointer to an existing
|
* supplies the name of the directory (up to dir_entry.namelen). We
|
||||||
* filename, and its only purpose is to represent existence of the
|
* track the number of 'active' files in a directory in dir_entry.nr,
|
||||||
* directory in the cache. It is very possible multiple directory
|
* so we can tell if the directory is still relevant, e.g. for git
|
||||||
* hash entries may point to the same cache_entry.
|
* status. However, if cache_entries are removed, we cannot pinpoint
|
||||||
|
* an exact cache_entry that's still active. It is very possible that
|
||||||
|
* multiple dir_entries point to the same cache_entry.
|
||||||
*/
|
*/
|
||||||
unsigned int hash;
|
struct dir_entry *dir;
|
||||||
void **pos;
|
|
||||||
|
|
||||||
const char *ptr = ce->name;
|
/* get length of parent directory */
|
||||||
while (*ptr) {
|
while (namelen > 0 && !is_dir_sep(ce->name[namelen - 1]))
|
||||||
while (*ptr && *ptr != '/')
|
namelen--;
|
||||||
++ptr;
|
if (namelen <= 0)
|
||||||
if (*ptr == '/') {
|
return NULL;
|
||||||
++ptr;
|
|
||||||
hash = hash_name(ce->name, ptr - ce->name);
|
/* lookup existing entry for that directory */
|
||||||
pos = insert_hash(hash, ce, &istate->name_hash);
|
dir = find_dir_entry(istate, ce->name, namelen);
|
||||||
if (pos) {
|
if (!dir) {
|
||||||
ce->dir_next = *pos;
|
/* not found, create it and add to hash table */
|
||||||
*pos = ce;
|
void **pdir;
|
||||||
}
|
unsigned int hash = hash_name(ce->name, namelen);
|
||||||
|
|
||||||
|
dir = xcalloc(1, sizeof(struct dir_entry));
|
||||||
|
dir->namelen = namelen;
|
||||||
|
dir->ce = ce;
|
||||||
|
|
||||||
|
pdir = insert_hash(hash, dir, &istate->dir_hash);
|
||||||
|
if (pdir) {
|
||||||
|
dir->next = *pdir;
|
||||||
|
*pdir = dir;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* recursively add missing parent directories */
|
||||||
|
dir->parent = hash_dir_entry(istate, ce, namelen - 1);
|
||||||
}
|
}
|
||||||
|
return dir;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void add_dir_entry(struct index_state *istate, struct cache_entry *ce)
|
||||||
|
{
|
||||||
|
/* Add reference to the directory entry (and parents if 0). */
|
||||||
|
struct dir_entry *dir = hash_dir_entry(istate, ce, ce_namelen(ce));
|
||||||
|
while (dir && !(dir->nr++))
|
||||||
|
dir = dir->parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void remove_dir_entry(struct index_state *istate, struct cache_entry *ce)
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
* Release reference to the directory entry (and parents if 0).
|
||||||
|
*
|
||||||
|
* Note: we do not remove / free the entry because there's no
|
||||||
|
* hash.[ch]::remove_hash and dir->next may point to other entries
|
||||||
|
* that are still valid, so we must not free the memory.
|
||||||
|
*/
|
||||||
|
struct dir_entry *dir = hash_dir_entry(istate, ce, ce_namelen(ce));
|
||||||
|
while (dir && dir->nr && !(--dir->nr))
|
||||||
|
dir = dir->parent;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void hash_index_entry(struct index_state *istate, struct cache_entry *ce)
|
static void hash_index_entry(struct index_state *istate, struct cache_entry *ce)
|
||||||
|
@ -74,7 +132,7 @@ static void hash_index_entry(struct index_state *istate, struct cache_entry *ce)
|
||||||
if (ce->ce_flags & CE_HASHED)
|
if (ce->ce_flags & CE_HASHED)
|
||||||
return;
|
return;
|
||||||
ce->ce_flags |= CE_HASHED;
|
ce->ce_flags |= CE_HASHED;
|
||||||
ce->next = ce->dir_next = NULL;
|
ce->next = NULL;
|
||||||
hash = hash_name(ce->name, ce_namelen(ce));
|
hash = hash_name(ce->name, ce_namelen(ce));
|
||||||
pos = insert_hash(hash, ce, &istate->name_hash);
|
pos = insert_hash(hash, ce, &istate->name_hash);
|
||||||
if (pos) {
|
if (pos) {
|
||||||
|
@ -82,8 +140,8 @@ static void hash_index_entry(struct index_state *istate, struct cache_entry *ce)
|
||||||
*pos = ce;
|
*pos = ce;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ignore_case)
|
if (ignore_case && !(ce->ce_flags & CE_UNHASHED))
|
||||||
hash_index_entry_directories(istate, ce);
|
add_dir_entry(istate, ce);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void lazy_init_name_hash(struct index_state *istate)
|
static void lazy_init_name_hash(struct index_state *istate)
|
||||||
|
@ -101,11 +159,33 @@ static void lazy_init_name_hash(struct index_state *istate)
|
||||||
|
|
||||||
void add_name_hash(struct index_state *istate, struct cache_entry *ce)
|
void add_name_hash(struct index_state *istate, struct cache_entry *ce)
|
||||||
{
|
{
|
||||||
|
/* if already hashed, add reference to directory entries */
|
||||||
|
if (ignore_case && (ce->ce_flags & CE_STATE_MASK) == CE_STATE_MASK)
|
||||||
|
add_dir_entry(istate, ce);
|
||||||
|
|
||||||
ce->ce_flags &= ~CE_UNHASHED;
|
ce->ce_flags &= ~CE_UNHASHED;
|
||||||
if (istate->name_hash_initialized)
|
if (istate->name_hash_initialized)
|
||||||
hash_index_entry(istate, ce);
|
hash_index_entry(istate, ce);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* We don't actually *remove* it, we can just mark it invalid so that
|
||||||
|
* we won't find it in lookups.
|
||||||
|
*
|
||||||
|
* Not only would we have to search the lists (simple enough), but
|
||||||
|
* we'd also have to rehash other hash buckets in case this makes the
|
||||||
|
* hash bucket empty (common). So it's much better to just mark
|
||||||
|
* it.
|
||||||
|
*/
|
||||||
|
void remove_name_hash(struct index_state *istate, struct cache_entry *ce)
|
||||||
|
{
|
||||||
|
/* if already hashed, release reference to directory entries */
|
||||||
|
if (ignore_case && (ce->ce_flags & CE_STATE_MASK) == CE_HASHED)
|
||||||
|
remove_dir_entry(istate, ce);
|
||||||
|
|
||||||
|
ce->ce_flags |= CE_UNHASHED;
|
||||||
|
}
|
||||||
|
|
||||||
static int slow_same_name(const char *name1, int len1, const char *name2, int len2)
|
static int slow_same_name(const char *name1, int len1, const char *name2, int len2)
|
||||||
{
|
{
|
||||||
if (len1 != len2)
|
if (len1 != len2)
|
||||||
|
@ -139,18 +219,7 @@ static int same_name(const struct cache_entry *ce, const char *name, int namelen
|
||||||
if (!icase)
|
if (!icase)
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
/*
|
return slow_same_name(name, namelen, ce->name, len);
|
||||||
* If the entry we're comparing is a filename (no trailing slash), then compare
|
|
||||||
* the lengths exactly.
|
|
||||||
*/
|
|
||||||
if (name[namelen - 1] != '/')
|
|
||||||
return slow_same_name(name, namelen, ce->name, len);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* For a directory, we point to an arbitrary cache_entry filename. Just
|
|
||||||
* make sure the directory portion matches.
|
|
||||||
*/
|
|
||||||
return slow_same_name(name, namelen, ce->name, namelen < len ? namelen : len);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct cache_entry *index_name_exists(struct index_state *istate, const char *name, int namelen, int icase)
|
struct cache_entry *index_name_exists(struct index_state *istate, const char *name, int namelen, int icase)
|
||||||
|
@ -166,27 +235,54 @@ struct cache_entry *index_name_exists(struct index_state *istate, const char *na
|
||||||
if (same_name(ce, name, namelen, icase))
|
if (same_name(ce, name, namelen, icase))
|
||||||
return ce;
|
return ce;
|
||||||
}
|
}
|
||||||
if (icase && name[namelen - 1] == '/')
|
ce = ce->next;
|
||||||
ce = ce->dir_next;
|
|
||||||
else
|
|
||||||
ce = ce->next;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Might be a submodule. Despite submodules being directories,
|
* When looking for a directory (trailing '/'), it might be a
|
||||||
|
* submodule or a directory. Despite submodules being directories,
|
||||||
* they are stored in the name hash without a closing slash.
|
* they are stored in the name hash without a closing slash.
|
||||||
* When ignore_case is 1, directories are stored in the name hash
|
* When ignore_case is 1, directories are stored in a separate hash
|
||||||
* with their closing slash.
|
* table *with* their closing slash.
|
||||||
*
|
*
|
||||||
* The side effect of this storage technique is we have need to
|
* The side effect of this storage technique is we have need to
|
||||||
|
* lookup the directory in a separate hash table, and if not found
|
||||||
* remove the slash from name and perform the lookup again without
|
* remove the slash from name and perform the lookup again without
|
||||||
* the slash. If a match is made, S_ISGITLINK(ce->mode) will be
|
* the slash. If a match is made, S_ISGITLINK(ce->mode) will be
|
||||||
* true.
|
* true.
|
||||||
*/
|
*/
|
||||||
if (icase && name[namelen - 1] == '/') {
|
if (icase && name[namelen - 1] == '/') {
|
||||||
|
struct dir_entry *dir = find_dir_entry(istate, name, namelen);
|
||||||
|
if (dir && dir->nr)
|
||||||
|
return dir->ce;
|
||||||
|
|
||||||
ce = index_name_exists(istate, name, namelen - 1, icase);
|
ce = index_name_exists(istate, name, namelen - 1, icase);
|
||||||
if (ce && S_ISGITLINK(ce->ce_mode))
|
if (ce && S_ISGITLINK(ce->ce_mode))
|
||||||
return ce;
|
return ce;
|
||||||
}
|
}
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static int free_dir_entry(void *entry, void *unused)
|
||||||
|
{
|
||||||
|
struct dir_entry *dir = entry;
|
||||||
|
while (dir) {
|
||||||
|
struct dir_entry *next = dir->next;
|
||||||
|
free(dir);
|
||||||
|
dir = next;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void free_name_hash(struct index_state *istate)
|
||||||
|
{
|
||||||
|
if (!istate->name_hash_initialized)
|
||||||
|
return;
|
||||||
|
istate->name_hash_initialized = 0;
|
||||||
|
if (ignore_case)
|
||||||
|
/* free directory entries */
|
||||||
|
for_each_hash(&istate->dir_hash, free_dir_entry, NULL);
|
||||||
|
|
||||||
|
free_hash(&istate->name_hash);
|
||||||
|
free_hash(&istate->dir_hash);
|
||||||
|
}
|
||||||
|
|
|
@ -46,7 +46,7 @@ static void replace_index_entry(struct index_state *istate, int nr, struct cache
|
||||||
{
|
{
|
||||||
struct cache_entry *old = istate->cache[nr];
|
struct cache_entry *old = istate->cache[nr];
|
||||||
|
|
||||||
remove_name_hash(old);
|
remove_name_hash(istate, old);
|
||||||
set_index_entry(istate, nr, ce);
|
set_index_entry(istate, nr, ce);
|
||||||
istate->cache_changed = 1;
|
istate->cache_changed = 1;
|
||||||
}
|
}
|
||||||
|
@ -460,7 +460,7 @@ int remove_index_entry_at(struct index_state *istate, int pos)
|
||||||
struct cache_entry *ce = istate->cache[pos];
|
struct cache_entry *ce = istate->cache[pos];
|
||||||
|
|
||||||
record_resolve_undo(istate, ce);
|
record_resolve_undo(istate, ce);
|
||||||
remove_name_hash(ce);
|
remove_name_hash(istate, ce);
|
||||||
istate->cache_changed = 1;
|
istate->cache_changed = 1;
|
||||||
istate->cache_nr--;
|
istate->cache_nr--;
|
||||||
if (pos >= istate->cache_nr)
|
if (pos >= istate->cache_nr)
|
||||||
|
@ -483,7 +483,7 @@ void remove_marked_cache_entries(struct index_state *istate)
|
||||||
|
|
||||||
for (i = j = 0; i < istate->cache_nr; i++) {
|
for (i = j = 0; i < istate->cache_nr; i++) {
|
||||||
if (ce_array[i]->ce_flags & CE_REMOVE)
|
if (ce_array[i]->ce_flags & CE_REMOVE)
|
||||||
remove_name_hash(ce_array[i]);
|
remove_name_hash(istate, ce_array[i]);
|
||||||
else
|
else
|
||||||
ce_array[j++] = ce_array[i];
|
ce_array[j++] = ce_array[i];
|
||||||
}
|
}
|
||||||
|
@ -1515,8 +1515,7 @@ int discard_index(struct index_state *istate)
|
||||||
istate->cache_changed = 0;
|
istate->cache_changed = 0;
|
||||||
istate->timestamp.sec = 0;
|
istate->timestamp.sec = 0;
|
||||||
istate->timestamp.nsec = 0;
|
istate->timestamp.nsec = 0;
|
||||||
istate->name_hash_initialized = 0;
|
free_name_hash(istate);
|
||||||
free_hash(&istate->name_hash);
|
|
||||||
cache_tree_free(&(istate->cache_tree));
|
cache_tree_free(&(istate->cache_tree));
|
||||||
istate->initialized = 0;
|
istate->initialized = 0;
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
test_description='git-status with core.ignorecase=true'
|
||||||
|
|
||||||
|
. ./test-lib.sh
|
||||||
|
|
||||||
|
test_expect_success 'status with hash collisions' '
|
||||||
|
# note: "V/", "V/XQANY/" and "WURZAUP/" produce the same hash code
|
||||||
|
# in name-hash.c::hash_name
|
||||||
|
mkdir V &&
|
||||||
|
mkdir V/XQANY &&
|
||||||
|
mkdir WURZAUP &&
|
||||||
|
touch V/XQANY/test &&
|
||||||
|
git config core.ignorecase true &&
|
||||||
|
git add . &&
|
||||||
|
# test is successful if git status completes (no endless loop)
|
||||||
|
git status
|
||||||
|
'
|
||||||
|
|
||||||
|
test_done
|
Загрузка…
Ссылка в новой задаче