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 &&