Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 32 additions & 1 deletion builtin/checkout.c
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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))
/*
Expand Down Expand Up @@ -421,6 +450,8 @@ 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 (ce_skip_worktree(ce))
continue;
errs |= checkout_entry(ce, &state,
NULL, &nr_checkouts);
continue;
Expand Down
53 changes: 53 additions & 0 deletions t/t1093-virtualfilesystem.sh
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,59 @@ test_expect_success 'checkout skips lstat for deleted skip-worktree entries in V
git checkout side
'

test_expect_success 'checkout <tree> -- <path> preserves skip-worktree in VFS mode' '
# When "git checkout <tree> -- <path>" 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 &&
Expand Down
Loading