Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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);
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand All @@ -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());
}
Expand Down Expand Up @@ -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"))
Expand All @@ -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)
Expand All @@ -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)}";
Expand Down
83 changes: 74 additions & 9 deletions GVFS/GVFS/CommandLine/DehydrateVerb.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,19 @@
"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;

Expand Down Expand Up @@ -75,6 +85,7 @@
{
{ "Confirmed", this.Confirmed },
{ "NoStatus", this.NoStatus },
{ "Full", this.Full },
{ "NamedPipeName", enlistment.NamedPipeName },
{ "Folders", this.Folders },
{ nameof(this.EnlistmentRootPathParameter), this.EnlistmentRootPathParameter },
Expand Down Expand Up @@ -112,14 +123,20 @@
}
}

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
Expand All @@ -130,25 +147,33 @@
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 <folder list>'"
: $"'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 <path>'.

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 <folder list>'
To actually execute the dehydrate, run {confirmCommand}
from a parent of the folders list.
");

Expand All @@ -158,7 +183,7 @@
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;
}

Expand Down Expand Up @@ -209,7 +234,15 @@
}
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)
{
Expand Down Expand Up @@ -256,7 +289,7 @@

using (modifiedPaths)
{
string ioError;

Check warning on line 292 in GVFS/GVFS/CommandLine/DehydrateVerb.cs

View workflow job for this annotation

GitHub Actions / Build and Unit Test (Release)

The variable 'ioError' is declared but never used

Check warning on line 292 in GVFS/GVFS/CommandLine/DehydrateVerb.cs

View workflow job for this annotation

GitHub Actions / Build and Unit Test (Debug)

The variable 'ioError' is declared but never used
foreach (string folder in folders)
{
string normalizedPath = GVFSDatabase.NormalizePath(folder);
Expand Down Expand Up @@ -310,6 +343,38 @@
return Path.Combine(backupRoot, "src");
}

private string[] GetRootLevelFolders(GVFSEnlistment enlistment)
{
HashSet<string> rootFolders = new HashSet<string>(GVFSPlatform.Instance.Constants.PathComparer);
GitProcess git = new GitProcess(enlistment);
GitProcess.Result result = git.LsTree(
GVFSConstants.DotGit.HeadName,
line =>
{
// ls-tree output format: "<mode> <type> <hash>\t<path>"
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 ||
Expand Down
Loading