Skip to content

Commit 844a185

Browse files
committed
unpack-trees: skip lstats for deleted VFS entries in checkout (#865)
When core_virtualfilesystem is set and a branch switch deletes entries (present in old tree, absent in new tree), deleted_entry() calls verify_absent_if_directory() with 'ce' pointing to a tree entry from traverse_trees(). This tree entry lacks CE_NEW_SKIP_WORKTREE because that flag is only set on src_index entries by mark_new_skip_worktree(). The missing flag causes verify_absent_if_directory()'s fast-path to fail, falling through to verify_absent_1() which lstats every such path. In a VFS repo each lstat may trigger callbacks, creating placeholders. On a large repo switching between LTS releases this produces tens of thousands of placeholders that the VFS must then clean up when they are deleted as part of the checkout. Fix this by propagating CE_NEW_SKIP_WORKTREE from the index entry (old) to the tree entry (ce) when core_virtualfilesystem is set. This allows the existing fast-path to work, eliminating the unnecessary lstats entirely. Measured on a 2.8M file VFS repo (0% hydrated): Before: ~135s checkout, ~23k folder placeholders created After: ~25s checkout, 0 folder placeholders created * [x] This change only applies to the virtualization hook and VFS for Git.
2 parents a0dd493 + c6d35dd commit 844a185

2 files changed

Lines changed: 63 additions & 0 deletions

File tree

t/t1093-virtualfilesystem.sh

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,57 @@ test_expect_success 'folder with same prefix as file' '
368368
test_cmp expected actual
369369
'
370370

371+
test_expect_success 'checkout skips lstat for deleted skip-worktree entries in VFS mode' '
372+
# When switching branches, entries present in the old tree but absent
373+
# in the new tree go through deleted_entry() -> verify_absent_if_directory().
374+
# Without the fix, the tree entry lacks CE_NEW_SKIP_WORKTREE (only
375+
# src_index entries get that flag), so verify_absent_if_directory()
376+
# falls through to verify_absent_1() which lstats the path. If a
377+
# directory exists where the deleted file entry was (simulating a
378+
# worst-case scenario), the lstat finds it and
379+
# verify_clean_subdirectory() rejects the checkout due to untracked
380+
# content inside.
381+
#
382+
# With the fix, verify_absent_if_directory() is skipped entirely
383+
# when VFS mode is active — no lstat, no rejection, checkout completes.
384+
#
385+
# Set up two branches: main has dir1/ + dir2/, side has only dir1/
386+
clean_repo &&
387+
388+
test_when_finished "rm -rf dir2/file1.txt && git -c core.virtualfilesystem= checkout main" &&
389+
390+
git -c core.virtualfilesystem= checkout -b side &&
391+
git -c core.virtualfilesystem= rm -rf dir2 &&
392+
git -c core.virtualfilesystem= commit -m "remove dir2" &&
393+
git -c core.virtualfilesystem= checkout main &&
394+
395+
# Configure VFS hook that returns nothing (0% hydration)
396+
write_script .git/hooks/virtualfilesystem <<-\EOF &&
397+
printf ""
398+
EOF
399+
400+
# Create a directory where the deleted file entry is, with
401+
# untracked content inside. This would not happen with a real
402+
# VFS because the VFS would report the file-to-directory change
403+
# in the virtualfilesystem hook results, clearing skip-worktree.
404+
# But it lets us verify that the lstat is not called: without
405+
# the fix, verify_absent_1() lstats this path, finds a directory,
406+
# and verify_clean_subdirectory() rejects the checkout because of
407+
# the untracked file inside.
408+
rm -f dir2/file1.txt &&
409+
mkdir -p dir2/file1.txt &&
410+
echo "untracked" >dir2/file1.txt/trap.txt &&
411+
412+
# Verify all entries are skip-worktree before checkout
413+
git ls-files -v >actual &&
414+
! grep "^H " actual &&
415+
416+
# Checkout to side branch. Without the fix this fails because
417+
# verify_absent_1 finds untracked content in the directory at
418+
# dir2/file1.txt. With the fix the lstat is skipped entirely.
419+
git checkout side
420+
'
421+
371422
test_expect_success MINGW,FSMONITOR_DAEMON 'virtualfilesystem hook disables built-in FSMonitor' '
372423
clean_repo &&
373424
test_config core.usebuiltinfsmonitor true &&

unpack-trees.c

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2721,6 +2721,18 @@ static int deleted_entry(const struct cache_entry *ce,
27212721
if (verify_absent(ce, ERROR_WOULD_LOSE_UNTRACKED_REMOVED, o))
27222722
return -1;
27232723
return 0;
2724+
} else if (core_virtualfilesystem &&
2725+
old->ce_flags & CE_NEW_SKIP_WORKTREE) {
2726+
/*
2727+
* When core_virtualfilesystem is set, 'ce' may be a tree
2728+
* entry from traverse_trees() that lacks CE_NEW_SKIP_WORKTREE
2729+
* (only src_index entries get that flag from
2730+
* mark_new_skip_worktree()). Propagate it from the index
2731+
* entry so apply_sparse_checkout() preserves CE_SKIP_WORKTREE
2732+
* later, and skip verify_absent_if_directory() entirely to
2733+
* avoid unnecessary lstats on virtualized paths.
2734+
*/
2735+
((struct cache_entry *)ce)->ce_flags |= CE_NEW_SKIP_WORKTREE;
27242736
} else if (verify_absent_if_directory(ce, ERROR_WOULD_LOSE_UNTRACKED_REMOVED, o)) {
27252737
return -1;
27262738
}

0 commit comments

Comments
 (0)