Skip to content

Commit 8833ccd

Browse files
committed
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
2 parents d6579d9 + 73f9145 commit 8833ccd

File tree

2 files changed

+282
-32
lines changed

2 files changed

+282
-32
lines changed

dir.c

Lines changed: 67 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1078,10 +1078,9 @@ static void prep_exclude(struct dir_struct *dir, const char *base, int baselen)
10781078
(!untracked || !untracked->valid ||
10791079
/*
10801080
* .. and .gitignore does not exist before
1081-
* (i.e. null exclude_sha1 and skip_worktree is
1082-
* not set). Then we can skip loading .gitignore,
1083-
* which would result in ENOENT anyway.
1084-
* skip_worktree is taken care in read_directory()
1081+
* (i.e. null exclude_sha1). Then we can skip
1082+
* loading .gitignore, which would result in
1083+
* ENOENT anyway.
10851084
*/
10861085
!is_null_sha1(untracked->exclude_sha1))) {
10871086
/*
@@ -1298,7 +1297,7 @@ static enum exist_status directory_exists_in_index(const char *dirname, int len)
12981297
*/
12991298
static enum path_treatment treat_directory(struct dir_struct *dir,
13001299
struct untracked_cache_dir *untracked,
1301-
const char *dirname, int len, int exclude,
1300+
const char *dirname, int len, int baselen, int exclude,
13021301
const struct path_simplify *simplify)
13031302
{
13041303
/* The "len-1" is to strip the final '/' */
@@ -1325,7 +1324,8 @@ static enum path_treatment treat_directory(struct dir_struct *dir,
13251324
if (!(dir->flags & DIR_HIDE_EMPTY_DIRECTORIES))
13261325
return exclude ? path_excluded : path_untracked;
13271326

1328-
untracked = lookup_untracked(dir->untracked, untracked, dirname, len);
1327+
untracked = lookup_untracked(dir->untracked, untracked,
1328+
dirname + baselen, len - baselen);
13291329
return read_directory_recursive(dir, dirname, len,
13301330
untracked, 1, simplify);
13311331
}
@@ -1445,6 +1445,7 @@ static int get_dtype(struct dirent *de, const char *path, int len)
14451445
static enum path_treatment treat_one_path(struct dir_struct *dir,
14461446
struct untracked_cache_dir *untracked,
14471447
struct strbuf *path,
1448+
int baselen,
14481449
const struct path_simplify *simplify,
14491450
int dtype, struct dirent *de)
14501451
{
@@ -1496,8 +1497,8 @@ static enum path_treatment treat_one_path(struct dir_struct *dir,
14961497
return path_none;
14971498
case DT_DIR:
14981499
strbuf_addch(path, '/');
1499-
return treat_directory(dir, untracked, path->buf, path->len, exclude,
1500-
simplify);
1500+
return treat_directory(dir, untracked, path->buf, path->len,
1501+
baselen, exclude, simplify);
15011502
case DT_REG:
15021503
case DT_LNK:
15031504
return exclude ? path_excluded : path_untracked;
@@ -1558,7 +1559,7 @@ static enum path_treatment treat_path(struct dir_struct *dir,
15581559
return path_none;
15591560

15601561
dtype = DTYPE(de);
1561-
return treat_one_path(dir, untracked, path, simplify, dtype, de);
1562+
return treat_one_path(dir, untracked, path, baselen, simplify, dtype, de);
15621563
}
15631564

15641565
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,
18281829
break;
18291830
if (simplify_away(sb.buf, sb.len, simplify))
18301831
break;
1831-
if (treat_one_path(dir, NULL, &sb, simplify,
1832+
if (treat_one_path(dir, NULL, &sb, baselen, simplify,
18321833
DT_DIR, NULL) == path_none)
18331834
break; /* do not recurse into it */
18341835
if (len <= baselen) {
@@ -1880,7 +1881,6 @@ static struct untracked_cache_dir *validate_untracked_cache(struct dir_struct *d
18801881
const struct pathspec *pathspec)
18811882
{
18821883
struct untracked_cache_dir *root;
1883-
int i;
18841884

18851885
if (!dir->untracked || getenv("GIT_DISABLE_UNTRACKED_CACHE"))
18861886
return NULL;
@@ -1932,15 +1932,6 @@ static struct untracked_cache_dir *validate_untracked_cache(struct dir_struct *d
19321932
if (dir->exclude_list_group[EXC_CMDL].nr)
19331933
return NULL;
19341934

1935-
/*
1936-
* An optimization in prep_exclude() does not play well with
1937-
* CE_SKIP_WORKTREE. It's a rare case anyway, if a single
1938-
* entry has that bit set, disable the whole untracked cache.
1939-
*/
1940-
for (i = 0; i < active_nr; i++)
1941-
if (ce_skip_worktree(active_cache[i]))
1942-
return NULL;
1943-
19441935
if (!ident_in_untracked(dir->untracked)) {
19451936
warning(_("Untracked cache is disabled on this system."));
19461937
return NULL;
@@ -2625,23 +2616,67 @@ struct untracked_cache *read_untracked_extension(const void *data, unsigned long
26252616
return uc;
26262617
}
26272618

2619+
static void invalidate_one_directory(struct untracked_cache *uc,
2620+
struct untracked_cache_dir *ucd)
2621+
{
2622+
uc->dir_invalidated++;
2623+
ucd->valid = 0;
2624+
ucd->untracked_nr = 0;
2625+
}
2626+
2627+
/*
2628+
* Normally when an entry is added or removed from a directory,
2629+
* invalidating that directory is enough. No need to touch its
2630+
* ancestors. When a directory is shown as "foo/bar/" in git-status
2631+
* however, deleting or adding an entry may have cascading effect.
2632+
*
2633+
* Say the "foo/bar/file" has become untracked, we need to tell the
2634+
* untracked_cache_dir of "foo" that "bar/" is not an untracked
2635+
* directory any more (because "bar" is managed by foo as an untracked
2636+
* "file").
2637+
*
2638+
* Similarly, if "foo/bar/file" moves from untracked to tracked and it
2639+
* was the last untracked entry in the entire "foo", we should show
2640+
* "foo/" instead. Which means we have to invalidate past "bar" up to
2641+
* "foo".
2642+
*
2643+
* This function traverses all directories from root to leaf. If there
2644+
* is a chance of one of the above cases happening, we invalidate back
2645+
* to root. Otherwise we just invalidate the leaf. There may be a more
2646+
* sophisticated way than checking for SHOW_OTHER_DIRECTORIES to
2647+
* detect these cases and avoid unnecessary invalidation, for example,
2648+
* checking for the untracked entry named "bar/" in "foo", but for now
2649+
* stick to something safe and simple.
2650+
*/
2651+
static int invalidate_one_component(struct untracked_cache *uc,
2652+
struct untracked_cache_dir *dir,
2653+
const char *path, int len)
2654+
{
2655+
const char *rest = strchr(path, '/');
2656+
2657+
if (rest) {
2658+
int component_len = rest - path;
2659+
struct untracked_cache_dir *d =
2660+
lookup_untracked(uc, dir, path, component_len);
2661+
int ret =
2662+
invalidate_one_component(uc, d, rest + 1,
2663+
len - (component_len + 1));
2664+
if (ret)
2665+
invalidate_one_directory(uc, dir);
2666+
return ret;
2667+
}
2668+
2669+
invalidate_one_directory(uc, dir);
2670+
return uc->dir_flags & DIR_SHOW_OTHER_DIRECTORIES;
2671+
}
2672+
26282673
void untracked_cache_invalidate_path(struct index_state *istate,
26292674
const char *path)
26302675
{
2631-
const char *sep;
2632-
struct untracked_cache_dir *d;
26332676
if (!istate->untracked || !istate->untracked->root)
26342677
return;
2635-
sep = strrchr(path, '/');
2636-
if (sep)
2637-
d = lookup_untracked(istate->untracked,
2638-
istate->untracked->root,
2639-
path, sep - path);
2640-
else
2641-
d = istate->untracked->root;
2642-
istate->untracked->dir_invalidated++;
2643-
d->valid = 0;
2644-
d->untracked_nr = 0;
2678+
invalidate_one_component(istate->untracked, istate->untracked->root,
2679+
path, strlen(path));
26452680
}
26462681

26472682
void untracked_cache_remove_from_index(struct index_state *istate,

t/t7063-status-untracked-cache.sh

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,4 +354,219 @@ EOF
354354
test_cmp ../expect ../actual
355355
'
356356

357+
test_expect_success 'set up for sparse checkout testing' '
358+
echo two >done/.gitignore &&
359+
echo three >>done/.gitignore &&
360+
echo two >done/two &&
361+
git add -f done/two done/.gitignore &&
362+
git commit -m "first commit"
363+
'
364+
365+
test_expect_success 'status after commit' '
366+
: >../trace &&
367+
GIT_TRACE_UNTRACKED_STATS="$TRASH_DIRECTORY/trace" \
368+
git status --porcelain >../actual &&
369+
cat >../status.expect <<EOF &&
370+
?? .gitignore
371+
?? dtwo/
372+
EOF
373+
test_cmp ../status.expect ../actual &&
374+
cat >../trace.expect <<EOF &&
375+
node creation: 0
376+
gitignore invalidation: 0
377+
directory invalidation: 0
378+
opendir: 2
379+
EOF
380+
test_cmp ../trace.expect ../trace
381+
'
382+
383+
test_expect_success 'untracked cache correct after commit' '
384+
test-dump-untracked-cache >../actual &&
385+
cat >../expect <<EOF &&
386+
info/exclude 13263c0978fb9fad16b2d580fb800b6d811c3ff0
387+
core.excludesfile 0000000000000000000000000000000000000000
388+
exclude_per_dir .gitignore
389+
flags 00000006
390+
/ e6fcc8f2ee31bae321d66afd183fcb7237afae6e recurse valid
391+
.gitignore
392+
dtwo/
393+
/done/ 0000000000000000000000000000000000000000 recurse valid
394+
/dthree/ 0000000000000000000000000000000000000000 recurse check_only valid
395+
/dtwo/ 0000000000000000000000000000000000000000 recurse check_only valid
396+
two
397+
EOF
398+
test_cmp ../expect ../actual
399+
'
400+
401+
test_expect_success 'set up sparse checkout' '
402+
echo "done/[a-z]*" >.git/info/sparse-checkout &&
403+
test_config core.sparsecheckout true &&
404+
git checkout master &&
405+
git update-index --force-untracked-cache &&
406+
git status --porcelain >/dev/null && # prime the cache
407+
test_path_is_missing done/.gitignore &&
408+
test_path_is_file done/one
409+
'
410+
411+
test_expect_success 'create/modify files, some of which are gitignored' '
412+
echo two bis >done/two &&
413+
echo three >done/three && # three is gitignored
414+
echo four >done/four && # four is gitignored at a higher level
415+
echo five >done/five # five is not gitignored
416+
'
417+
418+
test_expect_success 'test sparse status with untracked cache' '
419+
: >../trace &&
420+
avoid_racy &&
421+
GIT_TRACE_UNTRACKED_STATS="$TRASH_DIRECTORY/trace" \
422+
git status --porcelain >../status.actual &&
423+
cat >../status.expect <<EOF &&
424+
M done/two
425+
?? .gitignore
426+
?? done/five
427+
?? dtwo/
428+
EOF
429+
test_cmp ../status.expect ../status.actual &&
430+
cat >../trace.expect <<EOF &&
431+
node creation: 0
432+
gitignore invalidation: 1
433+
directory invalidation: 2
434+
opendir: 2
435+
EOF
436+
test_cmp ../trace.expect ../trace
437+
'
438+
439+
test_expect_success 'untracked cache correct after status' '
440+
test-dump-untracked-cache >../actual &&
441+
cat >../expect <<EOF &&
442+
info/exclude 13263c0978fb9fad16b2d580fb800b6d811c3ff0
443+
core.excludesfile 0000000000000000000000000000000000000000
444+
exclude_per_dir .gitignore
445+
flags 00000006
446+
/ e6fcc8f2ee31bae321d66afd183fcb7237afae6e recurse valid
447+
.gitignore
448+
dtwo/
449+
/done/ 1946f0437f90c5005533cbe1736a6451ca301714 recurse valid
450+
five
451+
/dthree/ 0000000000000000000000000000000000000000 recurse check_only valid
452+
/dtwo/ 0000000000000000000000000000000000000000 recurse check_only valid
453+
two
454+
EOF
455+
test_cmp ../expect ../actual
456+
'
457+
458+
test_expect_success 'test sparse status again with untracked cache' '
459+
avoid_racy &&
460+
: >../trace &&
461+
GIT_TRACE_UNTRACKED_STATS="$TRASH_DIRECTORY/trace" \
462+
git status --porcelain >../status.actual &&
463+
cat >../status.expect <<EOF &&
464+
M done/two
465+
?? .gitignore
466+
?? done/five
467+
?? dtwo/
468+
EOF
469+
test_cmp ../status.expect ../status.actual &&
470+
cat >../trace.expect <<EOF &&
471+
node creation: 0
472+
gitignore invalidation: 0
473+
directory invalidation: 0
474+
opendir: 0
475+
EOF
476+
test_cmp ../trace.expect ../trace
477+
'
478+
479+
test_expect_success 'set up for test of subdir and sparse checkouts' '
480+
mkdir done/sub &&
481+
mkdir done/sub/sub &&
482+
echo "sub" > done/sub/sub/file
483+
'
484+
485+
test_expect_success 'test sparse status with untracked cache and subdir' '
486+
avoid_racy &&
487+
: >../trace &&
488+
GIT_TRACE_UNTRACKED_STATS="$TRASH_DIRECTORY/trace" \
489+
git status --porcelain >../status.actual &&
490+
cat >../status.expect <<EOF &&
491+
M done/two
492+
?? .gitignore
493+
?? done/five
494+
?? done/sub/
495+
?? dtwo/
496+
EOF
497+
test_cmp ../status.expect ../status.actual &&
498+
cat >../trace.expect <<EOF &&
499+
node creation: 2
500+
gitignore invalidation: 0
501+
directory invalidation: 1
502+
opendir: 3
503+
EOF
504+
test_cmp ../trace.expect ../trace
505+
'
506+
507+
test_expect_success 'verify untracked cache dump (sparse/subdirs)' '
508+
test-dump-untracked-cache >../actual &&
509+
cat >../expect <<EOF &&
510+
info/exclude 13263c0978fb9fad16b2d580fb800b6d811c3ff0
511+
core.excludesfile 0000000000000000000000000000000000000000
512+
exclude_per_dir .gitignore
513+
flags 00000006
514+
/ e6fcc8f2ee31bae321d66afd183fcb7237afae6e recurse valid
515+
.gitignore
516+
dtwo/
517+
/done/ 1946f0437f90c5005533cbe1736a6451ca301714 recurse valid
518+
five
519+
sub/
520+
/done/sub/ 0000000000000000000000000000000000000000 recurse check_only valid
521+
sub/
522+
/done/sub/sub/ 0000000000000000000000000000000000000000 recurse check_only valid
523+
file
524+
/dthree/ 0000000000000000000000000000000000000000 recurse check_only valid
525+
/dtwo/ 0000000000000000000000000000000000000000 recurse check_only valid
526+
two
527+
EOF
528+
test_cmp ../expect ../actual
529+
'
530+
531+
test_expect_success 'test sparse status again with untracked cache and subdir' '
532+
avoid_racy &&
533+
: >../trace &&
534+
GIT_TRACE_UNTRACKED_STATS="$TRASH_DIRECTORY/trace" \
535+
git status --porcelain >../status.actual &&
536+
test_cmp ../status.expect ../status.actual &&
537+
cat >../trace.expect <<EOF &&
538+
node creation: 0
539+
gitignore invalidation: 0
540+
directory invalidation: 0
541+
opendir: 0
542+
EOF
543+
test_cmp ../trace.expect ../trace
544+
'
545+
546+
test_expect_success 'move entry in subdir from untracked to cached' '
547+
git add dtwo/two &&
548+
git status --porcelain >../status.actual &&
549+
cat >../status.expect <<EOF &&
550+
M done/two
551+
A dtwo/two
552+
?? .gitignore
553+
?? done/five
554+
?? done/sub/
555+
EOF
556+
test_cmp ../status.expect ../status.actual
557+
'
558+
559+
test_expect_success 'move entry in subdir from cached to untracked' '
560+
git rm --cached dtwo/two &&
561+
git status --porcelain >../status.actual &&
562+
cat >../status.expect <<EOF &&
563+
M done/two
564+
?? .gitignore
565+
?? done/five
566+
?? done/sub/
567+
?? dtwo/
568+
EOF
569+
test_cmp ../status.expect ../status.actual
570+
'
571+
357572
test_done

0 commit comments

Comments
 (0)