diff --git a/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/DehydrateTests.cs b/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/DehydrateTests.cs index f896825fe..e05277bf5 100644 --- a/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/DehydrateTests.cs +++ b/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/DehydrateTests.cs @@ -54,7 +54,19 @@ public void DehydrateShouldExitWithoutConfirm() [TestCase] public void DehydrateShouldSucceedInCommonCase() { - this.DehydrateShouldSucceed(new[] { "The repo was successfully dehydrated and remounted" }, confirm: true, noStatus: false); + this.DehydrateShouldSucceed(new[] { "folder dehydrate successful." }, confirm: true, noStatus: false); + } + + [TestCase] + public void FullDehydrateShouldExitWithoutConfirm() + { + this.DehydrateShouldSucceed(new[] { "To actually execute the dehydrate, run 'gvfs dehydrate --confirm --full'" }, confirm: false, noStatus: false, full: true); + } + + [TestCase] + public void FullDehydrateShouldSucceedInCommonCase() + { + this.DehydrateShouldSucceed(new[] { "The repo was successfully dehydrated and remounted" }, confirm: true, noStatus: false, full: true); } [TestCase] @@ -69,13 +81,13 @@ public void DehydrateShouldSucceedEvenIfObjectCacheIsDeleted() { this.Enlistment.UnmountGVFS(); RepositoryHelpers.DeleteTestDirectory(this.Enlistment.GetObjectRoot(this.fileSystem)); - this.DehydrateShouldSucceed(new[] { "The repo was successfully dehydrated and remounted" }, confirm: true, noStatus: true); + this.DehydrateShouldSucceed(new[] { "The repo was successfully dehydrated and remounted" }, confirm: true, noStatus: true, full: true); } [TestCase] public void DehydrateShouldBackupFiles() { - this.DehydrateShouldSucceed(new[] { "The repo was successfully dehydrated and remounted" }, confirm: true, noStatus: false); + this.DehydrateShouldSucceed(new[] { "The repo was successfully dehydrated and remounted" }, confirm: true, noStatus: false, full: true); string backupFolder = Path.Combine(this.Enlistment.EnlistmentRoot, "dehydrate_backup"); backupFolder.ShouldBeADirectory(this.fileSystem); string[] backupFolderItems = this.fileSystem.EnumerateDirectory(backupFolder).Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); @@ -112,7 +124,7 @@ public void DehydrateShouldFailIfLocalCacheNotInMetadata() GVFSHelpers.SaveDiskLayoutVersion(this.Enlistment.DotGVFSRoot, majorVersion, minorVersion); GVFSHelpers.SaveGitObjectsRoot(this.Enlistment.DotGVFSRoot, objectsRoot); - this.DehydrateShouldFail(new[] { "Failed to determine local cache path from repo metadata" }, noStatus: true); + this.DehydrateShouldFail(new[] { "Failed to determine local cache path from repo metadata" }, noStatus: true, full: true); this.fileSystem.DeleteFile(metadataPath); this.fileSystem.MoveFile(metadataBackupPath, metadataPath); @@ -136,7 +148,7 @@ public void DehydrateShouldFailIfGitObjectsRootNotInMetadata() GVFSHelpers.SaveDiskLayoutVersion(this.Enlistment.DotGVFSRoot, majorVersion, minorVersion); GVFSHelpers.SaveLocalCacheRoot(this.Enlistment.DotGVFSRoot, localCacheRoot); - this.DehydrateShouldFail(new[] { "Failed to determine git objects root from repo metadata" }, noStatus: true); + this.DehydrateShouldFail(new[] { "Failed to determine git objects root from repo metadata" }, noStatus: true, full: true); this.fileSystem.DeleteFile(metadataPath); this.fileSystem.MoveFile(metadataBackupPath, metadataPath); @@ -160,11 +172,11 @@ public void DehydrateShouldFailOnWrongDiskLayoutVersion() if (previousMajorVersionNum >= GVFSHelpers.GetCurrentDiskLayoutMinimumMajorVersion()) { GVFSHelpers.SaveDiskLayoutVersion(this.Enlistment.DotGVFSRoot, previousMajorVersionNum.ToString(), "0"); - this.DehydrateShouldFail(new[] { "disk layout version doesn't match current version" }, noStatus: true); + this.DehydrateShouldFail(new[] { "disk layout version doesn't match current version" }, noStatus: true, full: true); } GVFSHelpers.SaveDiskLayoutVersion(this.Enlistment.DotGVFSRoot, (majorVersionNum + 1).ToString(), "0"); - this.DehydrateShouldFail(new[] { "Changes to GVFS disk layout do not allow mounting after downgrade." }, noStatus: true); + this.DehydrateShouldFail(new[] { "Changes to GVFS disk layout do not allow mounting after downgrade." }, noStatus: true, full: true); GVFSHelpers.SaveDiskLayoutVersion(this.Enlistment.DotGVFSRoot, majorVersionNum.ToString(), minorVersionNum.ToString()); } @@ -558,9 +570,9 @@ private void CheckDehydratedFolderAfterUnmount(string path) } } - private void DehydrateShouldSucceed(string[] expectedInOutput, bool confirm, bool noStatus, params string[] foldersToDehydrate) + private void DehydrateShouldSucceed(string[] expectedInOutput, bool confirm, bool noStatus, bool full = false, params string[] foldersToDehydrate) { - ProcessResult result = this.RunDehydrateProcess(confirm, noStatus, foldersToDehydrate); + ProcessResult result = this.RunDehydrateProcess(confirm, noStatus, full, foldersToDehydrate); result.ExitCode.ShouldEqual(0, $"mount exit code was {result.ExitCode}. Output: {result.Output}"); if (result.Output.Contains("Failed to move the src folder: Access to the path")) @@ -572,14 +584,14 @@ private void DehydrateShouldSucceed(string[] expectedInOutput, bool confirm, boo result.Output.ShouldContain(expectedInOutput); } - private void DehydrateShouldFail(string[] expectedErrorMessages, bool noStatus, params string[] foldersToDehydrate) + private void DehydrateShouldFail(string[] expectedErrorMessages, bool noStatus, bool full = false, params string[] foldersToDehydrate) { - ProcessResult result = this.RunDehydrateProcess(confirm: true, noStatus: noStatus, foldersToDehydrate: foldersToDehydrate); + ProcessResult result = this.RunDehydrateProcess(confirm: true, noStatus: noStatus, full: full, foldersToDehydrate: foldersToDehydrate); result.ExitCode.ShouldEqual(GVFSGenericError, $"mount exit code was not {GVFSGenericError}"); result.Output.ShouldContain(expectedErrorMessages); } - private ProcessResult RunDehydrateProcess(bool confirm, bool noStatus, params string[] foldersToDehydrate) + private ProcessResult RunDehydrateProcess(bool confirm, bool noStatus, bool full = false, params string[] foldersToDehydrate) { string dehydrateFlags = string.Empty; if (confirm) @@ -592,6 +604,11 @@ private ProcessResult RunDehydrateProcess(bool confirm, bool noStatus, params st dehydrateFlags += " --no-status "; } + if (full) + { + dehydrateFlags += " --full "; + } + if (foldersToDehydrate.Length > 0) { dehydrateFlags += $" --folders {string.Join(";", foldersToDehydrate)}"; diff --git a/GVFS/GVFS/CommandLine/DehydrateVerb.cs b/GVFS/GVFS/CommandLine/DehydrateVerb.cs index 58061dc49..5f9702239 100644 --- a/GVFS/GVFS/CommandLine/DehydrateVerb.cs +++ b/GVFS/GVFS/CommandLine/DehydrateVerb.cs @@ -43,9 +43,19 @@ public class DehydrateVerb : GVFSVerb.ForExistingEnlistment "folders", Default = "", Required = false, - HelpText = "A semicolon (" + FolderListSeparator + ") delimited list of folders to dehydrate. Each folder must be relative to the repository root.")] + HelpText = "A semicolon (" + FolderListSeparator + ") delimited list of folders to dehydrate. " + + "Each folder must be relative to the repository root. " + + "When omitted (without --full), all root-level folders are dehydrated.")] public string Folders { get; set; } + [Option( + "full", + Default = false, + Required = false, + HelpText = "Perform a full dehydration that unmounts, backs up the entire src folder, and re-creates the virtualization root from scratch. " + + "Without this flag, the default behavior dehydrates individual folders which is faster and does not require a full unmount.")] + public bool Full { get; set; } + public string RunningVerbName { get; set; } = DehydrateVerbName; public string ActionName { get; set; } = DehydrateVerbName; @@ -75,6 +85,7 @@ protected override void Execute(GVFSEnlistment enlistment) { { "Confirmed", this.Confirmed }, { "NoStatus", this.NoStatus }, + { "Full", this.Full }, { "NamedPipeName", enlistment.NamedPipeName }, { "Folders", this.Folders }, { nameof(this.EnlistmentRootPathParameter), this.EnlistmentRootPathParameter }, @@ -112,14 +123,20 @@ protected override void Execute(GVFSEnlistment enlistment) } } - bool fullDehydrate = string.IsNullOrEmpty(this.Folders); + bool fullDehydrate = this.Full; + bool hasFoldersList = !string.IsNullOrEmpty(this.Folders); + + if (fullDehydrate && hasFoldersList) + { + this.ReportErrorAndExit("Cannot combine --full with --folders."); + } if (!this.Confirmed && fullDehydrate) { this.Output.WriteLine( $@"WARNING: THIS IS AN EXPERIMENTAL FEATURE -Dehydrate will back up your src folder, and then create a new, empty src folder +Dehydrate --full will back up your src folder, and then create a new, empty src folder with a fresh virtualization of the repo. All of your downloaded objects, branches, and siblings of the src folder will be preserved. Your modified working directory files will be moved to the backup, and your new working directory will not have @@ -130,25 +147,33 @@ any of your uncommitted changes. in the backup folder, but it will be harder to find them because 'git status' will not work in the backup. -To actually execute the dehydrate, run 'gvfs dehydrate --confirm' from {enlistment.EnlistmentRoot}. +To actually execute the dehydrate, run 'gvfs dehydrate --confirm --full' from {enlistment.EnlistmentRoot}. "); return; } else if (!this.Confirmed) { + string folderDescription = hasFoldersList + ? "the folders specified" + : "all root-level folders"; + + string confirmCommand = hasFoldersList + ? $"'gvfs dehydrate --confirm --folders '" + : $"'gvfs dehydrate --confirm'"; + this.Output.WriteLine( -@"WARNING: THIS IS AN EXPERIMENTAL FEATURE +$@"WARNING: THIS IS AN EXPERIMENTAL FEATURE All of your downloaded objects, branches, and siblings of the src folder -will be preserved. This will remove the folders specified and any working directory +will be preserved. This will remove {folderDescription} and any working directory files and folders even if ignored by git similar to 'git clean -xdf '. Before you dehydrate, you will have to commit any working directory changes you want to keep and have a clean 'git status', or run with --no-status to undo any uncommitted changes. -To actually execute the dehydrate, run 'gvfs dehydrate --confirm --folders ' +To actually execute the dehydrate, run {confirmCommand} from a parent of the folders list. "); @@ -158,7 +183,7 @@ from a parent of the folders list. if (fullDehydrate && Environment.CurrentDirectory.StartsWith(enlistment.WorkingDirectoryBackingRoot)) { /* If running from /src, the dehydrate would fail because of the handle we are holding on it. */ - this.Output.WriteLine($"Dehydrate must be run from {enlistment.EnlistmentRoot}"); + this.Output.WriteLine($"Dehydrate --full must be run from {enlistment.EnlistmentRoot}"); return; } @@ -209,7 +234,15 @@ from a parent of the folders list. } else { - string[] folders = this.Folders.Split(new[] { FolderListSeparator }, StringSplitOptions.RemoveEmptyEntries); + string[] folders; + if (hasFoldersList) + { + folders = this.Folders.Split(new[] { FolderListSeparator }, StringSplitOptions.RemoveEmptyEntries); + } + else + { + folders = this.GetRootLevelFolders(enlistment); + } if (folders.Length > 0) { @@ -310,6 +343,38 @@ private static string GetBackupSrcPath(string backupRoot) return Path.Combine(backupRoot, "src"); } + private string[] GetRootLevelFolders(GVFSEnlistment enlistment) + { + HashSet rootFolders = new HashSet(GVFSPlatform.Instance.Constants.PathComparer); + GitProcess git = new GitProcess(enlistment); + GitProcess.Result result = git.LsTree( + GVFSConstants.DotGit.HeadName, + line => + { + // ls-tree output format: " \t" + int tabIndex = line.IndexOf('\t'); + if (tabIndex >= 0) + { + string path = line.Substring(tabIndex + 1); + int separatorIndex = path.IndexOf('/'); + string rootFolder = separatorIndex >= 0 ? path.Substring(0, separatorIndex) : path; + if (!rootFolder.Equals(GVFSConstants.DotGit.Root, StringComparison.OrdinalIgnoreCase)) + { + rootFolders.Add(rootFolder); + } + } + }, + recursive: false, + showDirectories: true); + + if (result.ExitCodeIsFailure) + { + this.ReportErrorAndExit($"Failed to enumerate root-level folders from HEAD: {result.Errors}"); + } + + return rootFolders.ToArray(); + } + private bool IsFolderValid(string folderPath) { if (folderPath == GVFSConstants.DotGit.Root ||