From d40f13bf21b1991abb8a2cd3af891f2ab66714c4 Mon Sep 17 00:00:00 2001 From: Tyrie Vella Date: Thu, 14 May 2026 14:40:26 -0700 Subject: [PATCH] checkout: preserve skip-worktree for virtual filesystem paths When 'git checkout -- ' updates the index with entries from the source tree, update_some() creates a new cache entry that unconditionally clears skip-worktree. In a virtual filesystem repo (core.virtualfilesystem is set), this causes checkout_entry() to attempt unlink() on files that exist only as virtual projections with no physical NTFS entry. The unlink fails with ENOENT, producing 'error: unable to unlink old' messages and exit code 255. Fix this in three places: 1. update_some(): Propagate CE_SKIP_WORKTREE from the existing index entry to the replacement entry when core_virtualfilesystem is set. 2. mark_ce_for_checkout_overlay(): Allow skip-worktree entries that have CE_UPDATE (set by update_some for tree entries) to still match the pathspec, so report_path_error() does not reject them. 3. checkout_worktree(): Skip checkout_entry() for entries that have both CE_MATCHED and CE_SKIP_WORKTREE, since the virtual filesystem provider will serve the correct content from the updated projection. The index is updated to the new tree entry's OID while the working tree write is skipped entirely. The virtual filesystem provider re-reads the updated index and serves the correct content on next access. Same approach as the CE_NEW_SKIP_WORKTREE propagation in deleted_entry() (unpack-trees.c, PR #865) which avoids unnecessary lstats on virtualized paths during branch switches. Assisted-by: Claude Opus 4.6 Signed-off-by: Tyrie Vella --- builtin/checkout.c | 34 ++++++++++++++++++++++- t/t1093-virtualfilesystem.sh | 53 ++++++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 1 deletion(-) diff --git a/builtin/checkout.c b/builtin/checkout.c index 0fc4a42ae960be..5cb073afeb3ebd 100644 --- a/builtin/checkout.c +++ b/builtin/checkout.c @@ -181,6 +181,24 @@ static int update_some(const struct object_id *oid, struct strbuf *base, discard_cache_entry(ce); return 0; } + + /* + * When a virtual filesystem is in use, preserve + * skip-worktree from the existing index entry. + * Without this, checkout_entry() would try to + * unlink() and recreate the file on disk, but + * virtual (projected) files have no physical NTFS + * entry and the unlink fails with ENOENT, causing + * the checkout to fail with exit code 255. + * + * Preserving skip-worktree lets the index update to + * the new tree entry's OID while skipping the + * working tree write. The virtual filesystem + * provider will serve the correct content from the + * updated projection on next access. + */ + if (core_virtualfilesystem && ce_skip_worktree(old)) + ce->ce_flags |= CE_SKIP_WORKTREE; } add_index_entry(the_repository->index, ce, @@ -343,7 +361,18 @@ static void mark_ce_for_checkout_overlay(struct cache_entry *ce, const struct checkout_opts *opts) { ce->ce_flags &= ~CE_MATCHED; - if (!opts->ignore_skipworktree && ce_skip_worktree(ce)) + if (!opts->ignore_skipworktree && ce_skip_worktree(ce) && + !(core_virtualfilesystem && opts->source_tree && + (ce->ce_flags & CE_UPDATE))) + /* + * Skip-worktree entries are normally excluded from + * pathspec matching. The exception is virtual + * filesystem entries updated from a source tree + * (CE_UPDATE set by update_some): those must still + * match so report_path_error() does not reject them. + * The actual worktree write is skipped later in + * checkout_worktree() because skip-worktree is set. + */ return; if (opts->source_tree && !(ce->ce_flags & CE_UPDATE)) /* @@ -421,6 +450,9 @@ static int checkout_worktree(const struct checkout_opts *opts, struct cache_entry *ce = the_repository->index->cache[pos]; if (ce->ce_flags & CE_MATCHED) { if (!ce_stage(ce)) { + if (core_virtualfilesystem && + ce_skip_worktree(ce)) + continue; errs |= checkout_entry(ce, &state, NULL, &nr_checkouts); continue; diff --git a/t/t1093-virtualfilesystem.sh b/t/t1093-virtualfilesystem.sh index 31c1aa77c6712e..31620d0c9e90da 100755 --- a/t/t1093-virtualfilesystem.sh +++ b/t/t1093-virtualfilesystem.sh @@ -419,6 +419,59 @@ test_expect_success 'checkout skips lstat for deleted skip-worktree entries in V git checkout side ' +test_expect_success 'checkout -- preserves skip-worktree in VFS mode' ' + # When "git checkout -- " updates the index with a + # different version of a file, update_some() creates a replacement + # cache entry. Without the fix, skip-worktree is cleared on the + # new entry, causing checkout_entry() to try unlink() + write on + # disk. For virtual files with no physical NTFS entry, the unlink + # fails with ENOENT and the command exits 255. + # + # With the fix, skip-worktree is preserved from the old index + # entry when core_virtualfilesystem is set. The index is updated + # to the tree entry OID, but checkout_entry() is skipped entirely. + clean_repo && + + test_when_finished "git -c core.virtualfilesystem= checkout main" && + + # Create a second commit with modified content + git -c core.virtualfilesystem= checkout -b checkout-path-test && + echo "modified content" >dir1/file1.txt && + git -c core.virtualfilesystem= add dir1/file1.txt && + git -c core.virtualfilesystem= commit -m "modify dir1/file1.txt" && + + # Record the OIDs for verification + git rev-parse HEAD:dir1/file1.txt >expect_new_oid && + git rev-parse HEAD~1:dir1/file1.txt >expect_old_oid && + + # Configure VFS hook that returns nothing (0% hydration). + # All entries keep skip-worktree set, simulating virtual files + # with no physical on-disk representation. + write_script .git/hooks/virtualfilesystem <<-\EOF && + printf "" + EOF + + # Remove the physical file to simulate a virtual placeholder. + # With the fix, checkout should update the index without + # touching the working tree (no file creation). + # Without the fix, checkout would clear skip-worktree and + # write the file to disk. + rm -f dir1/file1.txt && + + # Checkout the old version of the file from the parent commit. + git checkout HEAD~1 -- dir1/file1.txt && + + # Index should have the old (HEAD~1) OID + git ls-files -s dir1/file1.txt >actual_index && + grep "$(cat expect_old_oid)" actual_index && + + # The file should NOT have been written to disk — the fix + # preserves skip-worktree so checkout_entry() is skipped. + # Without the fix, checkout clears skip-worktree and writes + # the file to disk. + test_path_is_missing dir1/file1.txt +' + test_expect_success MINGW,FSMONITOR_DAEMON 'virtualfilesystem hook disables built-in FSMonitor' ' clean_repo && test_config core.usebuiltinfsmonitor true &&