Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

sln-add: Support for slnx #44570

Merged
merged 64 commits into from
Dec 6, 2024
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
1d1c4b0
[IMP] sln-list: Support for slnx
edvilme Oct 30, 2024
bdff950
sln-add: Support for slnx
edvilme Oct 31, 2024
01b3031
Handle solution folders
edvilme Nov 1, 2024
277a1d7
Fix tests
edvilme Nov 1, 2024
5e6e3ff
Fix UTF8 BOM tests
edvilme Nov 1, 2024
b4ac6a4
Catch errors
edvilme Nov 1, 2024
3f967f7
Fix additional tests
edvilme Nov 4, 2024
3d52972
Fix additional tests
edvilme Nov 4, 2024
de95e6d
Fix additional issues
edvilme Nov 5, 2024
a090ad4
Remove sdk.slnx file
edvilme Nov 5, 2024
c03cad7
Fix additional tests
edvilme Nov 6, 2024
90f4248
Fix additional tests
edvilme Nov 6, 2024
4929979
Fix additional tests
edvilme Nov 6, 2024
f88c3eb
Fix duplicate project tests
edvilme Nov 6, 2024
a900dc3
Fix tests
edvilme Nov 7, 2024
c394d75
108/133 tests passing
edvilme Nov 7, 2024
c8e5996
Work on tests
edvilme Nov 8, 2024
039e694
Nit
edvilme Nov 8, 2024
cef4d22
Refactored code to fix guid tests
edvilme Nov 8, 2024
51294e9
Fix tests
edvilme Nov 8, 2024
6aee575
Fix some config tests
edvilme Nov 8, 2024
e168158
Revert guid detection
edvilme Nov 8, 2024
07343f9
Update tests guids and translations
edvilme Nov 11, 2024
af2917a
Update translations (build)
edvilme Nov 11, 2024
6d8c3fe
Fix additional tests
edvilme Nov 11, 2024
b195880
Fix issues from pr
edvilme Nov 12, 2024
93775e8
Solve solution folder tests
edvilme Nov 12, 2024
3195e02
Fix additional tests
edvilme Nov 12, 2024
8975dbc
Refactor code
edvilme Nov 13, 2024
2e9b5ea
Update tests
edvilme Nov 13, 2024
76a2f1d
Fix tests
edvilme Nov 13, 2024
60d434c
Fix WhenProjectWithAdditionalConfigurationsIsAddedSolutionDoesNotMapThem
edvilme Nov 19, 2024
f79ddf3
Revert changed project guids
edvilme Nov 19, 2024
d90d9df
Revert changed project guids
edvilme Nov 20, 2024
63c1fda
Fix project config tests
edvilme Nov 21, 2024
6f0cc41
Fix tests
edvilme Nov 21, 2024
7cf1a37
Fix all tests + Update vs-solutionpersistence
edvilme Nov 21, 2024
0c83e51
Nit
edvilme Nov 21, 2024
32cae25
Nit
edvilme Nov 21, 2024
0b3b696
Nit
edvilme Nov 21, 2024
a4a9f74
Fix whitespaces
edvilme Nov 21, 2024
14bac00
[TEST] sln-add: Add example slnx files, and parameters
edvilme Nov 22, 2024
96274cf
[TEST] sln-add: Use vs-solutionpersistence on templates
edvilme Nov 22, 2024
68f8de2
[TEST] sln-add: Add SolutionFilesTemplates
edvilme Nov 22, 2024
f1a735f
[TEST] sln-add: Compare slnx templates
edvilme Nov 23, 2024
9492a03
[TEST] sln-add: Fix sln-templates tests
edvilme Nov 25, 2024
6f8c166
[TEST] sln-add: Migrate all tests
edvilme Nov 26, 2024
13c8cd1
Fix typo
edvilme Nov 26, 2024
2dd69b0
[TEST] sln-list: Update testAsset identifiers
edvilme Nov 26, 2024
6e62402
[TEST] restore: Add App.sln to argument list
edvilme Nov 26, 2024
8992fce
Merge branch 'main' into edvilme-slnx-add
edvilme Nov 26, 2024
12533a5
Merge branch 'main' into edvilme-slnx-add
edvilme Nov 27, 2024
d8fbc8e
sln-add: When working and solution directories are different, resolve…
edvilme Nov 27, 2024
c3bac1d
Merge branch 'edvilme-slnx-add' of https://github.com/edvilme/sdk int…
edvilme Nov 27, 2024
f7a4fb7
Nit
edvilme Nov 27, 2024
5b5190e
Nit
edvilme Dec 2, 2024
931d2df
Nit
edvilme Dec 2, 2024
41c7c38
Nit
edvilme Dec 2, 2024
b0bff17
Nit: Update comments for readability
edvilme Dec 3, 2024
96cb8a0
Remove unused code
edvilme Dec 3, 2024
0f118ef
NIT: Make default solution configuration constant
edvilme Dec 4, 2024
bf80cbd
Merge branch 'main' into edvilme-slnx-add
edvilme Dec 4, 2024
74193d2
[FIX] sln-add: Default solution folders for nested projects
edvilme Dec 5, 2024
5205f1b
Nit: Remove comment
edvilme Dec 5, 2024
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
4 changes: 2 additions & 2 deletions src/Cli/dotnet/commands/dotnet-sln/LocalizableStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -184,9 +184,9 @@
<value>.slnx file {0} generated.</value>
</data>
<data name="CannotMigrateSlnx" xml:space="preserve">
<value>Only .sln files can be migrated to .slnx format.</value>
<value>Cannot migrate .slnx file.</value>
edvilme marked this conversation as resolved.
Show resolved Hide resolved
</data>
<data name="SerializerNotFound" xml:space="preserve">
<value>Could not read solution file {0}. Supported files are .sln and .slnx valid solutions.</value>
<value>Could not find serializer for file {0}.</value>
edvilme marked this conversation as resolved.
Show resolved Hide resolved
</data>
</root>
182 changes: 92 additions & 90 deletions src/Cli/dotnet/commands/dotnet-sln/add/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,143 +2,145 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.CommandLine;
using System.Linq.Expressions;
edvilme marked this conversation as resolved.
Show resolved Hide resolved
using System.Text.RegularExpressions;
using Microsoft.Build.Construction;
using Microsoft.Build.Exceptions;
using Microsoft.DotNet.Cli;
using Microsoft.DotNet.Cli.Sln.Internal;
using Microsoft.DotNet.Cli.Utils;
using Microsoft.DotNet.Tools.Common;
using Microsoft.VisualStudio.SolutionPersistence;
using Microsoft.VisualStudio.SolutionPersistence.Model;
using Microsoft.VisualStudio.SolutionPersistence.Serializer.SlnV12;

namespace Microsoft.DotNet.Tools.Sln.Add
{
internal class AddProjectToSolutionCommand : CommandBase
{
private readonly string _fileOrDirectory;
private readonly bool _inRoot;
private readonly IList<string> _relativeRootSolutionFolders;
private readonly IReadOnlyCollection<string> _arguments;
private readonly IReadOnlyCollection<string> _projects;
private readonly string? _solutionFolderPath;

public AddProjectToSolutionCommand(ParseResult parseResult) : base(parseResult)
{
_fileOrDirectory = parseResult.GetValue(SlnCommandParser.SlnArgument);

_arguments = parseResult.GetValue(SlnAddParser.ProjectPathArgument)?.ToArray() ?? (IReadOnlyCollection<string>)Array.Empty<string>();
_projects = parseResult.GetValue(SlnAddParser.ProjectPathArgument)?.ToArray() ?? (IReadOnlyCollection<string>)Array.Empty<string>();

_inRoot = parseResult.GetValue(SlnAddParser.InRootOption);
string relativeRoot = parseResult.GetValue(SlnAddParser.SolutionFolderOption);
_solutionFolderPath = parseResult.GetValue(SlnAddParser.SolutionFolderOption);

SlnArgumentValidator.ParseAndValidateArguments(_fileOrDirectory, _arguments, SlnArgumentValidator.CommandType.Add, _inRoot, relativeRoot);

bool hasRelativeRoot = !string.IsNullOrEmpty(relativeRoot);

if (hasRelativeRoot)
{
relativeRoot = PathUtility.GetPathWithDirectorySeparator(relativeRoot);
_relativeRootSolutionFolders = relativeRoot.Split(Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries);
}
else
{
_relativeRootSolutionFolders = null;
}
SlnArgumentValidator.ParseAndValidateArguments(_fileOrDirectory, _projects, SlnArgumentValidator.CommandType.Add, _inRoot, _solutionFolderPath);
edvilme marked this conversation as resolved.
Show resolved Hide resolved
}

public override int Execute()
{
SlnFile slnFile = SlnFileFactory.CreateFromFileOrDirectory(_fileOrDirectory);
var solutionFileFullPath = SlnCommandParser.GetSlnFileFullPath(_fileOrDirectory);

var arguments = (_parseResult.GetValue<IEnumerable<string>>(SlnAddParser.ProjectPathArgument) ?? Array.Empty<string>()).ToList().AsReadOnly();
if (arguments.Count == 0)
if (_projects.Count == 0)
edvilme marked this conversation as resolved.
Show resolved Hide resolved
{
throw new GracefulException(CommonLocalizableStrings.SpecifyAtLeastOneProjectToAdd);
}

PathUtility.EnsureAllPathsExist(arguments, CommonLocalizableStrings.CouldNotFindProjectOrDirectory, true);

var fullProjectPaths = _arguments.Select(p =>
PathUtility.EnsureAllPathsExist(_projects, CommonLocalizableStrings.CouldNotFindProjectOrDirectory, true);
try
edvilme marked this conversation as resolved.
Show resolved Hide resolved
{
var fullPath = Path.GetFullPath(p);
return Directory.Exists(fullPath) ?
MsbuildProject.GetProjectFileFromDirectory(fullPath).FullName :
fullPath;
}).ToList();

var preAddProjectCount = slnFile.Projects.Count;

foreach (var fullProjectPath in fullProjectPaths)
var fullProjectPaths = _projects.Select(project =>
{
var fullPath = Path.GetFullPath(project);
return Directory.Exists(fullPath) ? MsbuildProject.GetProjectFileFromDirectory(fullPath).FullName : fullPath;
});
AddProjectsToSolutionAsync(solutionFileFullPath, fullProjectPaths, CancellationToken.None).Wait();
edvilme marked this conversation as resolved.
Show resolved Hide resolved
edvilme marked this conversation as resolved.
Show resolved Hide resolved
return 0;
}
catch (GracefulException)
edvilme marked this conversation as resolved.
Show resolved Hide resolved
{
// Identify the intended solution folders
var solutionFolders = DetermineSolutionFolder(slnFile, fullProjectPath);

slnFile.AddProject(fullProjectPath, solutionFolders);
throw;
}

if (slnFile.Projects.Count > preAddProjectCount)
catch (SolutionException ex)
{
slnFile.Write();
throw new GracefulException(CommonLocalizableStrings.InvalidSolutionFormatString, solutionFileFullPath, ex.Message);
}

return 0;
}

private static IList<string> GetSolutionFoldersFromProjectPath(string projectFilePath)
{
var solutionFolders = new List<string>();

if (!IsPathInTreeRootedAtSolutionDirectory(projectFilePath))
return solutionFolders;

var currentDirString = $".{Path.DirectorySeparatorChar}";
if (projectFilePath.StartsWith(currentDirString))
catch (Exception ex)
{
projectFilePath = projectFilePath.Substring(currentDirString.Length);
throw new GracefulException(ex.Message, ex);
}

var projectDirectoryPath = TrimProject(projectFilePath);
if (string.IsNullOrEmpty(projectDirectoryPath))
return solutionFolders;

var solutionFoldersPath = TrimProjectDirectory(projectDirectoryPath);
if (string.IsNullOrEmpty(solutionFoldersPath))
return solutionFolders;

solutionFolders.AddRange(solutionFoldersPath.Split(Path.DirectorySeparatorChar));

return solutionFolders;
}

private IList<string> DetermineSolutionFolder(SlnFile slnFile, string fullProjectPath)
private async Task AddProjectsToSolutionAsync(string solutionFileFullPath, IEnumerable<string> projectPaths, CancellationToken cancellationToken)
{
if (_inRoot)
ISolutionSerializer serializer = SlnCommandParser.GetSolutionSerializer(solutionFileFullPath);
nagilson marked this conversation as resolved.
Show resolved Hide resolved
SolutionModel solution = await serializer.OpenAsync(solutionFileFullPath, cancellationToken);
// set UTF8 BOM encoding for .sln
if (serializer is ISolutionSerializer<SlnV12SerializerSettings> v12Serializer)
nagilson marked this conversation as resolved.
Show resolved Hide resolved
{
// The user requested all projects go to the root folder
return null;
solution.SerializerExtension = v12Serializer.CreateModelExtension(new()
{
Encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: true)
});
}
edvilme marked this conversation as resolved.
Show resolved Hide resolved

if (_relativeRootSolutionFolders != null)
SolutionFolderModel? solutionFolder = (!_inRoot && _solutionFolderPath != null)
? solution.AddFolder(GetSolutionFolderPathWithForwardSlashes())
: null;
foreach (var projectPath in projectPaths)
{
// The user has specified an explicit root
return _relativeRootSolutionFolders;
// Get full project path
var relativePath = Path.GetRelativePath(Path.GetDirectoryName(solutionFileFullPath), projectPath);
try
edvilme marked this conversation as resolved.
Show resolved Hide resolved
{
// Try to open the project to see if it is valid
ProjectRootElement project = ProjectRootElement.Open(projectPath);
AddProjectWithDefaultGuid(solution, relativePath, solutionFolder, project.GetProjectTypeGuid());
Reporter.Output.WriteLine(CommonLocalizableStrings.ProjectAddedToTheSolution, relativePath);
}
catch (InvalidProjectFileException ex)
{
Reporter.Error.WriteLine(string.Format(
CommonLocalizableStrings.InvalidProjectWithExceptionMessage, projectPath, ex.Message));
}
catch (ArgumentException ex)
{
// TODO: There are some cases where the project is not found but it already exists on the solution. So it is useful to check the error message. Will remove on future commit.
if (solution.FindProject(relativePath) != null || Regex.Match(ex.Message, @"Project name '.*' already exists in the solution folder.").Success)
edvilme marked this conversation as resolved.
Show resolved Hide resolved
{
Reporter.Output.WriteLine(CommonLocalizableStrings.SolutionAlreadyContainsProject, solutionFileFullPath, relativePath);
}
else
{
throw;
}
}
}

// We determine the root for each individual project
var relativeProjectPath = Path.GetRelativePath(
PathUtility.EnsureTrailingSlash(slnFile.BaseDirectory),
fullProjectPath);

return GetSolutionFoldersFromProjectPath(relativeProjectPath);
}

private static bool IsPathInTreeRootedAtSolutionDirectory(string path)
{
return !path.StartsWith("..");
solution.DistillProjectConfigurations();
await serializer.SaveAsync(solutionFileFullPath, solution, cancellationToken);
}

private static string TrimProject(string path)
private string GetSolutionFolderPathWithForwardSlashes()
{
return Path.GetDirectoryName(path);
// SolutionModel::AddFolder expects path to have leading, trailing and inner forward slashes
return "/" + string.Join("/", PathUtility.GetPathWithDirectorySeparator(_solutionFolderPath).Split(Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries)) + "/";
}

private static string TrimProjectDirectory(string path)
private void AddProjectWithDefaultGuid(SolutionModel solution, string relativePath, SolutionFolderModel solutionFolder, string guid)
{
return Path.GetDirectoryName(path);
SolutionProjectModel project;
try
{
solution.AddProject(relativePath, guid, solutionFolder);
edvilme marked this conversation as resolved.
Show resolved Hide resolved
}
catch (ArgumentException ex)
{
// TODO: Update with error codes from vs-solutionpersistence
if (ex.Message == "ProjectType '' not found. (Parameter 'projectTypeName')")
{
solution.AddProject(relativePath, "130159A9-F047-44B3-88CF-0CF7F02ED50F", solutionFolder);
}
else
{
edvilme marked this conversation as resolved.
Show resolved Hide resolved
throw;
}
}
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading