From: Jeff King <peff@peff.net>
To: Philipp Metzler <phil@goli.at>
Cc: Joshua Jensen <jjensen@workspacewhiz.com>,
Junio C Hamano <gitster@pobox.com>,
git@vger.kernel.org
Subject: Re: git commit -a reports untracked files after a clone
Date: Fri, 27 May 2011 14:00:45 -0400 [thread overview]
Message-ID: <20110527180045.GA29119@sigill.intra.peff.net> (raw)
In-Reply-To: <20110516145535.GA25930@sigill.intra.peff.net>
[+cc Joshua, as the problem is in his 5102c61 (Add case insensitivity
support for directories when using git status, 2010-10-03)]
On Mon, May 16, 2011 at 10:55:35AM -0400, Jeff King wrote:
> OK, I'm making some progress. I can replicate on Linux with:
>
> $ git config --global core.ignorecase true
> $ git clone git foo
> $ cd foo && git commit -a
> # On branch private
> # Untracked files:
> # Documentation/
> # block-sha1/
> # builtin/
> # compat/
> # contrib/
> # git-gui/
> # git_remote_helpers/
> # gitk-git/
> # gitweb/
> # perl/
> # po/
> # ppc/
> # t/
> # templates/
> # vcs-svn/
> # xdiff/
> nothing added to commit but untracked files present
>
> [i.e., all of those directories are listed as containing untracked
> contents even though they clearly have tracked files in them]
OK, I figured it out. Unfortunately, the fix is non-trivial. Basically
we are getting bogus data out of the index's name_hash for directories.
What is happening is this:
1. We load the index, and for each entry, insert it into the index's
name_hash. In addition, if ignorecase is turned on, we make an
entry in the name_hash for the directory (e.g., "contrib/"), which
uses the following code from 5102c61's hash_index_entry_directories:
hash = hash_name(ce->name, ptr - ce->name);
if (!lookup_hash(hash, &istate->name_hash)) {
pos = insert_hash(hash, &istate->name_hash);
ce->next = *pos;
*pos = ce;
}
Note that we only add the directory entry if there is not already an
entry.
2. We run add_files_to_cache, which gets updated information for each
cache entry. It helpfully inserts this information into the cache,
which calls replace_index_entry. This in turn calls
remove_name_hash() on the old entry, and add_name_hash() on the new
one. But remove_name_hash doesn't actually remove from the hash, it
only marks it as "no longer interesting" (from cache.h):
/*
* 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.
*/
static inline void remove_name_hash(struct cache_entry *ce)
{
ce->ce_flags |= CE_UNHASHED;
}
This is OK in the specific-file case, since the entries in the hash
form a linked list, and we can just skip the "not here anymore"
entries during lookup.
But for the directory hash entry, we will _not_ write a new entry,
because there is already one there: the old one that is actually no
longer interesting!
3. While traversing the directories, we end up in the
directory_exists_in_index_icase function to see if a directory is
interesting. This in turn checks index_name_exists, which will
look up the directory in the index's name_hash. We see the old,
deleted record, and assume there is nothing interesting. The
directory gets marked as untracked, even though there are index
entries in it.
And that explains the three prerequisites I needed for replicating the
bug:
1. You must be using ignore-case. The bug is in that code path.
2. You must have an index with stale stat information; this is what
provokes add_files_to_cache to update the index entries.
3. You must stop traversal at the directory boundary (i.e., not using
"-uall"). Otherwise we end up looking for the exact filenames,
which do get found.
The problem is in the code I showed above:
hash = hash_name(ce->name, ptr - ce->name);
if (!lookup_hash(hash, &istate->name_hash)) {
pos = insert_hash(hash, &istate->name_hash);
ce->next = *pos;
*pos = ce;
}
Having a single cache entry that represents the directory is not enough;
that entry may go away if the index is changed. It may be tempting to
say that the problem is in our removal method; if we removed the entry
entirely instead of simply marking it as "not here anymore", then we
would know we need to insert a new entry. But that only covers this
particular case of remove-replace. In the more general case, consider
something like this:
1. We add "foo/bar" and "foo/baz" to the index. Each gets their own
entry in name_hash, plus we make a "foo/" entry that points to
"foo/bar".
2. We remove the "foo/bar" entry from the index, and from the
name_hash.
3. We ask if "foo/" exists, and see no entry, even though "foo/baz"
exists.
So we need that directory entry to have the list of _all_ cache entries
that indicate that the directory is tracked. So that implies making a
linked list as we do for other entries, like:
hash = hash_name(ce->name, ptr - ce->name);
pos = insert_hash(hash, &istate->name_hash);
ce->next = *pos;
*pos = ce;
But that's not right either. In fact, it shows a second bug in the
current code, which is that the "ce->next" pointer is supposed to be
linking entries for a specific filename entry, but here we are
overwriting it for the directory entry. I _think_ this can't be
triggered as a bug, because:
1. This is the first entry in the directory (otherwise lookup_hash
would not have returned NULL), and is therefore the first entry
for this specific file. So ce->next must already be NULL.
2. lookup_hash returned NULL, which means "*pos" is going to be NULL.
So even though it looks like we might be truncating an existing list,
it's not possible to do so in practice. But if we start actually keeping
a directory list, we will run into problems, because we'll be splicing
unrelated lists together.
So we need to have a separate next pointer for the list in the directory
bucket, and we need to traverse that list in index_name_exists when we
are looking up a directory.
The patch below seems to fix it for me. I'm not 100% happy with adding
extra icase-only cruft to "struct cache_entry", but I don't really see a
way around it, short of separating out the "next" pointers from
cache_entry entirely (i.e., having a separate "cache_entry_list"
struct that gets stored in the name_hash). In practice, it probably
doesn't matter; we have thousands of cache entries, compared to the
millions of objects (where adding 4 bytes to the struct _does_ impact
performance).
---
diff --git a/cache.h b/cache.h
index 96cfc9a..2868dac 100644
--- a/cache.h
+++ b/cache.h
@@ -153,6 +153,7 @@ struct cache_entry {
unsigned int ce_flags;
unsigned char sha1[20];
struct cache_entry *next;
+ struct cache_entry *dir_next;
char name[FLEX_ARRAY]; /* more */
};
diff --git a/name-hash.c b/name-hash.c
index 1a8c619..30cb2e3 100644
--- a/name-hash.c
+++ b/name-hash.c
@@ -57,11 +57,9 @@ static void hash_index_entry_directories(struct index_state *istate, struct cach
if (*ptr == '/') {
++ptr;
hash = hash_name(ce->name, ptr - ce->name);
- if (!lookup_hash(hash, &istate->name_hash)) {
- pos = insert_hash(hash, &istate->name_hash);
- ce->next = *pos;
- *pos = ce;
- }
+ pos = insert_hash(hash, &istate->name_hash);
+ ce->dir_next = *pos;
+ *pos = ce;
}
}
}
@@ -162,7 +160,10 @@ struct cache_entry *index_name_exists(struct index_state *istate, const char *na
if (same_name(ce, name, namelen, icase))
return ce;
}
- ce = ce->next;
+ if (icase && name[namelen - 1] == '/')
+ ce = ce->dir_next;
+ else
+ ce = ce->next;
}
/*
next prev parent reply other threads:[~2011-05-27 18:00 UTC|newest]
Thread overview: 13+ messages / expand[flat|nested] mbox.gz Atom feed top
2011-05-15 0:46 git commit -a reports untracked files after a clone Philipp Metzler
2011-05-15 3:43 ` Junio C Hamano
2011-05-15 8:26 ` Philipp Metzler
2011-05-16 10:38 ` Jeff King
2011-05-16 10:49 ` Philipp Metzler
2011-05-16 12:08 ` Jeff King
2011-05-16 12:25 ` Philipp Metzler
2011-05-16 12:38 ` Philipp Metzler
2011-05-16 14:55 ` Jeff King
2011-05-27 18:00 ` Jeff King [this message]
2011-05-27 18:13 ` Jeff King
2011-10-05 14:26 ` Joerg Rosenkranz
2011-10-06 16:06 ` Jeff King
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20110527180045.GA29119@sigill.intra.peff.net \
--to=peff@peff.net \
--cc=git@vger.kernel.org \
--cc=gitster@pobox.com \
--cc=jjensen@workspacewhiz.com \
--cc=phil@goli.at \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).