Merge branch 'nd/fix-untracked-cache-invalidation'
authorJunio C Hamano <gitster@pobox.com>
Tue, 27 Feb 2018 18:33:49 +0000 (10:33 -0800)
committerJunio C Hamano <gitster@pobox.com>
Tue, 27 Feb 2018 18:33:50 +0000 (10:33 -0800)
Some bugs around "untracked cache" feature have been fixed.

* nd/fix-untracked-cache-invalidation:
dir.c: ignore paths containing .git when invalidating untracked cache
dir.c: stop ignoring opendir() error in open_cached_dir()
dir.c: fix missing dir invalidation in untracked code
dir.c: avoid stat() in valid_cached_dir()
status: add a failing test showing a core.untrackedCache bug

dir.c
dir.h
fsmonitor.c
fsmonitor.h
t/t7063-status-untracked-cache.sh
t/t7519-status-fsmonitor.sh
unpack-trees.c
diff --git a/dir.c b/dir.c
index 536416ff2df5f7c475deba4739d4edaf340ae898..6dd91be8185d4012e11495a9238636913fedeeef 100644 (file)
--- a/dir.c
+++ b/dir.c
@@ -771,7 +771,16 @@ static void invalidate_directory(struct untracked_cache *uc,
                                 struct untracked_cache_dir *dir)
 {
        int i;
-       uc->dir_invalidated++;
+
+       /*
+        * Invalidation increment here is just roughly correct. If
+        * untracked_nr or any of dirs[].recurse is non-zero, we
+        * should increment dir_invalidated too. But that's more
+        * expensive to do.
+        */
+       if (dir->valid)
+               uc->dir_invalidated++;
+
        dir->valid = 0;
        dir->untracked_nr = 0;
        for (i = 0; i < dir->dirs_nr; i++)
@@ -1770,7 +1779,7 @@ static enum path_treatment treat_path(struct dir_struct *dir,
        if (!de)
                return treat_path_fast(dir, untracked, cdir, istate, path,
                                       baselen, pathspec);
-       if (is_dot_or_dotdot(de->d_name) || !strcmp(de->d_name, ".git"))
+       if (is_dot_or_dotdot(de->d_name) || !fspathcmp(de->d_name, ".git"))
                return path_none;
        strbuf_setlen(path, baselen);
        strbuf_addstr(path, de->d_name);
@@ -1806,24 +1815,19 @@ static int valid_cached_dir(struct dir_struct *dir,
         */
        refresh_fsmonitor(istate);
        if (!(dir->untracked->use_fsmonitor && untracked->valid)) {
-               if (stat(path->len ? path->buf : ".", &st)) {
-                       invalidate_directory(dir->untracked, untracked);
+               if (lstat(path->len ? path->buf : ".", &st)) {
                        memset(&untracked->stat_data, 0, sizeof(untracked->stat_data));
                        return 0;
                }
                if (!untracked->valid ||
                        match_stat_data_racy(istate, &untracked->stat_data, &st)) {
-                       if (untracked->valid)
-                               invalidate_directory(dir->untracked, untracked);
                        fill_stat_data(&untracked->stat_data, &st);
                        return 0;
                }
        }
 
-       if (untracked->check_only != !!check_only) {
-               invalidate_directory(dir->untracked, untracked);
+       if (untracked->check_only != !!check_only)
                return 0;
-       }
 
        /*
         * prep_exclude will be called eventually on this directory,
@@ -1850,13 +1854,20 @@ static int open_cached_dir(struct cached_dir *cdir,
                           struct strbuf *path,
                           int check_only)
 {
+       const char *c_path;
+
        memset(cdir, 0, sizeof(*cdir));
        cdir->untracked = untracked;
        if (valid_cached_dir(dir, untracked, istate, path, check_only))
                return 0;
-       cdir->fdir = opendir(path->len ? path->buf : ".");
-       if (dir->untracked)
+       c_path = path->len ? path->buf : ".";
+       cdir->fdir = opendir(c_path);
+       if (!cdir->fdir)
+               warning_errno(_("could not open directory '%s'"), c_path);
+       if (dir->untracked) {
+               invalidate_directory(dir->untracked, untracked);
                dir->untracked->dir_opened++;
+       }
        if (!cdir->fdir)
                return -1;
        return 0;
@@ -2966,10 +2977,12 @@ static int invalidate_one_component(struct untracked_cache *uc,
 }
 
 void untracked_cache_invalidate_path(struct index_state *istate,
-                                    const char *path)
+                                    const char *path, int safe_path)
 {
        if (!istate->untracked || !istate->untracked->root)
                return;
+       if (!safe_path && !verify_path(path))
+               return;
        invalidate_one_component(istate->untracked, istate->untracked->root,
                                 path, strlen(path));
 }
@@ -2977,13 +2990,13 @@ void untracked_cache_invalidate_path(struct index_state *istate,
 void untracked_cache_remove_from_index(struct index_state *istate,
                                       const char *path)
 {
-       untracked_cache_invalidate_path(istate, path);
+       untracked_cache_invalidate_path(istate, path, 1);
 }
 
 void untracked_cache_add_to_index(struct index_state *istate,
                                  const char *path)
 {
-       untracked_cache_invalidate_path(istate, path);
+       untracked_cache_invalidate_path(istate, path, 1);
 }
 
 /* Update gitfile and core.worktree setting to connect work tree and git dir */
diff --git a/dir.h b/dir.h
index e7bb786a33967360750ad21446ebe65b3bea8b80..b0758b82a20017dd3ce29c54454678f026718078 100644 (file)
--- a/dir.h
+++ b/dir.h
@@ -350,7 +350,7 @@ static inline int dir_path_match(const struct dir_entry *ent,
 int cmp_dir_entry(const void *p1, const void *p2);
 int check_dir_entry_contains(const struct dir_entry *out, const struct dir_entry *in);
 
-void untracked_cache_invalidate_path(struct index_state *, const char *);
+void untracked_cache_invalidate_path(struct index_state *, const char *, int safe_path);
 void untracked_cache_remove_from_index(struct index_state *, const char *);
 void untracked_cache_add_to_index(struct index_state *, const char *);
 
index 0af7c4edba37fd9f6a8cee83cd2715c3fa08b488..6d7bcd5d0ed8f2d3f5abdea2f26c6be72909b657 100644 (file)
@@ -130,7 +130,7 @@ static void fsmonitor_refresh_callback(struct index_state *istate, const char *n
         * as it could be a new untracked file.
         */
        trace_printf_key(&trace_fsmonitor, "fsmonitor_refresh_callback '%s'", name);
-       untracked_cache_invalidate_path(istate, name);
+       untracked_cache_invalidate_path(istate, name, 0);
 }
 
 void refresh_fsmonitor(struct index_state *istate)
index cd3cc0ccf228c9d601621685042d51368a71c707..65f37436369cd934f2bdb3b627396c5f72a7cb03 100644 (file)
@@ -65,7 +65,7 @@ static inline void mark_fsmonitor_invalid(struct index_state *istate, struct cac
 {
        if (core_fsmonitor) {
                ce->ce_flags &= ~CE_FSMONITOR_VALID;
-               untracked_cache_invalidate_path(istate, ce->name);
+               untracked_cache_invalidate_path(istate, ce->name, 1);
                trace_printf_key(&trace_fsmonitor, "mark_fsmonitor_invalid '%s'", ce->name);
        }
 }
index e5fb892f9575fda4baf0b2a0e6b31cf13a0d6c0b..46b947824fd68150cfaa6018355cd086c5a8759e 100755 (executable)
@@ -22,6 +22,12 @@ avoid_racy() {
        sleep 1
 }
 
+status_is_clean() {
+       >../status.expect &&
+       git status --porcelain >../status.actual &&
+       test_cmp ../status.expect ../status.actual
+}
+
 test_lazy_prereq UNTRACKED_CACHE '
        { git update-index --test-untracked-cache; ret=$?; } &&
        test $ret -ne 1
@@ -683,4 +689,85 @@ test_expect_success 'untracked cache survives a commit' '
        test_cmp ../before ../after
 '
 
+test_expect_success 'teardown worktree' '
+       cd ..
+'
+
+test_expect_success SYMLINKS 'setup worktree for symlink test' '
+       git init worktree-symlink &&
+       cd worktree-symlink &&
+       git config core.untrackedCache true &&
+       mkdir one two &&
+       touch one/file two/file &&
+       git add one/file two/file &&
+       git commit -m"first commit" &&
+       git rm -rf one &&
+       ln -s two one &&
+       git add one &&
+       git commit -m"second commit"
+'
+
+test_expect_success SYMLINKS '"status" after symlink replacement should be clean with UC=true' '
+       git checkout HEAD~ &&
+       status_is_clean &&
+       status_is_clean &&
+       git checkout master &&
+       avoid_racy &&
+       status_is_clean &&
+       status_is_clean
+'
+
+test_expect_success SYMLINKS '"status" after symlink replacement should be clean with UC=false' '
+       git config core.untrackedCache false &&
+       git checkout HEAD~ &&
+       status_is_clean &&
+       status_is_clean &&
+       git checkout master &&
+       avoid_racy &&
+       status_is_clean &&
+       status_is_clean
+'
+
+test_expect_success 'setup worktree for non-symlink test' '
+       git init worktree-non-symlink &&
+       cd worktree-non-symlink &&
+       git config core.untrackedCache true &&
+       mkdir one two &&
+       touch one/file two/file &&
+       git add one/file two/file &&
+       git commit -m"first commit" &&
+       git rm -rf one &&
+       cp two/file one &&
+       git add one &&
+       git commit -m"second commit"
+'
+
+test_expect_success '"status" after file replacement should be clean with UC=true' '
+       git checkout HEAD~ &&
+       status_is_clean &&
+       status_is_clean &&
+       git checkout master &&
+       avoid_racy &&
+       status_is_clean &&
+       test-dump-untracked-cache >../actual &&
+       grep -F "recurse valid" ../actual >../actual.grep &&
+       cat >../expect.grep <<EOF &&
+/ 0000000000000000000000000000000000000000 recurse valid
+/two/ 0000000000000000000000000000000000000000 recurse valid
+EOF
+       status_is_clean &&
+       test_cmp ../expect.grep ../actual.grep
+'
+
+test_expect_success '"status" after file replacement should be clean with UC=false' '
+       git config core.untrackedCache false &&
+       git checkout HEAD~ &&
+       status_is_clean &&
+       status_is_clean &&
+       git checkout master &&
+       avoid_racy &&
+       status_is_clean &&
+       status_is_clean
+'
+
 test_done
index eb2d13bbcf8abefd7af2be6f9cb3bf97e389ab15..756beb0d8eb466d78b235af363b6a36dde37c79e 100755 (executable)
@@ -314,4 +314,43 @@ test_expect_success 'splitting the index results in the same state' '
        test_cmp expect actual
 '
 
+test_expect_success UNTRACKED_CACHE 'ignore .git changes when invalidating UNTR' '
+       test_create_repo dot-git &&
+       (
+               cd dot-git &&
+               mkdir -p .git/hooks &&
+               : >tracked &&
+               : >modified &&
+               mkdir dir1 &&
+               : >dir1/tracked &&
+               : >dir1/modified &&
+               mkdir dir2 &&
+               : >dir2/tracked &&
+               : >dir2/modified &&
+               write_integration_script &&
+               git config core.fsmonitor .git/hooks/fsmonitor-test &&
+               git update-index --untracked-cache &&
+               git update-index --fsmonitor &&
+               GIT_TRACE_UNTRACKED_STATS="$TRASH_DIRECTORY/trace-before" \
+               git status &&
+               test-dump-untracked-cache >../before
+       ) &&
+       cat >>dot-git/.git/hooks/fsmonitor-test <<-\EOF &&
+       printf ".git\0"
+       printf ".git/index\0"
+       printf "dir1/.git\0"
+       printf "dir1/.git/index\0"
+       EOF
+       (
+               cd dot-git &&
+               GIT_TRACE_UNTRACKED_STATS="$TRASH_DIRECTORY/trace-after" \
+               git status &&
+               test-dump-untracked-cache >../after
+       ) &&
+       grep "directory invalidation" trace-before >>before &&
+       grep "directory invalidation" trace-after >>after &&
+       # UNTR extension unchanged, dir invalidation count unchanged
+       test_cmp before after
+'
+
 test_done
index e6a15bbe44f24555a8edb9461c2af553de70cba0..c9f6e314d5cd830d63236ea88c5ff9c7baf418bb 100644 (file)
@@ -1528,7 +1528,7 @@ static void invalidate_ce_path(const struct cache_entry *ce,
        if (!ce)
                return;
        cache_tree_invalidate_path(o->src_index, ce->name);
-       untracked_cache_invalidate_path(o->src_index, ce->name);
+       untracked_cache_invalidate_path(o->src_index, ce->name, 1);
 }
 
 /*