diff --git a/t/t1093-virtualfilesystem.sh b/t/t1093-virtualfilesystem.sh index cad13d680cb199..27e9f6423e4d9e 100755 --- a/t/t1093-virtualfilesystem.sh +++ b/t/t1093-virtualfilesystem.sh @@ -368,6 +368,58 @@ 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 CE_NEW_SKIP_WORKTREE propagation fix, the tree entry + # lacks that flag, so the fast-path fails and verify_absent_1() 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, CE_NEW_SKIP_WORKTREE is propagated and the fast-path + # succeeds — no lstat, no rejection, checkout completes. + # + # Set up two branches: main has dir1/ + dir2/, side has only dir1/ + clean_repo && + + 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 && + + # Clean up: return to main so subsequent tests have dir2/ + rm -rf dir2/file1.txt && + git -c core.virtualfilesystem= checkout main +' + 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..d05c4609defd69 100644 --- a/unpack-trees.c +++ b/unpack-trees.c @@ -2720,8 +2720,20 @@ 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 (verify_absent_if_directory(ce, ERROR_WOULD_LOSE_UNTRACKED_REMOVED, o)) { - return -1; + } else { + /* + * 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 verify_absent_if_directory()'s fast-path works, + * avoiding unnecessary lstats on virtualized paths. + */ + if (core_virtualfilesystem && + old->ce_flags & CE_NEW_SKIP_WORKTREE) + ((struct cache_entry *)ce)->ce_flags |= CE_NEW_SKIP_WORKTREE; + if (verify_absent_if_directory(ce, ERROR_WOULD_LOSE_UNTRACKED_REMOVED, o)) + return -1; } if (!(old->ce_flags & CE_CONFLICTED) && verify_uptodate(old, o))