diff --git a/t/t1093-virtualfilesystem.sh b/t/t1093-virtualfilesystem.sh index cad13d680cb199..31c1aa77c6712e 100755 --- a/t/t1093-virtualfilesystem.sh +++ b/t/t1093-virtualfilesystem.sh @@ -368,6 +368,57 @@ test_expect_success 'folder with same prefix as file' ' test_cmp expected actual ' +test_expect_success 'checkout skips lstat for deleted skip-worktree entries in VFS mode' ' + # When switching branches, entries present in the old tree but absent + # in the new tree go through deleted_entry() -> verify_absent_if_directory(). + # Without the fix, the tree entry lacks CE_NEW_SKIP_WORKTREE (only + # src_index entries get that flag), so verify_absent_if_directory() + # falls through to verify_absent_1() which lstats the path. If a + # directory exists where the deleted file entry was (simulating a + # worst-case scenario), the lstat finds it and + # verify_clean_subdirectory() rejects the checkout due to untracked + # content inside. + # + # With the fix, verify_absent_if_directory() is skipped entirely + # when VFS mode is active — no lstat, no rejection, checkout completes. + # + # Set up two branches: main has dir1/ + dir2/, side has only dir1/ + clean_repo && + + test_when_finished "rm -rf dir2/file1.txt && git -c core.virtualfilesystem= checkout main" && + + git -c core.virtualfilesystem= checkout -b side && + git -c core.virtualfilesystem= rm -rf dir2 && + git -c core.virtualfilesystem= commit -m "remove dir2" && + git -c core.virtualfilesystem= checkout main && + + # Configure VFS hook that returns nothing (0% hydration) + write_script .git/hooks/virtualfilesystem <<-\EOF && + printf "" + EOF + + # Create a directory where the deleted file entry is, with + # untracked content inside. This would not happen with a real + # VFS because the VFS would report the file-to-directory change + # in the virtualfilesystem hook results, clearing skip-worktree. + # But it lets us verify that the lstat is not called: without + # the fix, verify_absent_1() lstats this path, finds a directory, + # and verify_clean_subdirectory() rejects the checkout because of + # the untracked file inside. + rm -f dir2/file1.txt && + mkdir -p dir2/file1.txt && + echo "untracked" >dir2/file1.txt/trap.txt && + + # Verify all entries are skip-worktree before checkout + git ls-files -v >actual && + ! grep "^H " actual && + + # Checkout to side branch. Without the fix this fails because + # verify_absent_1 finds untracked content in the directory at + # dir2/file1.txt. With the fix the lstat is skipped entirely. + git checkout side +' + test_expect_success MINGW,FSMONITOR_DAEMON 'virtualfilesystem hook disables built-in FSMonitor' ' clean_repo && test_config core.usebuiltinfsmonitor true && diff --git a/unpack-trees.c b/unpack-trees.c index 4d897829419a3d..1463758686d816 100644 --- a/unpack-trees.c +++ b/unpack-trees.c @@ -2720,6 +2720,18 @@ static int deleted_entry(const struct cache_entry *ce, if (verify_absent(ce, ERROR_WOULD_LOSE_UNTRACKED_REMOVED, o)) return -1; return 0; + } else if (core_virtualfilesystem && + old->ce_flags & CE_NEW_SKIP_WORKTREE) { + /* + * When core_virtualfilesystem is set, 'ce' may be a tree + * entry from traverse_trees() that lacks CE_NEW_SKIP_WORKTREE + * (only src_index entries get that flag from + * mark_new_skip_worktree()). Propagate it from the index + * entry so apply_sparse_checkout() preserves CE_SKIP_WORKTREE + * later, and skip verify_absent_if_directory() entirely to + * avoid unnecessary lstats on virtualized paths. + */ + ((struct cache_entry *)ce)->ce_flags |= CE_NEW_SKIP_WORKTREE; } else if (verify_absent_if_directory(ce, ERROR_WOULD_LOSE_UNTRACKED_REMOVED, o)) { return -1; }