Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 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
53 changes: 53 additions & 0 deletions src/Cli/dotnet/Commands/CliCommandStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -1501,6 +1501,59 @@ Tool '{1}' (version '{2}') was successfully installed. Entry is added to the man
<data name="ProjectConvertAppFullName" xml:space="preserve">
<value>Convert a file-based program to a project-based program.</value>
</data>
<data name="CmdOptionKeepSourceDescription" xml:space="preserve">
<value>Whether to keep source files intact (copy) or delete them after conversion (move).</value>
</data>
<data name="ProjectConvertAskForOutputDirectory" xml:space="preserve">
<value>Specify the output directory ({0}):</value>
<comment>{0} is the default value</comment>
</data>
<data name="ProjectConvertAskForSourceFilesAction" xml:space="preserve">
<value>Source files action [{0}/{1}] ({0}):</value>
<comment>{0} is the copy action (the default choice). {1} is the move action.</comment>
</data>
<data name="ProjectConvertCopyAction" xml:space="preserve">
<value>copy</value>
<comment>One of the choices for the source files action. The user needs to type this to the console to select the option.</comment>
</data>
<data name="ProjectConvertMoveAction" xml:space="preserve">
<value>move</value>
<comment>One of the choices for the source files action. The user needs to type this to the console to select the option.</comment>
</data>
<data name="ProjectConvertInvalidSourceAction" xml:space="preserve">
<value>Please type '{0}' or '{1}'.</value>
</data>
<data name="ProjectConvertNeedsConfirmation" xml:space="preserve">
<value>Conversion command needs to confirm whether to keep source files. Run in interactive mode or use the "--source" command-line option to confirm.</value>
<comment>{Locked="--source"}</comment>
</data>
<data name="ProjectConvertDryRun" xml:space="preserve">
<value>Determines changes without actually modifying the file system</value>
</data>
<data name="ProjectConvertWouldCreateDirectory" xml:space="preserve">
<value>Dry run: would create directory: {0}</value>
<comment>{0} is the directory full path.</comment>
</data>
<data name="ProjectConvertWouldCopyFile" xml:space="preserve">
<value>Dry run: would copy file '{0}' to '{1}'.</value>
<comment>{0} and {1} are file full paths.</comment>
</data>
<data name="ProjectConvertWouldConvertAndMoveFile" xml:space="preserve">
<value>Dry run: would remove directives from file '{0}' and move it to '{1}'.</value>
<comment>{0} and {1} are file full paths.</comment>
</data>
<data name="ProjectConvertWouldMoveFile" xml:space="preserve">
<value>Dry run: would move file '{0}' to '{1}'.</value>
<comment>{0} and {1} are file full paths.</comment>
</data>
<data name="ProjectConvertWouldDeleteFile" xml:space="preserve">
<value>Dry run: would delete file: {0}</value>
<comment>{0} is the file full path.</comment>
</data>
<data name="ProjectConvertWouldCreateFile" xml:space="preserve">
<value>Dry run: would create file: {0}</value>
<comment>{0} is the file full path.</comment>
</data>
<data name="ProjectManifest" xml:space="preserve">
<value>PROJECT_MANIFEST</value>
</data>
Expand Down
180 changes: 163 additions & 17 deletions src/Cli/dotnet/Commands/Project/Convert/ProjectConvertCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.CommandLine;
using System.Diagnostics.CodeAnalysis;
using Microsoft.Build.Evaluation;
using Microsoft.DotNet.Cli.Commands.Run;
using Microsoft.DotNet.Cli.Utils;
Expand All @@ -17,17 +18,15 @@ internal sealed class ProjectConvertCommand(ParseResult parseResult) : CommandBa

public override int Execute()
{
// Check the entry point file path.
string file = Path.GetFullPath(_file);
if (!VirtualProjectBuildingCommand.IsValidEntryPointPath(file))
{
throw new GracefulException(CliCommandStrings.InvalidFilePath, file);
}

string targetDirectory = _outputDirectory ?? Path.ChangeExtension(file, null);
if (Directory.Exists(targetDirectory))
{
throw new GracefulException(CliCommandStrings.DirectoryAlreadyExists, targetDirectory);
}
string targetDirectory = DetermineOutputDirectory(file);
bool keepSourceFiles = ShouldKeepSourceFiles();

// Find directives (this can fail, so do this before creating the target directory).
var sourceFile = VirtualProjectBuildingCommand.LoadSourceFile(file);
Expand All @@ -36,28 +35,48 @@ public override int Execute()
// Find other items to copy over, e.g., default Content items like JSON files in Web apps.
var includeItems = FindIncludedItems().ToList();

Directory.CreateDirectory(targetDirectory);
bool dryRun = _parseResult.GetValue(ProjectConvertCommandParser.DryRunOption);

CreateDirectory(targetDirectory);

var targetFile = Path.Join(targetDirectory, Path.GetFileName(file));

// If there were any directives, remove them from the file.
if (directives.Length != 0)
// Process the entry point file.
if (keepSourceFiles)
{
VirtualProjectBuildingCommand.RemoveDirectivesFromFile(directives, sourceFile.Text, targetFile);
File.Delete(file);
CopyFile(file, targetFile);
}
else if (directives.Length != 0)
{
if (dryRun)
{
Reporter.Output.WriteLine(CliCommandStrings.ProjectConvertWouldConvertAndMoveFile, file, targetFile);
}
else
{
VirtualProjectBuildingCommand.RemoveDirectivesFromFile(directives, sourceFile.Text, targetFile);
File.Delete(file);
}
}
else
{
File.Move(file, targetFile);
MoveFile(file, targetFile);
}

// Create project file.
string projectFile = Path.Join(targetDirectory, Path.GetFileNameWithoutExtension(file) + ".csproj");
using var stream = File.Open(projectFile, FileMode.Create, FileAccess.Write);
using var writer = new StreamWriter(stream, Encoding.UTF8);
VirtualProjectBuildingCommand.WriteProjectFile(writer, directives, isVirtualProject: false);
if (dryRun)
{
Reporter.Output.WriteLine(CliCommandStrings.ProjectConvertWouldCreateFile, projectFile);
}
else
{
using var stream = File.Open(projectFile, FileMode.Create, FileAccess.Write);
using var writer = new StreamWriter(stream, Encoding.UTF8);
VirtualProjectBuildingCommand.WriteProjectFile(writer, directives, isVirtualProject: false);
}

// Copy over included items.
// Copy or move over included items.
foreach (var item in includeItems)
{
string targetItemFullPath = Path.Combine(targetDirectory, item.RelativePath);
Expand All @@ -69,12 +88,58 @@ public override int Execute()
}

string targetItemDirectory = Path.GetDirectoryName(targetItemFullPath)!;
Directory.CreateDirectory(targetItemDirectory);
File.Copy(item.FullPath, targetItemFullPath);
CreateDirectory(targetItemDirectory);
if (keepSourceFiles)
{
CopyFile(item.FullPath, targetItemFullPath);
}
else
{
MoveFile(item.FullPath, targetItemFullPath);
}
}

return 0;

void CreateDirectory(string path)
{
if (dryRun)
{
if (!Directory.Exists(path))
{
Reporter.Output.WriteLine(CliCommandStrings.ProjectConvertWouldCreateDirectory, path);
}
}
else
{
Directory.CreateDirectory(path);
}
}

void CopyFile(string source, string target)
{
if (dryRun)
{
Reporter.Output.WriteLine(CliCommandStrings.ProjectConvertWouldCopyFile, source, target);
}
else
{
File.Copy(source, target);
}
}

void MoveFile(string source, string target)
{
if (dryRun)
{
Reporter.Output.WriteLine(CliCommandStrings.ProjectConvertWouldMoveFile, source, target);
}
else
{
File.Move(source, target);
}
}

IEnumerable<(string FullPath, string RelativePath)> FindIncludedItems()
{
string entryPointFileDirectory = PathUtility.EnsureTrailingSlash(Path.GetDirectoryName(file)!);
Expand Down Expand Up @@ -118,4 +183,85 @@ public override int Execute()
}
}
}

private string DetermineOutputDirectory(string file)
{
string defaultValue = Path.ChangeExtension(file, null);
string defaultValueRelative = Path.GetRelativePath(relativeTo: Environment.CurrentDirectory, defaultValue);
string targetDirectory = _outputDirectory
?? TryAskForOutputDirectory(defaultValueRelative)
?? defaultValue;
if (Directory.Exists(targetDirectory))
{
throw new GracefulException(CliCommandStrings.DirectoryAlreadyExists, targetDirectory);
}

return targetDirectory;
}

private string? TryAskForOutputDirectory(string defaultValueRelative)
{
return InteractiveConsole.Ask<string?>(
string.Format(CliCommandStrings.ProjectConvertAskForOutputDirectory, defaultValueRelative),
_parseResult,
(path, out result, [NotNullWhen(returnValue: false)] out error) =>
{
if (Directory.Exists(path))
{
result = null;
error = string.Format(CliCommandStrings.DirectoryAlreadyExists, Path.GetFullPath(path));
return false;
}

result = path is null ? null : Path.GetFullPath(path);
error = null;
return true;
},
out var result)
? result
: null;
}

private bool ShouldKeepSourceFiles()
{
if (_parseResult.GetValue(ProjectConvertCommandParser.SourceOption) is { } source)
{
if (source is ProjectConvertCommandParser.SourceAction.copy)
{
return true;
}

if (source is ProjectConvertCommandParser.SourceAction.move)
{
return false;
}
}

return InteractiveConsole.Ask<bool>(
string.Format(CliCommandStrings.ProjectConvertAskForSourceFilesAction, CliCommandStrings.ProjectConvertCopyAction, CliCommandStrings.ProjectConvertMoveAction),
_parseResult,
(answer, out result, [NotNullWhen(returnValue: false)] out error) =>
{
if (answer is null || string.Equals(answer, CliCommandStrings.ProjectConvertCopyAction, StringComparison.OrdinalIgnoreCase))
{
result = true;
error = null;
return true;
}

if (string.Equals(answer, CliCommandStrings.ProjectConvertMoveAction, StringComparison.OrdinalIgnoreCase))
{
result = false;
error = null;
return true;
}

result = default;
error = string.Format(CliCommandStrings.ProjectConvertInvalidSourceAction, CliCommandStrings.ProjectConvertCopyAction, CliCommandStrings.ProjectConvertMoveAction);
return false;
},
out var result)
? result
: throw new GracefulException(CliCommandStrings.ProjectConvertNeedsConfirmation);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,34 @@ internal sealed class ProjectConvertCommandParser
Arity = ArgumentArity.Zero,
};

public static readonly Option<SourceAction?> SourceOption = new("--source")
{
Description = CliCommandStrings.CmdOptionKeepSourceDescription,
Arity = ArgumentArity.ExactlyOne,
};

public enum SourceAction
{
copy,
move,
}

public static readonly Option<bool> DryRunOption = new("--dry-run")
{
Description = CliCommandStrings.ProjectConvertDryRun,
Arity = ArgumentArity.Zero,
};

public static Command GetCommand()
{
Command command = new("convert", CliCommandStrings.ProjectConvertAppFullName)
{
FileArgument,
SharedOptions.OutputOption,
SourceOption,
ForceOption,
CommonOptions.InteractiveOption(),
DryRunOption,
};

command.SetAction((parseResult) => new ProjectConvertCommand(parseResult).Execute());
Expand Down
43 changes: 1 addition & 42 deletions src/Cli/dotnet/Commands/Tool/Execute/ToolExecuteCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ internal class ToolExecuteCommand(ParseResult result, ToolManifestFinder? toolMa
private readonly string[] _addSource = result.GetValue(ToolExecuteCommandParser.AddSourceOption) ?? [];
private readonly bool _interactive = result.GetValue(ToolExecuteCommandParser.InteractiveOption);
private readonly VerbosityOptions _verbosity = result.GetValue(ToolExecuteCommandParser.VerbosityOption);
private readonly bool _yes = result.GetValue(ToolExecuteCommandParser.YesOption);
private readonly IToolPackageDownloader _toolPackageDownloader = ToolPackageFactory.CreateToolPackageStoresAndDownloader().downloader;

private readonly RestoreActionConfig _restoreActionConfig = new RestoreActionConfig(DisableParallel: result.GetValue(ToolCommandRestorePassThroughOptions.DisableParallelOption),
Expand Down Expand Up @@ -128,47 +127,7 @@ public override int Execute()

private bool UserAgreedToRunFromSource(PackageId packageId, NuGetVersion version, PackageSource source)
{
if (_yes)
{
return true;
}

if (!_interactive)
{
return false;
}

string promptMessage = string.Format(CliCommandStrings.ToolDownloadConfirmationPrompt, packageId, version.ToString(), source.Source);

static string AddPromptOptions(string message)
{
return $"{message} [{CliCommandStrings.ConfirmationPromptYesValue}/{CliCommandStrings.ConfirmationPromptNoValue}] ({CliCommandStrings.ConfirmationPromptYesValue}): ";
}

Console.Write(AddPromptOptions(promptMessage));

static bool KeyMatches(ConsoleKeyInfo pressedKey, string valueKey)
{
// Apparently you can't do invariant case insensitive comparison on a char directly, so we have to convert it to a string.
// The resource string should be a single character, but we take the first character just to be sure.
return pressedKey.KeyChar.ToString().ToLowerInvariant().Equals(
valueKey.ToLowerInvariant().Substring(0, 1));
}

while (true)
{
var key = Console.ReadKey();
Console.WriteLine();
if (key.Key == ConsoleKey.Enter || KeyMatches(key, CliCommandStrings.ConfirmationPromptYesValue))
{
return true;
}
if (key.Key == ConsoleKey.Escape || KeyMatches(key, CliCommandStrings.ConfirmationPromptNoValue))
{
return false;
}

Console.Write(AddPromptOptions(string.Format(CliCommandStrings.ConfirmationPromptInvalidChoiceMessage, CliCommandStrings.ConfirmationPromptYesValue, CliCommandStrings.ConfirmationPromptNoValue)));
}
return InteractiveConsole.Confirm(promptMessage, _parseResult, acceptEscapeForFalse: true) == true;
}
}
Loading