diff --git a/GVFS/GVFS.Platform.Windows/WindowsFileSystemVirtualizer.cs b/GVFS/GVFS.Platform.Windows/WindowsFileSystemVirtualizer.cs index 23a49030b..36edf0dde 100644 --- a/GVFS/GVFS.Platform.Windows/WindowsFileSystemVirtualizer.cs +++ b/GVFS/GVFS.Platform.Windows/WindowsFileSystemVirtualizer.cs @@ -201,8 +201,14 @@ public override FileSystemResult UpdatePlaceholderIfNeeded( public override FileSystemResult DehydrateFolder(string relativePath) { - // Don't need to do anything here because the parent will reproject the folder. - return new FileSystemResult(FSResult.Ok, 0); + // The folder should have already been deleted, but + // its tombstone also needs to be deleted to allow reprojection. + var result = this.virtualizationInstance.DeleteFile( + relativePath, + UpdateType.AllowTombstone, + out UpdateFailureCause failureCause); + + return new FileSystemResult(HResultToFSResult(result), unchecked((int)result)); } // TODO: Need ProjFS 13150199 to be fixed so that GVFS doesn't leak memory if the enumeration cancelled. diff --git a/GVFS/GVFS.Virtualization/FileSystemCallbacks.cs b/GVFS/GVFS.Virtualization/FileSystemCallbacks.cs index e7737ccba..6eeb909c4 100644 --- a/GVFS/GVFS.Virtualization/FileSystemCallbacks.cs +++ b/GVFS/GVFS.Virtualization/FileSystemCallbacks.cs @@ -30,6 +30,7 @@ public class FileSystemCallbacks : IDisposable, IHeartBeatMetadataProvider GitCommandLineParser.Verbs.UpdateIndex; private readonly string logsHeadPath; + private readonly ConcurrentHashSet dehydratingFolders = new ConcurrentHashSet(); private GVFSContext context; private IPlaceholderCollection placeholderDatabase; @@ -305,6 +306,10 @@ public bool TryDehydrateFolder(string relativePath, out string errorMessage) try { + this.dehydratingFolders.Add(relativePath); + + var absolutePath = Path.Combine(this.context.Enlistment.WorkingDirectoryBackingRoot, relativePath); + this.context.FileSystem.DeleteDirectory(absolutePath, recursive: true); relativePath = GVFSDatabase.NormalizePath(relativePath); removedPlaceholders = this.placeholderDatabase.RemoveAllEntriesForFolder(relativePath); removedModifiedPaths = this.modifiedPaths.RemoveAllEntriesForFolder(relativePath); @@ -321,6 +326,10 @@ public bool TryDehydrateFolder(string relativePath, out string errorMessage) EventMetadata metadata = this.CreateEventMetadata(relativePath, ex); this.context.Tracer.RelatedError(metadata, errorMessage); } + finally + { + this.dehydratingFolders.TryRemove(relativePath); + } if (!string.IsNullOrEmpty(errorMessage)) { @@ -466,12 +475,18 @@ public virtual void OnFileSymLinkCreated(string newLinkRelativePath) public void OnFileDeleted(string relativePath) { - this.backgroundFileSystemTaskRunner.Enqueue(FileSystemTask.OnFileDeleted(relativePath)); + if (!this.IsDehydrating(relativePath)) + { + this.backgroundFileSystemTaskRunner.Enqueue(FileSystemTask.OnFileDeleted(relativePath)); + } } public void OnFilePreDelete(string relativePath) { - this.backgroundFileSystemTaskRunner.Enqueue(FileSystemTask.OnFilePreDelete(relativePath)); + if (!this.IsDehydrating(relativePath)) + { + this.backgroundFileSystemTaskRunner.Enqueue(FileSystemTask.OnFilePreDelete(relativePath)); + } } /// @@ -506,17 +521,26 @@ public virtual void OnFolderRenamed(string oldRelativePath, string newRelativePa public void OnFolderDeleted(string relativePath) { - this.backgroundFileSystemTaskRunner.Enqueue(FileSystemTask.OnFolderDeleted(relativePath)); + if (!this.IsDehydrating(relativePath)) + { + this.backgroundFileSystemTaskRunner.Enqueue(FileSystemTask.OnFolderDeleted(relativePath)); + } } public void OnPossibleTombstoneFolderCreated(string relativePath) { - this.GitIndexProjection.OnPossibleTombstoneFolderCreated(relativePath); + if (!this.IsDehydrating(relativePath)) + { + this.GitIndexProjection.OnPossibleTombstoneFolderCreated(relativePath); + } } public void OnFolderPreDelete(string relativePath) { - this.backgroundFileSystemTaskRunner.Enqueue(FileSystemTask.OnFolderPreDelete(relativePath)); + if (!this.IsDehydrating(relativePath)) + { + this.backgroundFileSystemTaskRunner.Enqueue(FileSystemTask.OnFolderPreDelete(relativePath)); + } } public void OnPlaceholderFileCreated(string relativePath, string sha, string triggeringProcessImageFileName) @@ -1025,6 +1049,12 @@ private EventMetadata CreateEventMetadata( return metadata; } + private bool IsDehydrating(string relativePath) + { + return this.dehydratingFolders.Any(f => + relativePath.StartsWith(f + GVFSConstants.GitPathSeparatorString, GVFSPlatform.Instance.Constants.PathComparison)); + } + private class PlaceHolderCreateCounter { private long count; diff --git a/GVFS/GVFS/CommandLine/DehydrateVerb.cs b/GVFS/GVFS/CommandLine/DehydrateVerb.cs index b739b9b2d..b5d7df9ff 100644 --- a/GVFS/GVFS/CommandLine/DehydrateVerb.cs +++ b/GVFS/GVFS/CommandLine/DehydrateVerb.cs @@ -181,16 +181,16 @@ from a parent of the folders list. this.Output.WriteLine(); - this.Unmount(tracer); - - string error; - if (!DiskLayoutUpgrade.TryCheckDiskLayoutVersion(tracer, enlistment.EnlistmentRoot, out error)) - { - this.ReportErrorAndExit(tracer, error); - } - if (fullDehydrate) { + this.Unmount(tracer); + + string error; + if (!DiskLayoutUpgrade.TryCheckDiskLayoutVersion(tracer, enlistment.EnlistmentRoot, out error)) + { + this.ReportErrorAndExit(tracer, error); + } + RetryConfig retryConfig; if (!RetryConfig.TryLoadFromGitConfig(tracer, enlistment, out retryConfig, out error)) { @@ -216,7 +216,7 @@ from a parent of the folders list. { if (cleanStatus) { - this.DehydrateFolders(tracer, enlistment, folders); + this.DehydrateFolders(tracer, enlistment, folders, backupRoot); } else { @@ -231,8 +231,15 @@ from a parent of the folders list. } } - private void DehydrateFolders(JsonTracer tracer, GVFSEnlistment enlistment, string[] folders) + private void DehydrateFolders(JsonTracer tracer, GVFSEnlistment enlistment, string[] folders, string backupRoot) { + if (!this.TryBackupNonSrcFiles(tracer, enlistment, backupRoot)) + { + this.Output.WriteLine(); + this.WriteMessage(tracer, "ERROR: Backup failed. "); + return; + } + List foldersToDehydrate = new List(); List folderErrors = new List(); @@ -241,7 +248,7 @@ private void DehydrateFolders(JsonTracer tracer, GVFSEnlistment enlistment, stri { if (!ModifiedPathsDatabase.TryLoadOrCreate( tracer, - Path.Combine(enlistment.DotGVFSRoot, GVFSConstants.DotGVFS.Databases.ModifiedPaths), + Path.Combine(GetBackupDatabasesPath(backupRoot), GVFSConstants.DotGVFS.Databases.ModifiedPaths), this.fileSystem, out ModifiedPathsDatabase modifiedPaths, out string error)) @@ -271,26 +278,13 @@ private void DehydrateFolders(JsonTracer tracer, GVFSEnlistment enlistment, stri else { string fullPath = Path.Combine(enlistment.WorkingDirectoryBackingRoot, folder); - if (this.fileSystem.DirectoryExists(fullPath)) + if (!this.fileSystem.DirectoryExists(fullPath)) { - // Since directories are deleted last and will be empty at that point we can skip errors - // while trying to delete it and leave the empty directory and continue to dehydrate - if (!this.TryIO(tracer, () => this.fileSystem.DeleteDirectory(fullPath, ignoreDirectoryDeleteExceptions: true), $"Deleting '{fullPath}'", out ioError)) - { - this.WriteMessage(tracer, $"Cannot {this.ActionName} folder '{folder}': removing '{folder}' failed."); - this.WriteMessage(tracer, "Ensure no applications are accessing the folder and retry."); - this.WriteMessage(tracer, $"More details: {ioError}"); - folderErrors.Add($"{folder}\0{ioError}"); - } - else - { - foldersToDehydrate.Add(folder); - } + this.WriteMessage(tracer, $"Cannot {this.ActionName} folder '{folder}': '{folder}' does not exist."); + foldersToDehydrate.Add(folder); } else { - this.WriteMessage(tracer, $"Cannot {this.ActionName} folder '{folder}': '{folder}' does not exist."); - // Still add to foldersToDehydrate so that any placeholders or modified paths get cleaned up foldersToDehydrate.Add(folder); } @@ -306,15 +300,9 @@ private void DehydrateFolders(JsonTracer tracer, GVFSEnlistment enlistment, stri this.ReportErrorAndExit(tracer, $"{this.ActionName} for folders failed."); } - // We can skip the version check because dehydrating folders requires that a git status - // be run first, and running git status requires that the repo already be mounted (meaning - // we don't need to perform another version check again) - this.Mount( - tracer, - skipVersionCheck: true); - if (foldersToDehydrate.Count > 0) { + string backupSrc = GetBackupSrcPath(backupRoot); this.SendDehydrateMessage(tracer, enlistment, folderErrors, foldersToDehydrate); } @@ -329,6 +317,11 @@ private void DehydrateFolders(JsonTracer tracer, GVFSEnlistment enlistment, stri } } + private static string GetBackupSrcPath(string backupRoot) + { + return Path.Combine(backupRoot, "src"); + } + private bool IsFolderValid(string folderPath) { if (folderPath == GVFSConstants.DotGit.Root || @@ -353,10 +346,15 @@ private void SendDehydrateMessage(ITracer tracer, GVFSEnlistment enlistment, Lis { if (!pipeClient.Connect()) { - this.ReportErrorAndExit("Unable to connect to GVFS. Try running 'gvfs mount'"); + this.Output.WriteLine("Mounting..."); + this.Mount(tracer, skipVersionCheck: false); + if (!pipeClient.Connect()) + { + this.ReportErrorAndExit("Unable to connect to GVFS. Try running 'gvfs mount'"); + } } - NamedPipeMessages.DehydrateFolders.Request request = new NamedPipeMessages.DehydrateFolders.Request(string.Join(FolderListSeparator, folders)); + NamedPipeMessages.DehydrateFolders.Request request = new NamedPipeMessages.DehydrateFolders.Request(string.Join(";", folders)); pipeClient.SendRequest(request.CreateMessage()); response = NamedPipeMessages.DehydrateFolders.Response.FromMessage(NamedPipeMessages.Message.FromString(pipeClient.ReadRawResponse())); } @@ -535,12 +533,83 @@ private void PrepareSrcFolder(ITracer tracer, GVFSEnlistment enlistment) } } + private bool TryBackupNonSrcFiles(ITracer tracer, GVFSEnlistment enlistment, string backupRoot) + { + string backupSrc = GetBackupSrcPath(backupRoot); + string backupGit = Path.Combine(backupRoot, ".git"); + string backupGvfs = Path.Combine(backupRoot, GVFSPlatform.Instance.Constants.DotGVFSRoot); + string backupDatabases = GetBackupDatabasesPath(backupGvfs); + + string errorMessage = string.Empty; + if (!this.ShowStatusWhileRunning( + () => + { + string ioError; + if (!this.TryIO(tracer, () => Directory.CreateDirectory(backupRoot), "Create backup directory", out ioError) || + !this.TryIO(tracer, () => Directory.CreateDirectory(backupGit), "Create backup .git directory", out ioError) || + !this.TryIO(tracer, () => Directory.CreateDirectory(backupGvfs), "Create backup .gvfs directory", out ioError) || + !this.TryIO(tracer, () => Directory.CreateDirectory(backupDatabases), "Create backup .gvfs databases directory", out ioError)) + { + errorMessage = "Failed to create backup folders at " + backupRoot + ": " + ioError; + return false; + } + + // ... backup the .gvfs hydration-related data structures... + string databasesFolder = Path.Combine(enlistment.DotGVFSRoot, GVFSConstants.DotGVFS.Databases.Name); + if (!this.TryCopyFilesInFolder(tracer, databasesFolder, backupDatabases, searchPattern: "*", filenamesToSkip: "RepoMetadata.dat")) + { + return false; + } + + // ... backup everything related to the .git\index... + if (!this.TryIO( + tracer, + () => File.Copy( + Path.Combine(enlistment.DotGitRoot, GVFSConstants.DotGit.IndexName), + Path.Combine(backupGit, GVFSConstants.DotGit.IndexName)), + "Backup the git index", + out errorMessage) || + !this.TryIO( + tracer, + () => File.Copy( + Path.Combine(enlistment.DotGVFSRoot, GitIndexProjection.ProjectionIndexBackupName), + Path.Combine(backupGvfs, GitIndexProjection.ProjectionIndexBackupName)), + "Backup GVFS_projection", + out errorMessage)) + { + return false; + } + + // ... backup all .git\*.lock files + if (!this.TryCopyFilesInFolder(tracer, enlistment.DotGitRoot, backupGit, searchPattern: "*.lock")) + { + return false; + } + + return true; + }, + "Backing up your files")) + { + this.Output.WriteLine(); + this.WriteMessage(tracer, "ERROR: " + errorMessage); + + return false; + } + + return true; + } + + private static string GetBackupDatabasesPath(string backupGvfs) + { + return Path.Combine(backupGvfs, GVFSConstants.DotGVFS.Databases.Name); + } + private bool TryBackupFiles(ITracer tracer, GVFSEnlistment enlistment, string backupRoot) { - string backupSrc = Path.Combine(backupRoot, "src"); + string backupSrc = GetBackupSrcPath(backupRoot); string backupGit = Path.Combine(backupRoot, ".git"); string backupGvfs = Path.Combine(backupRoot, GVFSPlatform.Instance.Constants.DotGVFSRoot); - string backupDatabases = Path.Combine(backupGvfs, GVFSConstants.DotGVFS.Databases.Name); + string backupDatabases = GetBackupDatabasesPath(backupRoot); string errorMessage = string.Empty; if (!this.ShowStatusWhileRunning( @@ -638,6 +707,28 @@ private bool TryBackupFilesInFolder(ITracer tracer, string folderPath, string ba return true; } + private bool TryCopyFilesInFolder(ITracer tracer, string folderPath, string backupPath, string searchPattern, params string[] filenamesToSkip) + { + string errorMessage; + foreach (string file in Directory.GetFiles(folderPath, searchPattern)) + { + string fileName = Path.GetFileName(file); + if (!filenamesToSkip.Any(x => x.Equals(fileName, GVFSPlatform.Instance.Constants.PathComparison))) + { + if (!this.TryIO( + tracer, + () => File.Copy(file, file.Replace(folderPath, backupPath)), + $"Backing up {Path.GetFileName(file)}", + out errorMessage)) + { + return false; + } + } + } + + return true; + } + private bool TryDownloadGitObjects(ITracer tracer, GVFSEnlistment enlistment, RetryConfig retryConfig) { string errorMessage = null;