From 37b6a5044937b88a76b77ecbecc35a5af7139f59 Mon Sep 17 00:00:00 2001 From: Tyrie Vella Date: Wed, 21 Jan 2026 15:34:13 -0800 Subject: [PATCH] Fix `restore --staged` for certain vfs ops When using virtualfilesystem, Running git restore --staged after git cherry-pick -n or git reset --soft would result in an incorrect state - the modified files would still have their modified contents, but git would no longer recognize them as being modified; and the added files would be deleted from disk instead of just unstaged. See microsoft/VFSForGit#1855 for more details. This commit fixes these issues with two changes: Add a new flag to the index state structure, vfs_check_added_entries_for_clear_skip_worktree. This flag is set during cherry-pick -n before calculating the new index state, and causes newly added entries to have their SKIP_WORKTREE bit cleared. This ensures that newly added files are added to both the index and the worktree in the next step. Otherwise, sparse-checkout would prevent them from being added to the worktree. Set the existing flag updated_skipworktree on the index when running a checkout index (aka restore --staged) operation. These operations already will clear SKIP_WORKTREE bits for modified files, but without this flag set the virtualfilesystem hook notification does not indicate that they changed, causing the VFS to not update its state correctly. --- builtin/checkout.c | 9 +++++++++ read-cache-ll.h | 3 ++- sequencer.c | 11 ++++++++++- unpack-trees.c | 2 ++ virtualfilesystem.c | 26 ++++++++++++++++++++++++++ 5 files changed, 49 insertions(+), 2 deletions(-) diff --git a/builtin/checkout.c b/builtin/checkout.c index 5109573aed32e9..750076eb17c045 100644 --- a/builtin/checkout.c +++ b/builtin/checkout.c @@ -652,6 +652,15 @@ static int checkout_paths(const struct checkout_opts *opts, checkout_index = opts->checkout_index; if (checkout_index) { + if (core_virtualfilesystem) { + /* Some scenarios that checkout the index may update skipworktree bits, + * such as `restore --staged` after `cherry-pick -n` or `reset --soft`, + * so this flag should be set to ensure the correct virtual filesystem + * event is sent. + */ + the_repository->index->updated_skipworktree = 1; + } + if (write_locked_index(the_repository->index, &lock_file, COMMIT_LOCK)) die(_("unable to write new index file")); } else { diff --git a/read-cache-ll.h b/read-cache-ll.h index 84092540a7830b..bcdeca9dacdf66 100644 --- a/read-cache-ll.h +++ b/read-cache-ll.h @@ -176,7 +176,8 @@ struct index_state { drop_cache_tree : 1, updated_workdir : 1, updated_skipworktree : 1, - fsmonitor_has_run_once : 1; + fsmonitor_has_run_once : 1, + vfs_check_added_entries_for_clear_skip_worktree : 1; enum sparse_index_mode sparse_index; struct hashmap name_hash; struct hashmap dir_hash; diff --git a/sequencer.c b/sequencer.c index 05a77977199e3d..36a124c502222e 100644 --- a/sequencer.c +++ b/sequencer.c @@ -787,13 +787,22 @@ static int do_recursive_merge(struct repository *r, * to be replace with the tree the index matched before we * started doing any picks. */ + if (opts->no_commit && core_virtualfilesystem) { + /* When using the virtual file system, staged new files + * should clear SKIP_WORKTREE during this step to ensure the new files + * are properly added to the working tree as well as index - otherwise + * sparse-checkout functionality will prevent them from being added. + */ + o.repo->index->vfs_check_added_entries_for_clear_skip_worktree = 1; + } merge_switch_to_result(&o, head_tree, &result, 1, show_output); + o.repo->index->vfs_check_added_entries_for_clear_skip_worktree = 0; + clean = result.clean; if (clean < 0) { rollback_lock_file(&index_lock); return clean; } - if (write_locked_index(r->index, &index_lock, COMMIT_LOCK | SKIP_IF_UNCHANGED)) /* diff --git a/unpack-trees.c b/unpack-trees.c index 4d897829419a3d..76316d54e18a5b 100644 --- a/unpack-trees.c +++ b/unpack-trees.c @@ -2004,6 +2004,8 @@ int unpack_trees(unsigned len, struct tree_desc *t, struct unpack_trees_options is_sparse_index_allowed(&o->internal.result, 0)) o->internal.result.sparse_index = 1; + o->internal.result.vfs_check_added_entries_for_clear_skip_worktree = + o->src_index->vfs_check_added_entries_for_clear_skip_worktree; /* * Sparse checkout loop #1: set NEW_SKIP_WORKTREE on existing entries */ diff --git a/virtualfilesystem.c b/virtualfilesystem.c index 269af2de1d969d..7617e87e360d17 100644 --- a/virtualfilesystem.c +++ b/virtualfilesystem.c @@ -337,6 +337,32 @@ static void clear_ce_flags_virtualfilesystem_1(struct index_state *istate, int s entry += len + 1; } } + + /* + * If vfs_check_added_entries_for_clear_skip_worktree is set and we are checking + * for added entries, clear the mask from all added entries even if they + * are not in the virtual filesystem. + * This is used in scenarios like cherry-pick -n, where the added entries + * are not added to the virtual file system but still need to be checked out + * in the working tree. + */ + if ((select_mask & CE_ADDED) + && (clear_mask & CE_SKIP_WORKTREE) + && istate->vfs_check_added_entries_for_clear_skip_worktree) { + for (i = 0; i < istate->cache_nr; i++) { + struct cache_entry *ce = istate->cache[i]; + if (!select_mask || (ce->ce_flags & select_mask)) { + if (ce->ce_flags & clear_mask) { + ce->ce_flags &= ~clear_mask; + /* + * We also signal to VFS that there are updates to skipworktree + * that it needs to react to. + */ + istate->updated_skipworktree = 1; + } + } + } + } } /*