Skip to content

Commit 175686d

Browse files
tyrielvCopilot
authored andcommitted
Fix git restore after deleting directory with nested subdirs
Add functional test to reproduce issue #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 #1901 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 7c1ba07 commit 175686d

5 files changed

Lines changed: 65 additions & 1 deletion

File tree

GVFS/GVFS.FunctionalTests/Tests/GitCommands/CorruptionReproTests.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,5 +75,24 @@ public void ReproCherryPickRestoreCorruption()
7575
this.ValidateGitCommand("restore -- .");
7676
this.FilesShouldMatchCheckoutOfSourceBranch();
7777
}
78+
79+
/// <summary>
80+
/// Reproduction of a reported issue:
81+
/// Restoring a file after its parent directory was deleted fails with
82+
/// "fatal: could not unlink 'path\to\': Directory not empty"
83+
///
84+
/// See https://github.com/microsoft/VFSForGit/issues/1901
85+
/// </summary>
86+
[TestCase]
87+
public void RestoreAfterDeleteNesteredDirectory()
88+
{
89+
// Delete a directory with nested subdirectories and files.
90+
this.ValidateNonGitCommand("cmd.exe", "/c \"rmdir /s /q GVFlt_DeleteFileTest\"");
91+
92+
// Restore the working directory.
93+
this.ValidateGitCommand("restore .");
94+
95+
this.FilesShouldMatchCheckoutOfSourceBranch();
96+
}
7897
}
7998
}

GVFS/GVFS.FunctionalTests/Tests/GitCommands/GitRepoTests.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,23 @@ protected void ValidateGitCommand(string command, params object[] args)
269269
args);
270270
}
271271

272+
protected void ValidateNonGitCommand(string command, string args = "", bool ignoreErrors = false, bool checkStatus = true)
273+
{
274+
string controlRepoRoot = this.ControlGitRepo.RootPath;
275+
string gvfsRepoRoot = this.Enlistment.RepoRoot;
276+
277+
ProcessResult expectedResult = ProcessHelper.Run(command, args, controlRepoRoot);
278+
ProcessResult actualResult = ProcessHelper.Run(command, args, gvfsRepoRoot);
279+
if (!ignoreErrors)
280+
{
281+
GitHelpers.ErrorsShouldMatch(command, expectedResult, actualResult);
282+
}
283+
if (checkStatus)
284+
{
285+
this.ValidateGitCommand("status");
286+
}
287+
}
288+
272289
protected void ChangeMode(string filePath, ushort mode)
273290
{
274291
string virtualFile = Path.Combine(this.Enlistment.RepoRoot, filePath);

GVFS/GVFS.FunctionalTests/Tools/ProcessHelper.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ namespace GVFS.FunctionalTests.Tools
66
public static class ProcessHelper
77
{
88
public static ProcessResult Run(string fileName, string arguments)
9+
{
10+
return Run(fileName, arguments, null);
11+
}
12+
13+
public static ProcessResult Run(string fileName, string arguments, string workingDirectory)
914
{
1015
ProcessStartInfo startInfo = new ProcessStartInfo();
1116
startInfo.UseShellExecute = false;
@@ -14,6 +19,10 @@ public static ProcessResult Run(string fileName, string arguments)
1419
startInfo.CreateNoWindow = true;
1520
startInfo.FileName = fileName;
1621
startInfo.Arguments = arguments;
22+
if (!string.IsNullOrEmpty(workingDirectory))
23+
{
24+
startInfo.WorkingDirectory = workingDirectory;
25+
}
1726

1827
return Run(startInfo);
1928
}

GVFS/GVFS.Platform.Windows/WindowsFileSystemVirtualizer.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1014,7 +1014,15 @@ private void NotifyNewFileCreatedHandler(
10141014
GitCommandLineParser gitCommand = new GitCommandLineParser(this.Context.Repository.GVFSLock.GetLockedGitCommand());
10151015
if (gitCommand.IsValidGitCommand)
10161016
{
1017-
this.MarkDirectoryAsPlaceholder(virtualPath, triggeringProcessId, triggeringProcessImageFileName);
1017+
// When git recreates a directory that was previously deleted (and is
1018+
// tracked in ModifiedPaths), skip marking it as a ProjFS placeholder.
1019+
// Otherwise ProjFS would immediately project all children into it,
1020+
// conflicting with git's own attempt to populate the directory.
1021+
// See https://github.com/microsoft/VFSForGit/issues/1901
1022+
if (!this.FileSystemCallbacks.IsPathOrParentInModifiedPaths(virtualPath, isFolder: true))
1023+
{
1024+
this.MarkDirectoryAsPlaceholder(virtualPath, triggeringProcessId, triggeringProcessImageFileName);
1025+
}
10181026
}
10191027
else
10201028
{

GVFS/GVFS.Virtualization/FileSystemCallbacks.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,17 @@ public IEnumerable<string> GetAllModifiedPaths()
367367
return this.modifiedPaths.GetAllModifiedPaths();
368368
}
369369

370+
/// <summary>
371+
/// Checks whether the given folder path, or any of its parent folders,
372+
/// is in the ModifiedPaths database. Used to determine if git/user has
373+
/// taken ownership of a directory tree.
374+
/// </summary>
375+
public bool IsPathOrParentInModifiedPaths(string path, bool isFolder)
376+
{
377+
return this.modifiedPaths.Contains(path, isFolder) ||
378+
this.modifiedPaths.ContainsParentFolder(path, out _);
379+
}
380+
370381
/// <summary>
371382
/// Finds index entries that are staged (differ from HEAD) matching the given
372383
/// pathspec, and adds them to ModifiedPaths. This prepares for an unstage operation

0 commit comments

Comments
 (0)