untracked cache: fix entry invalidation

First, the current code in untracked_cache_invalidate_path() is wrong because it can only handle paths "a" or "a/b", not "a/b/c" because lookup_untracked() only looks for entries directly under the given directory. In the last case, it will look for the entry "b/c" in directory "a" instead. This means if you delete or add an entry in a subdirectory, untracked cache may become out of date because it does not invalidate properly. This is noticed by David Turner. The second problem is about invalidation inside a fully untracked/excluded directory. In this case we may have to invalidate back to root. See the comment block for detail. Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Nguyễn Thái Ngọc Duy committed Aug 19, 2015 at 20:01 UTC 73f9145fbf748d39dd1e145ec846a5481cf7a36f
2 files changed +83 -13
dir.c
+56 -12
@@ -2616,23 +2616,67 @@ done2:
2616 return uc;
2617 }
2618
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 +
2673 void untracked_cache_invalidate_path(struct index_state *istate,
2674 const char *path)
2675 {
2622 - const char *sep;
2623 - struct untracked_cache_dir *d;
2676 if (!istate->untracked || !istate->untracked->root)
2677 return;
2626 - sep = strrchr(path, '/');
2627 - if (sep)
2628 - d = lookup_untracked(istate->untracked,
2629 - istate->untracked->root,
2630 - path, sep - path);
2631 - else
2632 - d = istate->untracked->root;
2633 - istate->untracked->dir_invalidated++;
2634 - d->valid = 0;
2635 - d->untracked_nr = 0;
2678 + invalidate_one_component(istate->untracked, istate->untracked->root,
2679 + path, strlen(path));
2680 }
2681
2682 void untracked_cache_remove_from_index(struct index_state *istate,
t/t7063-status-untracked-cache.sh
+27 -1
@@ -375,7 +375,7 @@ EOF
375 node creation: 0
376 gitignore invalidation: 0
377 directory invalidation: 0
378 -opendir: 1
378 +opendir: 2
379 EOF
380 test_cmp ../trace.expect ../trace
381 '
@@ -543,4 +543,30 @@ 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 +
572 test_done