Merge branch 'dt/untracked-subdir' into maint

The experimental untracked-cache feature were buggy when paths with
a few levels of subdirectories are involved.

* dt/untracked-subdir:
  untracked cache: fix entry invalidation
  untracked-cache: fix subdirectory handling
  t7063: use --force-untracked-cache to speed up a bit
  untracked-cache: support sparse checkout
This commit is contained in:
Junio C Hamano 2015-09-17 12:12:29 -07:00
commit 8833ccd7d0
2 changed files with 282 additions and 32 deletions

99
dir.c
View File

@ -1078,10 +1078,9 @@ static void prep_exclude(struct dir_struct *dir, const char *base, int baselen)
(!untracked || !untracked->valid ||
/*
* .. and .gitignore does not exist before
* (i.e. null exclude_sha1 and skip_worktree is
* not set). Then we can skip loading .gitignore,
* which would result in ENOENT anyway.
* skip_worktree is taken care in read_directory()
* (i.e. null exclude_sha1). Then we can skip
* loading .gitignore, which would result in
* ENOENT anyway.
*/
!is_null_sha1(untracked->exclude_sha1))) {
/*
@ -1298,7 +1297,7 @@ static enum exist_status directory_exists_in_index(const char *dirname, int len)
*/
static enum path_treatment treat_directory(struct dir_struct *dir,
struct untracked_cache_dir *untracked,
const char *dirname, int len, int exclude,
const char *dirname, int len, int baselen, int exclude,
const struct path_simplify *simplify)
{
/* The "len-1" is to strip the final '/' */
@ -1325,7 +1324,8 @@ static enum path_treatment treat_directory(struct dir_struct *dir,
if (!(dir->flags & DIR_HIDE_EMPTY_DIRECTORIES))
return exclude ? path_excluded : path_untracked;
untracked = lookup_untracked(dir->untracked, untracked, dirname, len);
untracked = lookup_untracked(dir->untracked, untracked,
dirname + baselen, len - baselen);
return read_directory_recursive(dir, dirname, len,
untracked, 1, simplify);
}
@ -1445,6 +1445,7 @@ static int get_dtype(struct dirent *de, const char *path, int len)
static enum path_treatment treat_one_path(struct dir_struct *dir,
struct untracked_cache_dir *untracked,
struct strbuf *path,
int baselen,
const struct path_simplify *simplify,
int dtype, struct dirent *de)
{
@ -1496,8 +1497,8 @@ static enum path_treatment treat_one_path(struct dir_struct *dir,
return path_none;
case DT_DIR:
strbuf_addch(path, '/');
return treat_directory(dir, untracked, path->buf, path->len, exclude,
simplify);
return treat_directory(dir, untracked, path->buf, path->len,
baselen, exclude, simplify);
case DT_REG:
case DT_LNK:
return exclude ? path_excluded : path_untracked;
@ -1558,7 +1559,7 @@ static enum path_treatment treat_path(struct dir_struct *dir,
return path_none;
dtype = DTYPE(de);
return treat_one_path(dir, untracked, path, simplify, dtype, de);
return treat_one_path(dir, untracked, path, baselen, simplify, dtype, de);
}
static void add_untracked(struct untracked_cache_dir *dir, const char *name)
@ -1828,7 +1829,7 @@ static int treat_leading_path(struct dir_struct *dir,
break;
if (simplify_away(sb.buf, sb.len, simplify))
break;
if (treat_one_path(dir, NULL, &sb, simplify,
if (treat_one_path(dir, NULL, &sb, baselen, simplify,
DT_DIR, NULL) == path_none)
break; /* do not recurse into it */
if (len <= baselen) {
@ -1880,7 +1881,6 @@ static struct untracked_cache_dir *validate_untracked_cache(struct dir_struct *d
const struct pathspec *pathspec)
{
struct untracked_cache_dir *root;
int i;
if (!dir->untracked || getenv("GIT_DISABLE_UNTRACKED_CACHE"))
return NULL;
@ -1932,15 +1932,6 @@ static struct untracked_cache_dir *validate_untracked_cache(struct dir_struct *d
if (dir->exclude_list_group[EXC_CMDL].nr)
return NULL;
/*
* An optimization in prep_exclude() does not play well with
* CE_SKIP_WORKTREE. It's a rare case anyway, if a single
* entry has that bit set, disable the whole untracked cache.
*/
for (i = 0; i < active_nr; i++)
if (ce_skip_worktree(active_cache[i]))
return NULL;
if (!ident_in_untracked(dir->untracked)) {
warning(_("Untracked cache is disabled on this system."));
return NULL;
@ -2625,23 +2616,67 @@ done2:
return uc;
}
static void invalidate_one_directory(struct untracked_cache *uc,
struct untracked_cache_dir *ucd)
{
uc->dir_invalidated++;
ucd->valid = 0;
ucd->untracked_nr = 0;
}
/*
* Normally when an entry is added or removed from a directory,
* invalidating that directory is enough. No need to touch its
* ancestors. When a directory is shown as "foo/bar/" in git-status
* however, deleting or adding an entry may have cascading effect.
*
* Say the "foo/bar/file" has become untracked, we need to tell the
* untracked_cache_dir of "foo" that "bar/" is not an untracked
* directory any more (because "bar" is managed by foo as an untracked
* "file").
*
* Similarly, if "foo/bar/file" moves from untracked to tracked and it
* was the last untracked entry in the entire "foo", we should show
* "foo/" instead. Which means we have to invalidate past "bar" up to
* "foo".
*
* This function traverses all directories from root to leaf. If there
* is a chance of one of the above cases happening, we invalidate back
* to root. Otherwise we just invalidate the leaf. There may be a more
* sophisticated way than checking for SHOW_OTHER_DIRECTORIES to
* detect these cases and avoid unnecessary invalidation, for example,
* checking for the untracked entry named "bar/" in "foo", but for now
* stick to something safe and simple.
*/
static int invalidate_one_component(struct untracked_cache *uc,
struct untracked_cache_dir *dir,
const char *path, int len)
{
const char *rest = strchr(path, '/');
if (rest) {
int component_len = rest - path;
struct untracked_cache_dir *d =
lookup_untracked(uc, dir, path, component_len);
int ret =
invalidate_one_component(uc, d, rest + 1,
len - (component_len + 1));
if (ret)
invalidate_one_directory(uc, dir);
return ret;
}
invalidate_one_directory(uc, dir);
return uc->dir_flags & DIR_SHOW_OTHER_DIRECTORIES;
}
void untracked_cache_invalidate_path(struct index_state *istate,
const char *path)
{
const char *sep;
struct untracked_cache_dir *d;
if (!istate->untracked || !istate->untracked->root)
return;
sep = strrchr(path, '/');
if (sep)
d = lookup_untracked(istate->untracked,
istate->untracked->root,
path, sep - path);
else
d = istate->untracked->root;
istate->untracked->dir_invalidated++;
d->valid = 0;
d->untracked_nr = 0;
invalidate_one_component(istate->untracked, istate->untracked->root,
path, strlen(path));
}
void untracked_cache_remove_from_index(struct index_state *istate,

View File

@ -354,4 +354,219 @@ EOF
test_cmp ../expect ../actual
'
test_expect_success 'set up for sparse checkout testing' '
echo two >done/.gitignore &&
echo three >>done/.gitignore &&
echo two >done/two &&
git add -f done/two done/.gitignore &&
git commit -m "first commit"
'
test_expect_success 'status after commit' '
: >../trace &&
GIT_TRACE_UNTRACKED_STATS="$TRASH_DIRECTORY/trace" \
git status --porcelain >../actual &&
cat >../status.expect <<EOF &&
?? .gitignore
?? dtwo/
EOF
test_cmp ../status.expect ../actual &&
cat >../trace.expect <<EOF &&
node creation: 0
gitignore invalidation: 0
directory invalidation: 0
opendir: 2
EOF
test_cmp ../trace.expect ../trace
'
test_expect_success 'untracked cache correct after commit' '
test-dump-untracked-cache >../actual &&
cat >../expect <<EOF &&
info/exclude 13263c0978fb9fad16b2d580fb800b6d811c3ff0
core.excludesfile 0000000000000000000000000000000000000000
exclude_per_dir .gitignore
flags 00000006
/ e6fcc8f2ee31bae321d66afd183fcb7237afae6e recurse valid
.gitignore
dtwo/
/done/ 0000000000000000000000000000000000000000 recurse valid
/dthree/ 0000000000000000000000000000000000000000 recurse check_only valid
/dtwo/ 0000000000000000000000000000000000000000 recurse check_only valid
two
EOF
test_cmp ../expect ../actual
'
test_expect_success 'set up sparse checkout' '
echo "done/[a-z]*" >.git/info/sparse-checkout &&
test_config core.sparsecheckout true &&
git checkout master &&
git update-index --force-untracked-cache &&
git status --porcelain >/dev/null && # prime the cache
test_path_is_missing done/.gitignore &&
test_path_is_file done/one
'
test_expect_success 'create/modify files, some of which are gitignored' '
echo two bis >done/two &&
echo three >done/three && # three is gitignored
echo four >done/four && # four is gitignored at a higher level
echo five >done/five # five is not gitignored
'
test_expect_success 'test sparse status with untracked cache' '
: >../trace &&
avoid_racy &&
GIT_TRACE_UNTRACKED_STATS="$TRASH_DIRECTORY/trace" \
git status --porcelain >../status.actual &&
cat >../status.expect <<EOF &&
M done/two
?? .gitignore
?? done/five
?? dtwo/
EOF
test_cmp ../status.expect ../status.actual &&
cat >../trace.expect <<EOF &&
node creation: 0
gitignore invalidation: 1
directory invalidation: 2
opendir: 2
EOF
test_cmp ../trace.expect ../trace
'
test_expect_success 'untracked cache correct after status' '
test-dump-untracked-cache >../actual &&
cat >../expect <<EOF &&
info/exclude 13263c0978fb9fad16b2d580fb800b6d811c3ff0
core.excludesfile 0000000000000000000000000000000000000000
exclude_per_dir .gitignore
flags 00000006
/ e6fcc8f2ee31bae321d66afd183fcb7237afae6e recurse valid
.gitignore
dtwo/
/done/ 1946f0437f90c5005533cbe1736a6451ca301714 recurse valid
five
/dthree/ 0000000000000000000000000000000000000000 recurse check_only valid
/dtwo/ 0000000000000000000000000000000000000000 recurse check_only valid
two
EOF
test_cmp ../expect ../actual
'
test_expect_success 'test sparse status again with untracked cache' '
avoid_racy &&
: >../trace &&
GIT_TRACE_UNTRACKED_STATS="$TRASH_DIRECTORY/trace" \
git status --porcelain >../status.actual &&
cat >../status.expect <<EOF &&
M done/two
?? .gitignore
?? done/five
?? dtwo/
EOF
test_cmp ../status.expect ../status.actual &&
cat >../trace.expect <<EOF &&
node creation: 0
gitignore invalidation: 0
directory invalidation: 0
opendir: 0
EOF
test_cmp ../trace.expect ../trace
'
test_expect_success 'set up for test of subdir and sparse checkouts' '
mkdir done/sub &&
mkdir done/sub/sub &&
echo "sub" > done/sub/sub/file
'
test_expect_success 'test sparse status with untracked cache and subdir' '
avoid_racy &&
: >../trace &&
GIT_TRACE_UNTRACKED_STATS="$TRASH_DIRECTORY/trace" \
git status --porcelain >../status.actual &&
cat >../status.expect <<EOF &&
M done/two
?? .gitignore
?? done/five
?? done/sub/
?? dtwo/
EOF
test_cmp ../status.expect ../status.actual &&
cat >../trace.expect <<EOF &&
node creation: 2
gitignore invalidation: 0
directory invalidation: 1
opendir: 3
EOF
test_cmp ../trace.expect ../trace
'
test_expect_success 'verify untracked cache dump (sparse/subdirs)' '
test-dump-untracked-cache >../actual &&
cat >../expect <<EOF &&
info/exclude 13263c0978fb9fad16b2d580fb800b6d811c3ff0
core.excludesfile 0000000000000000000000000000000000000000
exclude_per_dir .gitignore
flags 00000006
/ e6fcc8f2ee31bae321d66afd183fcb7237afae6e recurse valid
.gitignore
dtwo/
/done/ 1946f0437f90c5005533cbe1736a6451ca301714 recurse valid
five
sub/
/done/sub/ 0000000000000000000000000000000000000000 recurse check_only valid
sub/
/done/sub/sub/ 0000000000000000000000000000000000000000 recurse check_only valid
file
/dthree/ 0000000000000000000000000000000000000000 recurse check_only valid
/dtwo/ 0000000000000000000000000000000000000000 recurse check_only valid
two
EOF
test_cmp ../expect ../actual
'
test_expect_success 'test sparse status again with untracked cache and subdir' '
avoid_racy &&
: >../trace &&
GIT_TRACE_UNTRACKED_STATS="$TRASH_DIRECTORY/trace" \
git status --porcelain >../status.actual &&
test_cmp ../status.expect ../status.actual &&
cat >../trace.expect <<EOF &&
node creation: 0
gitignore invalidation: 0
directory invalidation: 0
opendir: 0
EOF
test_cmp ../trace.expect ../trace
'
test_expect_success 'move entry in subdir from untracked to cached' '
git add dtwo/two &&
git status --porcelain >../status.actual &&
cat >../status.expect <<EOF &&
M done/two
A dtwo/two
?? .gitignore
?? done/five
?? done/sub/
EOF
test_cmp ../status.expect ../status.actual
'
test_expect_success 'move entry in subdir from cached to untracked' '
git rm --cached dtwo/two &&
git status --porcelain >../status.actual &&
cat >../status.expect <<EOF &&
M done/two
?? .gitignore
?? done/five
?? done/sub/
?? dtwo/
EOF
test_cmp ../status.expect ../status.actual
'
test_done