diff --git a/GVFS/GVFS.Common/ModifiedPathsDatabase.cs b/GVFS/GVFS.Common/ModifiedPathsDatabase.cs index 043a1e3bc..ffde4f692 100644 --- a/GVFS/GVFS.Common/ModifiedPathsDatabase.cs +++ b/GVFS/GVFS.Common/ModifiedPathsDatabase.cs @@ -75,30 +75,39 @@ public bool TryAdd(string path, bool isFolder, out bool isRetryable) } catch (IOException e) { - if (this.Tracer != null) - { - EventMetadata metadata = new EventMetadata(); - metadata.Add("Area", "ModifiedPathsDatabase"); - metadata.Add(nameof(entry), entry); - metadata.Add(nameof(isFolder), isFolder); - metadata.Add("Exception", e.ToString()); - this.Tracer.RelatedWarning(metadata, $"IOException caught while processing {nameof(this.TryAdd)}"); - } - + this.TraceWarning(isFolder, entry, e, nameof(this.TryAdd)); return false; } catch (Exception e) { - if (this.Tracer != null) - { - EventMetadata metadata = new EventMetadata(); - metadata.Add("Area", "ModifiedPathsDatabase"); - metadata.Add(nameof(entry), entry); - metadata.Add(nameof(isFolder), isFolder); - metadata.Add("Exception", e.ToString()); - this.Tracer.RelatedError(metadata, $"Exception caught while processing {nameof(this.TryAdd)}"); - } + this.TraceError(isFolder, entry, e, nameof(this.TryAdd)); + isRetryable = false; + return false; + } + } + return true; + } + + public bool TryRemove(string path, bool isFolder, out bool isRetryable) + { + isRetryable = true; + string entry = this.NormalizeEntryString(path, isFolder); + if (this.modifiedPaths.Contains(entry)) + { + isRetryable = true; + try + { + this.WriteRemoveEntry(entry, () => this.modifiedPaths.TryRemove(entry)); + } + catch (IOException e) + { + this.TraceWarning(isFolder, entry, e, nameof(this.TryRemove)); + return false; + } + catch (Exception e) + { + this.TraceError(isFolder, entry, e, nameof(this.TryRemove)); isRetryable = false; return false; } @@ -107,6 +116,58 @@ public bool TryAdd(string path, bool isFolder, out bool isRetryable) return true; } + public void WriteAllEntriesAndFlush() + { + try + { + this.WriteAndReplaceDataFile(this.GenerateDataLines); + } + catch (Exception e) + { + throw new FileBasedCollectionException(e); + } + } + + private static EventMetadata CreateEventMetadata(bool isFolder, string entry, Exception e) + { + EventMetadata metadata = new EventMetadata(); + metadata.Add("Area", "ModifiedPathsDatabase"); + metadata.Add(nameof(entry), entry); + metadata.Add(nameof(isFolder), isFolder); + if (e != null) + { + metadata.Add("Exception", e.ToString()); + } + + return metadata; + } + + private IEnumerable GenerateDataLines() + { + foreach (string entry in this.modifiedPaths) + { + yield return this.FormatAddLine(entry); + } + } + + private void TraceWarning(bool isFolder, string entry, Exception e, string method) + { + if (this.Tracer != null) + { + EventMetadata metadata = CreateEventMetadata(isFolder, entry, e); + this.Tracer.RelatedWarning(metadata, $"{e.GetType().Name} caught while processing {method}"); + } + } + + private void TraceError(bool isFolder, string entry, Exception e, string method) + { + if (this.Tracer != null) + { + EventMetadata metadata = CreateEventMetadata(isFolder, entry, e); + this.Tracer.RelatedError(metadata, $"{e.GetType().Name} caught while processing {method}"); + } + } + private bool TryParseAddLine(string line, out string key, out string value, out string error) { key = line; diff --git a/GVFS/GVFS.FunctionalTests.Windows/Windows/Tests/DiskLayoutUpgradeTests.cs b/GVFS/GVFS.FunctionalTests.Windows/Windows/Tests/DiskLayoutUpgradeTests.cs index d3ea1f93b..eb1ce473d 100644 --- a/GVFS/GVFS.FunctionalTests.Windows/Windows/Tests/DiskLayoutUpgradeTests.cs +++ b/GVFS/GVFS.FunctionalTests.Windows/Windows/Tests/DiskLayoutUpgradeTests.cs @@ -324,24 +324,30 @@ public void MountCreatesModifiedPathsDatabase() this.Enlistment.MountGVFS(); this.Enlistment.UnmountGVFS(); - string expectedModifiedPaths = @"A .gitattributes -A developer/me/ -A developer/me/JLANGE9._prerazzle -A developer/me/StateSwitch.Save -A tools/x86/remote.exe -A tools/x86/runelevated.exe -A tools/amd64/remote.exe -A tools/amd64/runelevated.exe -A tools/perllib/MS/TraceLogging.dll -A tools/managed/v2.0/midldd.CheckedInExe -A tools/managed/v4.0/sdapi.dll -A tools/managed/v2.0/midlpars.dll -A tools/managed/v2.0/RPCDataSupport.dll -A tools/managed/v2.0/MidlStaticAnalysis.dll -A tools/perllib/MS/Somefile.txt -"; - - modifiedPathsDatabasePath.ShouldBeAFile(this.fileSystem).WithContents(expectedModifiedPaths); + string[] expectedModifiedPaths = + { + "A .gitattributes", + "A developer/me/", + "A developer/me/JLANGE9._prerazzle", + "A developer/me/StateSwitch.Save", + "A tools/x86/remote.exe", + "A tools/x86/runelevated.exe", + "A tools/amd64/remote.exe", + "A tools/amd64/runelevated.exe", + "A tools/perllib/MS/TraceLogging.dll", + "A tools/managed/v2.0/midldd.CheckedInExe", + "A tools/managed/v4.0/sdapi.dll", + "A tools/managed/v2.0/midlpars.dll", + "A tools/managed/v2.0/RPCDataSupport.dll", + "A tools/managed/v2.0/MidlStaticAnalysis.dll", + "A tools/perllib/MS/Somefile.txt", + }; + + modifiedPathsDatabasePath.ShouldBeAFile(this.fileSystem); + this.fileSystem.ReadAllText(modifiedPathsDatabasePath) + .Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries) + .OrderBy(x => x) + .ShouldMatchInOrder(expectedModifiedPaths.OrderBy(x => x)); this.ValidatePersistedVersionMatchesCurrentVersion(); } diff --git a/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/GitFilesTests.cs b/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/GitFilesTests.cs index eca57a1c2..bfbdcc457 100644 --- a/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/GitFilesTests.cs +++ b/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/GitFilesTests.cs @@ -88,7 +88,6 @@ public void RenameEmptyFolderTest() string renamedFolderName = "folder3b"; string[] expectedModifiedEntries = { - folderName + "/", renamedFolderName + "/", }; @@ -110,19 +109,14 @@ public void RenameFolderTest() string[] fileNames = { "a", "b", "c" }; string[] expectedModifiedEntries = { - renamedFolderName + "/" + fileNames[0], - renamedFolderName + "/" + fileNames[1], - renamedFolderName + "/" + fileNames[2], - folderName + "/" + fileNames[0], - folderName + "/" + fileNames[1], - folderName + "/" + fileNames[2], + renamedFolderName + "/", }; this.Enlistment.GetVirtualPathTo(folderName).ShouldNotExistOnDisk(this.fileSystem); this.fileSystem.CreateDirectory(this.Enlistment.GetVirtualPathTo(folderName)); foreach (string fileName in fileNames) { - string filePath = folderName + "\\" + fileName; + string filePath = Path.Combine(folderName, fileName); this.fileSystem.CreateEmptyFile(this.Enlistment.GetVirtualPathTo(filePath)); this.Enlistment.GetVirtualPathTo(filePath).ShouldBeAFile(this.fileSystem); } @@ -138,22 +132,32 @@ public void RenameFolderTest() [Category(Categories.MacTODO.M2)] public void CaseOnlyRenameOfNewFolderKeepsModifiedPathsEntries() { - string[] expectedModifiedPathsEntries = + if (this.fileSystem is PowerShellRunner) + { + Assert.Ignore("Powershell does not support case only renames."); + } + + string[] expectedModifiedPathsEntriesAfterCreate = + { + "A Folder/", + "A Folder/testfile", + }; + + string[] expectedModifiedPathsEntriesAfterRename = { - "Folder/", - "Folder/testfile", + "A folder/", }; this.fileSystem.CreateDirectory(Path.Combine(this.Enlistment.RepoRoot, "Folder")); this.fileSystem.CreateEmptyFile(Path.Combine(this.Enlistment.RepoRoot, "Folder", "testfile")); this.Enlistment.WaitForBackgroundOperations().ShouldEqual(true, "Background operations failed to complete."); - GVFSHelpers.ModifiedPathsShouldContain(this.fileSystem, this.Enlistment.DotGVFSRoot, expectedModifiedPathsEntries); + GVFSHelpers.ModifiedPathsShouldContain(this.fileSystem, this.Enlistment.DotGVFSRoot, expectedModifiedPathsEntriesAfterCreate); this.fileSystem.RenameDirectory(this.Enlistment.RepoRoot, "Folder", "folder"); this.Enlistment.WaitForBackgroundOperations().ShouldEqual(true, "Background operations failed to complete."); - GVFSHelpers.ModifiedPathsShouldContain(this.fileSystem, this.Enlistment.DotGVFSRoot, expectedModifiedPathsEntries); + GVFSHelpers.ModifiedPathsShouldContain(this.fileSystem, this.Enlistment.DotGVFSRoot, expectedModifiedPathsEntriesAfterRename); } [TestCase, Order(7)] diff --git a/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerTestCase/ModifiedPathsTests.cs b/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerTestCase/ModifiedPathsTests.cs index c57bffccd..e3e9de6a5 100644 --- a/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerTestCase/ModifiedPathsTests.cs +++ b/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerTestCase/ModifiedPathsTests.cs @@ -3,7 +3,9 @@ using GVFS.FunctionalTests.Tools; using GVFS.Tests.Should; using NUnit.Framework; +using System; using System.IO; +using System.Linq; namespace GVFS.FunctionalTests.Tests.EnlistmentPerTestCase { @@ -23,26 +25,62 @@ public class ModifiedPathsTests : TestsWithEnlistmentPerTestCase private static readonly string FileToCreateOutsideRepo = $"{nameof(ModifiedPathsTests)}_outsideRepo.txt"; private static readonly string FolderToCreateOutsideRepo = $"{nameof(ModifiedPathsTests)}_outsideFolder"; private static readonly string FolderToDelete = "Scripts"; - private static readonly string ExpectedModifiedFilesContentsAfterRemount = -$@"A .gitattributes -A {GVFSHelpers.ConvertPathToGitFormat(FileToAdd)} -A {GVFSHelpers.ConvertPathToGitFormat(FileToUpdate)} -A {FileToDelete} -A {GVFSHelpers.ConvertPathToGitFormat(FileToRename)} -A {GVFSHelpers.ConvertPathToGitFormat(RenameFileTarget)} -A {FolderToCreate}/ -A {FolderToRename}/ -A {RenameFolderTarget}/ -A {RenameNewDotGitFileTarget} -A {FileToCreateOutsideRepo} -A {FolderToCreateOutsideRepo}/ -A {FolderToDelete}/CreateCommonAssemblyVersion.bat -A {FolderToDelete}/CreateCommonCliAssemblyVersion.bat -A {FolderToDelete}/CreateCommonVersionHeader.bat -A {FolderToDelete}/RunFunctionalTests.bat -A {FolderToDelete}/RunUnitTests.bat -A {FolderToDelete}/ -"; + private static readonly string[] ExpectedModifiedFilesContentsAfterRemount = + { + $"A .gitattributes", + $"A {GVFSHelpers.ConvertPathToGitFormat(FileToAdd)}", + $"A {GVFSHelpers.ConvertPathToGitFormat(FileToUpdate)}", + $"A {FileToDelete}", + $"A {GVFSHelpers.ConvertPathToGitFormat(FileToRename)}", + $"A {GVFSHelpers.ConvertPathToGitFormat(RenameFileTarget)}", + $"A {FolderToCreate}/", + $"A {RenameNewDotGitFileTarget}", + $"A {FileToCreateOutsideRepo}", + $"A {FolderToCreateOutsideRepo}/", + $"A {FolderToDelete}/CreateCommonAssemblyVersion.bat", + $"A {FolderToDelete}/CreateCommonCliAssemblyVersion.bat", + $"A {FolderToDelete}/CreateCommonVersionHeader.bat", + $"A {FolderToDelete}/RunFunctionalTests.bat", + $"A {FolderToDelete}/RunUnitTests.bat", + $"A {FolderToDelete}/", + }; + + [TestCaseSource(typeof(FileSystemRunner), FileSystemRunner.TestRunners)] + public void DeletedTempFileIsRemovedFromModifiedFiles(FileSystemRunner fileSystem) + { + string tempFile = this.CreateFile(fileSystem, "temp.txt"); + fileSystem.DeleteFile(tempFile); + tempFile.ShouldNotExistOnDisk(fileSystem); + + this.Enlistment.UnmountGVFS(); + this.ValidateModifiedPathsDoNotContain(fileSystem, "temp.txt"); + } + + [TestCaseSource(typeof(FileSystemRunner), FileSystemRunner.TestRunners)] + public void DeletedTempFolderIsRemovedFromModifiedFiles(FileSystemRunner fileSystem) + { + string tempFolder = this.CreateDirectory(fileSystem, "Temp"); + fileSystem.DeleteDirectory(tempFolder); + tempFolder.ShouldNotExistOnDisk(fileSystem); + + this.Enlistment.UnmountGVFS(); + this.ValidateModifiedPathsDoNotContain(fileSystem, "Temp/"); + } + + [TestCaseSource(typeof(FileSystemRunner), FileSystemRunner.TestRunners)] + public void DeletedTempFolderDeletesFilesFromModifiedFiles(FileSystemRunner fileSystem) + { + string tempFolder = this.CreateDirectory(fileSystem, "Temp"); + string tempFile1 = this.CreateFile(fileSystem, Path.Combine("Temp", "temp1.txt")); + string tempFile2 = this.CreateFile(fileSystem, Path.Combine("Temp", "temp2.txt")); + fileSystem.DeleteDirectory(tempFolder); + tempFolder.ShouldNotExistOnDisk(fileSystem); + tempFile1.ShouldNotExistOnDisk(fileSystem); + tempFile2.ShouldNotExistOnDisk(fileSystem); + + this.Enlistment.UnmountGVFS(); + this.ValidateModifiedPathsDoNotContain(fileSystem, "Temp/", "Temp/temp1.txt", "Temp/temp2.txt"); + } [Category(Categories.MacTODO.M2)] [TestCaseSource(typeof(FileSystemRunner), FileSystemRunner.TestRunners)] @@ -69,7 +107,7 @@ public void ModifiedPathsSavedAfterRemount(FileSystemRunner fileSystem) string folderToRenameTarget = this.Enlistment.GetVirtualPathTo(RenameFolderTarget); fileSystem.MoveDirectory(folderToRename, folderToRenameTarget); - // Moving the new folder out of the repo should not change the modified paths + // Moving the new folder out of the repo will remove it from the modified paths file string folderTargetOutsideSrc = Path.Combine(this.Enlistment.EnlistmentRoot, RenameFolderTarget); folderTargetOutsideSrc.ShouldNotExistOnDisk(fileSystem); fileSystem.MoveDirectory(folderToRenameTarget, folderTargetOutsideSrc); @@ -114,18 +152,20 @@ public void ModifiedPathsSavedAfterRemount(FileSystemRunner fileSystem) modifiedPathsDatabase.ShouldBeAFile(fileSystem); using (StreamReader reader = new StreamReader(File.Open(modifiedPathsDatabase, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))) { - reader.ReadToEnd().ShouldEqual(ExpectedModifiedFilesContentsAfterRemount); + reader.ReadToEnd().Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries).OrderBy(x => x) + .ShouldMatchInOrder(ExpectedModifiedFilesContentsAfterRemount.OrderBy(x => x)); } } [TestCaseSource(typeof(FileSystemRunner), FileSystemRunner.TestRunners)] public void ModifiedPathsCorrectAfterHardLinking(FileSystemRunner fileSystem) { - const string ExpectedModifiedFilesContentsAfterHardlinks = -@"A .gitattributes -A LinkToReadme.md -A LinkToFileOutsideSrc.txt -"; + string[] expectedModifiedFilesContentsAfterHardlinks = + { + "A .gitattributes", + "A LinkToReadme.md", + "A LinkToFileOutsideSrc.txt", + }; // Create a link from src\LinkToReadme.md to src\Readme.md string existingFileInRepoPath = this.Enlistment.GetVirtualPathTo("Readme.md"); @@ -157,8 +197,31 @@ A LinkToFileOutsideSrc.txt modifiedPathsDatabase.ShouldBeAFile(fileSystem); using (StreamReader reader = new StreamReader(File.Open(modifiedPathsDatabase, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))) { - reader.ReadToEnd().ShouldEqual(ExpectedModifiedFilesContentsAfterHardlinks); + reader.ReadToEnd().Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries).OrderBy(x => x) + .ShouldMatchInOrder(expectedModifiedFilesContentsAfterHardlinks.OrderBy(x => x)); } } + + private string CreateDirectory(FileSystemRunner fileSystem, string relativePath) + { + string tempFolder = this.Enlistment.GetVirtualPathTo(relativePath); + fileSystem.CreateDirectory(tempFolder); + tempFolder.ShouldBeADirectory(fileSystem); + return tempFolder; + } + + private string CreateFile(FileSystemRunner fileSystem, string relativePath) + { + string tempFile = this.Enlistment.GetVirtualPathTo(relativePath); + fileSystem.WriteAllText(tempFile, $"Contents for the {relativePath} file"); + tempFile.ShouldBeAFile(fileSystem); + return tempFile; + } + + private void ValidateModifiedPathsDoNotContain(FileSystemRunner fileSystem, params string[] paths) + { + GVFSHelpers.ModifiedPathsShouldNotContain(fileSystem, this.Enlistment.DotGVFSRoot, paths.Select(x => $"A {x}" + Environment.NewLine).ToArray()); + GVFSHelpers.ModifiedPathsShouldNotContain(fileSystem, this.Enlistment.DotGVFSRoot, paths.Select(x => $"D {x}" + Environment.NewLine).ToArray()); + } } } diff --git a/GVFS/GVFS.FunctionalTests/Tests/GitCommands/GitCommandsTests.cs b/GVFS/GVFS.FunctionalTests/Tests/GitCommands/GitCommandsTests.cs index 0b3ccccce..c623f5ea7 100644 --- a/GVFS/GVFS.FunctionalTests/Tests/GitCommands/GitCommandsTests.cs +++ b/GVFS/GVFS.FunctionalTests/Tests/GitCommands/GitCommandsTests.cs @@ -1041,7 +1041,7 @@ private void BasicCommit(Action fileSystemAction, string addCommand, [CallerMemb fileSystemAction(); this.ValidateGitCommand("status"); this.ValidateGitCommand(addCommand); - this.RunGitCommand("commit -m \"BasicCommit for {test}\""); + this.RunGitCommand($"commit -m \"BasicCommit for {test}\""); } private void SwitchBranch(Action fileSystemAction, [CallerMemberName]string test = GitCommandsTests.UnknownTestName) diff --git a/GVFS/GVFS.Platform.Mac/MacFileSystemVirtualizer.cs b/GVFS/GVFS.Platform.Mac/MacFileSystemVirtualizer.cs index 3d22f7f34..127173d0e 100644 --- a/GVFS/GVFS.Platform.Mac/MacFileSystemVirtualizer.cs +++ b/GVFS/GVFS.Platform.Mac/MacFileSystemVirtualizer.cs @@ -352,7 +352,7 @@ private Result OnPreDelete(string relativePath, bool isDirectory) } else { - this.OnWorkingDirectoryFileOrFolderDeleted(relativePath, isDirectory); + this.OnWorkingDirectoryFileOrFolderDeleteNotification(relativePath, isDirectory, isPreDelete: true); } } catch (Exception e) diff --git a/GVFS/GVFS.Platform.Windows/WindowsFileSystemVirtualizer.cs b/GVFS/GVFS.Platform.Windows/WindowsFileSystemVirtualizer.cs index 42c1f109a..de1963b99 100644 --- a/GVFS/GVFS.Platform.Windows/WindowsFileSystemVirtualizer.cs +++ b/GVFS/GVFS.Platform.Windows/WindowsFileSystemVirtualizer.cs @@ -1296,7 +1296,7 @@ private void NotifyFileHandleClosedFileModifiedOrDeletedHandler( } else { - this.OnWorkingDirectoryFileOrFolderDeleted(virtualPath, isDirectory); + this.OnWorkingDirectoryFileOrFolderDeleteNotification(virtualPath, isDirectory, isPreDelete: false); } } } diff --git a/GVFS/GVFS.Virtualization/Background/FileSystemTask.cs b/GVFS/GVFS.Virtualization/Background/FileSystemTask.cs index b5644bf86..eaab198a4 100644 --- a/GVFS/GVFS.Virtualization/Background/FileSystemTask.cs +++ b/GVFS/GVFS.Virtualization/Background/FileSystemTask.cs @@ -29,7 +29,9 @@ public enum OperationType OnFolderFirstWrite, OnIndexWriteWithoutProjectionChange, OnPlaceholderCreationsBlockedForGit, - OnFileHardLinkCreated + OnFileHardLinkCreated, + OnFilePreDelete, + OnFolderPreDelete, } public OperationType Operation { get; } @@ -57,6 +59,11 @@ public static FileSystemTask OnFileDeleted(string virtualPath) return new FileSystemTask(OperationType.OnFileDeleted, virtualPath, oldVirtualPath: null); } + public static FileSystemTask OnFilePreDelete(string virtualPath) + { + return new FileSystemTask(OperationType.OnFilePreDelete, virtualPath, oldVirtualPath: null); + } + public static FileSystemTask OnFileOverwritten(string virtualPath) { return new FileSystemTask(OperationType.OnFileOverwritten, virtualPath, oldVirtualPath: null); @@ -97,6 +104,11 @@ public static FileSystemTask OnFolderDeleted(string virtualPath) return new FileSystemTask(OperationType.OnFolderDeleted, virtualPath, oldVirtualPath: null); } + public static FileSystemTask OnFolderPreDelete(string virtualPath) + { + return new FileSystemTask(OperationType.OnFolderPreDelete, virtualPath, oldVirtualPath: null); + } + public static FileSystemTask OnIndexWriteWithoutProjectionChange() { return new FileSystemTask(OperationType.OnIndexWriteWithoutProjectionChange, virtualPath: null, oldVirtualPath: null); diff --git a/GVFS/GVFS.Virtualization/FileSystem/FileSystemVirtualizer.cs b/GVFS/GVFS.Virtualization/FileSystem/FileSystemVirtualizer.cs index c77e94669..70cac75d1 100644 --- a/GVFS/GVFS.Virtualization/FileSystem/FileSystemVirtualizer.cs +++ b/GVFS/GVFS.Virtualization/FileSystem/FileSystemVirtualizer.cs @@ -185,7 +185,7 @@ protected void OnDotGitFileOrFolderDeleted(string relativePath) } } - protected void OnWorkingDirectoryFileOrFolderDeleted(string relativePath, bool isDirectory) + protected void OnWorkingDirectoryFileOrFolderDeleteNotification(string relativePath, bool isDirectory, bool isPreDelete) { if (isDirectory) { @@ -193,12 +193,26 @@ protected void OnWorkingDirectoryFileOrFolderDeleted(string relativePath, bool i GitCommandLineParser gitCommand = new GitCommandLineParser(this.Context.Repository.GVFSLock.GetLockedGitCommand()); if (!gitCommand.IsValidGitCommand) { - this.FileSystemCallbacks.OnFolderDeleted(relativePath); + if (isPreDelete) + { + this.FileSystemCallbacks.OnFolderPreDelete(relativePath); + } + else + { + this.FileSystemCallbacks.OnFolderDeleted(relativePath); + } } } else { - this.FileSystemCallbacks.OnFileDeleted(relativePath); + if (isPreDelete) + { + this.FileSystemCallbacks.OnFilePreDelete(relativePath); + } + else + { + this.FileSystemCallbacks.OnFileDeleted(relativePath); + } } this.FileSystemCallbacks.InvalidateGitStatusCache(); diff --git a/GVFS/GVFS.Virtualization/FileSystemCallbacks.cs b/GVFS/GVFS.Virtualization/FileSystemCallbacks.cs index a9ea5200e..d7c0d57b4 100644 --- a/GVFS/GVFS.Virtualization/FileSystemCallbacks.cs +++ b/GVFS/GVFS.Virtualization/FileSystemCallbacks.cs @@ -33,6 +33,7 @@ public class FileSystemCallbacks : IDisposable, IHeartBeatMetadataProvider private GVFSContext context; private GVFSGitObjects gitObjects; private ModifiedPathsDatabase modifiedPaths; + private ConcurrentHashSet newlyCreatedFileAndFolderPaths; private ConcurrentDictionary placeHolderCreationCount; private BackgroundFileSystemTaskRunner backgroundFileSystemTaskRunner; private FileSystemVirtualizer fileSystemVirtualizer; @@ -75,6 +76,7 @@ public FileSystemCallbacks( this.fileSystemVirtualizer = fileSystemVirtualizer; this.placeHolderCreationCount = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + this.newlyCreatedFileAndFolderPaths = new ConcurrentHashSet(StringComparer.OrdinalIgnoreCase); string error; if (!ModifiedPathsDatabase.TryLoadOrCreate( @@ -364,6 +366,8 @@ public virtual void OnIndexFileChange() this.GitIndexProjection.InvalidateProjection(); this.InvalidateGitStatusCache(); } + + this.newlyCreatedFileAndFolderPaths.Clear(); } public void InvalidateGitStatusCache() @@ -400,6 +404,7 @@ public void OnExcludeFileChanged() public void OnFileCreated(string relativePath) { + this.newlyCreatedFileAndFolderPaths.Add(relativePath); this.backgroundFileSystemTaskRunner.Enqueue(FileSystemTask.OnFileCreated(relativePath)); } @@ -433,8 +438,14 @@ public void OnFileDeleted(string relativePath) this.backgroundFileSystemTaskRunner.Enqueue(FileSystemTask.OnFileDeleted(relativePath)); } + public void OnFilePreDelete(string relativePath) + { + this.backgroundFileSystemTaskRunner.Enqueue(FileSystemTask.OnFilePreDelete(relativePath)); + } + public void OnFolderCreated(string relativePath) { + this.newlyCreatedFileAndFolderPaths.Add(relativePath); this.backgroundFileSystemTaskRunner.Enqueue(FileSystemTask.OnFolderCreated(relativePath)); } @@ -448,6 +459,11 @@ public void OnFolderDeleted(string relativePath) this.backgroundFileSystemTaskRunner.Enqueue(FileSystemTask.OnFolderDeleted(relativePath)); } + public void OnFolderPreDelete(string relativePath) + { + this.backgroundFileSystemTaskRunner.Enqueue(FileSystemTask.OnFolderPreDelete(relativePath)); + } + public void OnPlaceholderFileCreated(string relativePath, string sha, string triggeringProcessImageFileName) { this.GitIndexProjection.OnPlaceholderFileCreated(relativePath, sha); @@ -625,7 +641,14 @@ private FileSystemTaskResult ExecuteBackgroundOperation(FileSystemTask gitUpdate result = FileSystemTaskResult.Success; if (!string.IsNullOrEmpty(gitUpdate.OldVirtualPath) && !IsPathInsideDotGit(gitUpdate.OldVirtualPath)) { - result = this.AddModifiedPathAndRemoveFromPlaceholderList(gitUpdate.OldVirtualPath); + if (this.newlyCreatedFileAndFolderPaths.Contains(gitUpdate.OldVirtualPath)) + { + result = this.TryRemoveModifiedPath(gitUpdate.OldVirtualPath, isFolder: false); + } + else + { + result = this.AddModifiedPathAndRemoveFromPlaceholderList(gitUpdate.OldVirtualPath); + } } if (result == FileSystemTaskResult.Success && @@ -637,9 +660,44 @@ private FileSystemTaskResult ExecuteBackgroundOperation(FileSystemTask gitUpdate break; + case FileSystemTask.OperationType.OnFilePreDelete: + // This code assumes that the current implementations of FileSystemVirtualizer will call either + // the PreDelete or the Delete not both so if a new implementation starts calling both + // this will need to be cleaned up to not duplicate the work that is being done. + metadata.Add("virtualPath", gitUpdate.VirtualPath); + if (this.newlyCreatedFileAndFolderPaths.Contains(gitUpdate.VirtualPath)) + { + string fullPathToFolder = Path.Combine(this.context.Enlistment.WorkingDirectoryRoot, gitUpdate.VirtualPath); + if (!this.context.FileSystem.FileExists(fullPathToFolder)) + { + result = this.TryRemoveModifiedPath(gitUpdate.VirtualPath, isFolder: false); + } + else + { + result = FileSystemTaskResult.Success; + } + } + else + { + result = this.AddModifiedPathAndRemoveFromPlaceholderList(gitUpdate.VirtualPath); + } + + break; + case FileSystemTask.OperationType.OnFileDeleted: + // This code assumes that the current implementations of FileSystemVirtualizer will call either + // the PreDelete or the Delete not both so if a new implementation starts calling both + // this will need to be cleaned up to not duplicate the work that is being done. metadata.Add("virtualPath", gitUpdate.VirtualPath); - result = this.AddModifiedPathAndRemoveFromPlaceholderList(gitUpdate.VirtualPath); + if (this.newlyCreatedFileAndFolderPaths.Contains(gitUpdate.VirtualPath)) + { + result = this.TryRemoveModifiedPath(gitUpdate.VirtualPath, isFolder: false); + } + else + { + result = this.AddModifiedPathAndRemoveFromPlaceholderList(gitUpdate.VirtualPath); + } + break; case FileSystemTask.OperationType.OnFileOverwritten: @@ -660,19 +718,27 @@ private FileSystemTaskResult ExecuteBackgroundOperation(FileSystemTask gitUpdate metadata.Add("oldVirtualPath", gitUpdate.OldVirtualPath); metadata.Add("virtualPath", gitUpdate.VirtualPath); + if (!string.IsNullOrEmpty(gitUpdate.OldVirtualPath) && + this.newlyCreatedFileAndFolderPaths.Contains(gitUpdate.OldVirtualPath)) + { + result = this.TryRemoveModifiedPath(gitUpdate.OldVirtualPath, isFolder: true); + } + // An empty destination path means the folder was renamed to somewhere outside of the repo // Note that only full folders can be moved\renamed, and so there will already be a recursive // sparse-checkout entry for the virtualPath of the folder being moved (meaning that no // additional work is needed for any files\folders inside the folder being moved) - if (!string.IsNullOrEmpty(gitUpdate.VirtualPath)) + if (result == FileSystemTaskResult.Success && !string.IsNullOrEmpty(gitUpdate.VirtualPath)) { result = this.TryAddModifiedPath(gitUpdate.VirtualPath, isFolder: true); if (result == FileSystemTaskResult.Success) { + this.newlyCreatedFileAndFolderPaths.Add(gitUpdate.VirtualPath); + Queue relativeFolderPaths = new Queue(); relativeFolderPaths.Enqueue(gitUpdate.VirtualPath); - // Add all the files in the renamed folder to the always_exclude file + // Remove old paths from modified paths if in the newly created list while (relativeFolderPaths.Count > 0) { string folderPath = relativeFolderPaths.Dequeue(); @@ -683,14 +749,17 @@ private FileSystemTaskResult ExecuteBackgroundOperation(FileSystemTask gitUpdate foreach (DirectoryItemInfo itemInfo in this.context.FileSystem.ItemsInDirectory(Path.Combine(this.context.Enlistment.WorkingDirectoryRoot, folderPath))) { string itemVirtualPath = Path.Combine(folderPath, itemInfo.Name); - if (itemInfo.IsDirectory) + string oldItemVirtualPath = gitUpdate.OldVirtualPath + itemVirtualPath.Substring(gitUpdate.VirtualPath.Length); + + this.newlyCreatedFileAndFolderPaths.Add(itemVirtualPath); + if (this.newlyCreatedFileAndFolderPaths.Contains(oldItemVirtualPath)) { - relativeFolderPaths.Enqueue(itemVirtualPath); + result = this.TryRemoveModifiedPath(oldItemVirtualPath, isFolder: itemInfo.IsDirectory); } - else + + if (itemInfo.IsDirectory) { - string oldItemVirtualPath = gitUpdate.OldVirtualPath + itemVirtualPath.Substring(gitUpdate.VirtualPath.Length); - result = this.TryAddModifiedPath(itemVirtualPath, isFolder: false); + relativeFolderPaths.Enqueue(itemVirtualPath); } } } @@ -734,9 +803,44 @@ private FileSystemTaskResult ExecuteBackgroundOperation(FileSystemTask gitUpdate break; + case FileSystemTask.OperationType.OnFolderPreDelete: + // This code assumes that the current implementations of FileSystemVirtualizer will call either + // the PreDelete or the Delete not both so if a new implementation starts calling both + // this will need to be cleaned up to not duplicate the work that is being done. + metadata.Add("virtualPath", gitUpdate.VirtualPath); + if (this.newlyCreatedFileAndFolderPaths.Contains(gitUpdate.VirtualPath)) + { + string fullPathToFolder = Path.Combine(this.context.Enlistment.WorkingDirectoryRoot, gitUpdate.VirtualPath); + if (!this.context.FileSystem.DirectoryExists(fullPathToFolder)) + { + result = this.TryRemoveModifiedPath(gitUpdate.VirtualPath, isFolder: true); + } + else + { + result = FileSystemTaskResult.Success; + } + } + else + { + result = this.TryAddModifiedPath(gitUpdate.VirtualPath, isFolder: true); + } + + break; + case FileSystemTask.OperationType.OnFolderDeleted: + // This code assumes that the current implementations of FileSystemVirtualizer will call either + // the PreDelete or the Delete not both so if a new implementation starts calling both + // this will need to be cleaned up to not duplicate the work that is being done. metadata.Add("virtualPath", gitUpdate.VirtualPath); - result = this.TryAddModifiedPath(gitUpdate.VirtualPath, isFolder: true); + if (this.newlyCreatedFileAndFolderPaths.Contains(gitUpdate.VirtualPath)) + { + result = this.TryRemoveModifiedPath(gitUpdate.VirtualPath, isFolder: true); + } + else + { + result = this.TryAddModifiedPath(gitUpdate.VirtualPath, isFolder: true); + } + break; case FileSystemTask.OperationType.OnFolderFirstWrite: @@ -768,6 +872,19 @@ private FileSystemTaskResult ExecuteBackgroundOperation(FileSystemTask gitUpdate return result; } + private FileSystemTaskResult TryRemoveModifiedPath(string virtualPath, bool isFolder) + { + if (!this.modifiedPaths.TryRemove(virtualPath, isFolder, out bool isRetryable)) + { + return isRetryable ? FileSystemTaskResult.RetryableError : FileSystemTaskResult.FatalError; + } + + this.newlyCreatedFileAndFolderPaths.TryRemove(virtualPath); + + this.InvalidateGitStatusCache(); + return FileSystemTaskResult.Success; + } + private FileSystemTaskResult TryAddModifiedPath(string virtualPath, bool isFolder) { if (!this.modifiedPaths.TryAdd(virtualPath, isFolder, out bool isRetryable)) @@ -802,7 +919,7 @@ private FileSystemTaskResult AddModifiedPathAndRemoveFromPlaceholderList(string private FileSystemTaskResult PostBackgroundOperation() { - this.modifiedPaths.ForceFlush(); + this.modifiedPaths.WriteAllEntriesAndFlush(); this.gitStatusCache.RefreshAsynchronously(); return this.GitIndexProjection.CloseIndex(); }