Skip to content

Commit ff944c1

Browse files
authored
Merge pull request microsoft#1900 from tyrielv/tyrielv/delete-folder-then-restore-functional-test
Fix for delete/restore issue
2 parents 44b6d6b + 4fb56b4 commit ff944c1

5 files changed

Lines changed: 75 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: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1014,7 +1014,25 @@ 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+
//
1022+
// This check is safe from races with the background task that updates
1023+
// ModifiedPaths: the deletion happens from a non-git process (e.g.,
1024+
// rmdir), and IsReadyForExternalAcquireLockRequests() blocks git from
1025+
// acquiring the GVFS lock until the background queue is drained. When
1026+
// git itself deletes a folder, the code takes the IsValidGitCommand
1027+
// path in OnWorkingDirectoryFileOrFolderDeleteNotification and calls
1028+
// OnPossibleTombstoneFolderCreated instead of OnFolderDeleted, so
1029+
// ModifiedPaths is not involved.
1030+
//
1031+
// See https://github.com/microsoft/VFSForGit/issues/1901
1032+
if (!this.FileSystemCallbacks.IsPathOrParentInModifiedPaths(virtualPath, isFolder: true))
1033+
{
1034+
this.MarkDirectoryAsPlaceholder(virtualPath, triggeringProcessId, triggeringProcessImageFileName);
1035+
}
10181036
}
10191037
else
10201038
{

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)