Draft
Conversation
dffce20 to
175686d
Compare
Add functional test to reproduce issue microsoft#1901: running 'git restore .' after deleting a directory with nested subdirectories fails with 'fatal: cannot create directory: Directory not empty'. Root cause: when git recreates a deleted directory, GVFS's NotifyNewFileCreated handler calls MarkDirectoryAsPlaceholder(), which causes ProjFS to immediately project all children back into the directory. Git then fails when it tries to create subdirectories that ProjFS has already auto-projected. Fix: skip MarkDirectoryAsPlaceholder() for directories whose path (or a parent path) is already in ModifiedPaths, indicating git/user has taken ownership. The directory stays non-virtualized so git can populate it directly without ProjFS interference. Fixes microsoft#1901 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
175686d to
90fdd5b
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fix
git restoreafter deleting directory with nested subdirsFixes #1901
Problem
Running
git restore .after deleting a directory withrmdir /s /qfails with:This affects
git restore(both.and individual file paths),git checkout -- <dir>, and any directory with nested subdirectories — even flat directories with only files.git reset --hardandgit checkout <commit>are not affected.Root Cause
When git recreates a deleted directory during
git restore, ProjFS firesNotifyNewFileCreated. GVFS's handler unconditionally callsMarkDirectoryAsPlaceholder(), which tells ProjFS to virtualize the directory. ProjFS then immediately calls back into GVFS's enumeration callbacks to project all children — recreating subdirectories and files as placeholders before git has a chance to populate them itself.Git then tries to create those same subdirectories and fails because they already exist (and are non-empty, since ProjFS recursively projected their contents too).
Trace of the failing sequence
Fix
Skip
MarkDirectoryAsPlaceholder()when the directory (or a parent) is already in the ModifiedPaths database. A folder in ModifiedPaths means git/user has taken ownership of that path (e.g., by deleting it). Leaving the directory as a regular (non-virtualized) directory allows git to populate it directly without ProjFS interference.Changes
WindowsFileSystemVirtualizer.cs— InNotifyNewFileCreatedHandler, guard theMarkDirectoryAsPlaceholder()call with anIsPathOrParentInModifiedPathscheck.FileSystemCallbacks.cs— AddIsPathOrParentInModifiedPaths()helper that checks theModifiedPathsDatabasefor the path and its ancestors.CorruptionReproTests.cs— AddRestoreAfterDeleteNesteredDirectoryfunctional test reproducing the issue.GitRepoTests.cs— AddValidateNonGitCommandhelper for running non-git commands (e.g.,rmdir) against both control and GVFS repos.ProcessHelper.cs— AddRunoverload accepting a working directory.Testing
Functional test
RestoreAfterDeleteNesteredDirectory:GVFlt_DeleteFileTest/(which has 14 subdirectories, some deeply nested) viarmdir /s /qgit restore .Also manually verified that existing scenarios continue to work:
git reset --hardafter directory deletiongit checkout <commit>after committing a deletiongit checkout -- .after creating a new file in a directorySynchronization Safety
The
IsPathOrParentInModifiedPathscheck relies on ModifiedPaths being updated beforeNotifyNewFileCreatedHandlerfires. ModifiedPaths is updated asynchronously via a background task queue, which raises a potential race condition concern. However, this is safe because:Cross-process case (
rmdirthengit restore): When git starts, its pre-command hook requests the GVFS lock.IsReadyForExternalAcquireLockRequests()checksbackgroundFileSystemTaskRunner.IsEmptyand denies the lock until the queue is drained. So ModifiedPaths is always up-to-date before git'sNotifyNewFileCreatedcan fire.Within a single git command: When git itself deletes and recreates a directory,
OnWorkingDirectoryFileOrFolderDeleteNotificationtakes thegitCommand.IsValidGitCommandbranch and callsOnPossibleTombstoneFolderCreatedinstead ofOnFolderDeleted— ModifiedPaths is not involved, and the fix is not triggered.The fix only activates when a directory is in ModifiedPaths because a non-git process deleted it, and a subsequent git command recreates it — with a lock boundary guaranteeing ModifiedPaths is current.