Skip to content

Commit ef91c4e

Browse files
edvilmekasperk81
andauthored
sln-remove: Support for slnx (#45160)
Co-authored-by: kasperk81 <[email protected]>
1 parent 5faffec commit ef91c4e

20 files changed

+657
-363
lines changed

src/Cli/dotnet/commands/dotnet-sln/remove/Program.cs

+101-23
Original file line numberDiff line numberDiff line change
@@ -2,58 +2,136 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System.CommandLine;
5+
using Microsoft.Build.Construction;
6+
using Microsoft.Build.Execution;
57
using Microsoft.DotNet.Cli;
6-
using Microsoft.DotNet.Cli.Sln.Internal;
8+
using Microsoft.DotNet.Cli.Utils;
79
using Microsoft.DotNet.Tools.Common;
10+
using Microsoft.Extensions.EnvironmentAbstractions;
11+
using Microsoft.VisualStudio.SolutionPersistence;
12+
using Microsoft.VisualStudio.SolutionPersistence.Model;
13+
using Microsoft.VisualStudio.SolutionPersistence.Serializer.SlnV12;
814

915
namespace Microsoft.DotNet.Tools.Sln.Remove
1016
{
1117
internal class RemoveProjectFromSolutionCommand : CommandBase
1218
{
1319
private readonly string _fileOrDirectory;
14-
private readonly IReadOnlyCollection<string> _arguments;
20+
private readonly IReadOnlyCollection<string> _projects;
21+
22+
private int CountNonFolderDescendants(
23+
SolutionModel solution,
24+
SolutionFolderModel item,
25+
Dictionary<SolutionFolderModel, SolutionItemModel[]> solutionItemsGroupedByParent,
26+
Dictionary<SolutionFolderModel, int> cached)
27+
{
28+
if (cached.ContainsKey(item))
29+
{
30+
return cached[item];
31+
}
32+
int count = item.Files?.Count ?? 0;
33+
var children = solutionItemsGroupedByParent.TryGetValue(item, out var items) ? items : Array.Empty<SolutionItemModel>();
34+
foreach (var child in children)
35+
{
36+
count += child is SolutionFolderModel folderModel
37+
? CountNonFolderDescendants(solution, folderModel, solutionItemsGroupedByParent, cached)
38+
: 1;
39+
}
40+
cached.Add(item, count);
41+
return count;
42+
}
1543

1644
public RemoveProjectFromSolutionCommand(ParseResult parseResult) : base(parseResult)
1745
{
1846
_fileOrDirectory = parseResult.GetValue(SlnCommandParser.SlnArgument);
1947

20-
_arguments = (parseResult.GetValue(SlnRemoveParser.ProjectPathArgument) ?? Array.Empty<string>()).ToList().AsReadOnly();
48+
_projects = (parseResult.GetValue(SlnRemoveParser.ProjectPathArgument) ?? Array.Empty<string>()).ToList().AsReadOnly();
2149

22-
SlnArgumentValidator.ParseAndValidateArguments(_fileOrDirectory, _arguments, SlnArgumentValidator.CommandType.Remove);
50+
SlnArgumentValidator.ParseAndValidateArguments(_fileOrDirectory, _projects, SlnArgumentValidator.CommandType.Remove);
2351
}
2452

2553
public override int Execute()
2654
{
27-
SlnFile slnFile = SlnFileFactory.CreateFromFileOrDirectory(_fileOrDirectory);
55+
string solutionFileFullPath = SlnCommandParser.GetSlnFileFullPath(_fileOrDirectory);
56+
if (_projects.Count == 0)
57+
{
58+
throw new GracefulException(CommonLocalizableStrings.SpecifyAtLeastOneProjectToRemove);
59+
}
60+
61+
try
62+
{
63+
var relativeProjectPaths = _projects.Select(p =>
64+
{
65+
var fullPath = Path.GetFullPath(p);
66+
return Path.GetRelativePath(
67+
Path.GetDirectoryName(solutionFileFullPath),
68+
Directory.Exists(fullPath)
69+
? MsbuildProject.GetProjectFileFromDirectory(fullPath).FullName
70+
: fullPath);
71+
});
72+
RemoveProjectsAsync(solutionFileFullPath, relativeProjectPaths, CancellationToken.None).Wait();
73+
return 0;
74+
}
75+
catch (Exception ex) when (ex is not GracefulException)
76+
{
77+
if (ex is SolutionException || ex.InnerException is SolutionException)
78+
{
79+
throw new GracefulException(CommonLocalizableStrings.InvalidSolutionFormatString, solutionFileFullPath, ex.Message);
80+
}
81+
if (ex.InnerException is GracefulException)
82+
{
83+
throw ex.InnerException;
84+
}
85+
throw new GracefulException(ex.Message, ex);
86+
}
87+
}
88+
89+
private async Task RemoveProjectsAsync(string solutionFileFullPath, IEnumerable<string> projectPaths, CancellationToken cancellationToken)
90+
{
91+
ISolutionSerializer serializer = SlnCommandParser.GetSolutionSerializer(solutionFileFullPath);
92+
SolutionModel solution = await serializer.OpenAsync(solutionFileFullPath, cancellationToken);
2893

29-
var baseDirectory = PathUtility.EnsureTrailingSlash(slnFile.BaseDirectory);
30-
var relativeProjectPaths = _arguments.Select(p =>
94+
// set UTF8 BOM encoding for .sln
95+
if (serializer is ISolutionSerializer<SlnV12SerializerSettings> v12Serializer)
3196
{
32-
var fullPath = Path.GetFullPath(p);
33-
return Path.GetRelativePath(
34-
baseDirectory,
35-
Directory.Exists(fullPath) ?
36-
MsbuildProject.GetProjectFileFromDirectory(fullPath).FullName :
37-
fullPath
38-
);
39-
});
97+
solution.SerializerExtension = v12Serializer.CreateModelExtension(new()
98+
{
99+
Encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: true)
100+
});
101+
}
40102

41-
bool slnChanged = false;
42-
foreach (var path in relativeProjectPaths)
103+
foreach (var projectPath in projectPaths)
43104
{
44-
slnChanged |= slnFile.RemoveProject(path);
105+
var project = solution.FindProject(projectPath);
106+
if (project != null)
107+
{
108+
solution.RemoveProject(project);
109+
Reporter.Output.WriteLine(CommonLocalizableStrings.ProjectRemovedFromTheSolution, projectPath);
110+
}
111+
else
112+
{
113+
Reporter.Output.WriteLine(CommonLocalizableStrings.ProjectNotFoundInTheSolution, projectPath);
114+
}
45115
}
46116

47-
slnFile.RemoveEmptyConfigurationSections();
117+
Dictionary<SolutionFolderModel, SolutionItemModel[]> solutionItemsGroupedByParent = solution.SolutionItems
118+
.Where(i => i.Parent != null)
119+
.GroupBy(i => i.Parent)
120+
.ToDictionary(g => g.Key, g => g.ToArray());
48121

49-
slnFile.RemoveEmptySolutionFolders();
122+
Dictionary<SolutionFolderModel, int> nonFolderDescendantsCount = new();
123+
foreach (var item in solution.SolutionFolders)
124+
{
125+
CountNonFolderDescendants(solution, item, solutionItemsGroupedByParent, nonFolderDescendantsCount);
126+
}
50127

51-
if (slnChanged)
128+
var emptyFolders = nonFolderDescendantsCount.Where(i => i.Value == 0).Select(i => i.Key);
129+
foreach (var folder in emptyFolders)
52130
{
53-
slnFile.Write();
131+
solution.RemoveFolder(folder);
54132
}
55133

56-
return 0;
134+
await serializer.SaveAsync(solutionFileFullPath, solution, cancellationToken);
57135
}
58136
}
59137
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<Solution>
2+
<Folder Name="/EmptyFolder/" />
3+
<Folder Name="/EmptyFolder/NestedEmptyFolder/" />
4+
<Folder Name="/NestedSolution/" />
5+
<Folder Name="/NestedSolution/NestedFolder/" />
6+
<Folder Name="/NestedSolution/NestedFolder/NestedFolder/">
7+
<Project Path="ConsoleApp2/ConsoleApp2.csproj" />
8+
</Folder>
9+
<Folder Name="/NewFolder1/" />
10+
<Folder Name="/NewFolder1/NewFolder2/">
11+
<File Path="TextFile1.txt" />
12+
</Folder>
13+
<Folder Name="/Root Empty Folder/" />
14+
<Project Path="ConsoleApp1/ConsoleApp1.csproj" />
15+
</Solution>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
Microsoft Visual Studio Solution File, Format Version 12.00
2+
# Visual Studio 15
3+
VisualStudioVersion = 15.0.26006.2
4+
MinimumVisualStudioVersion = 10.0.40219.1
5+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "App", "App\App.csproj", "{7072A694-548F-4CAE-A58F-12D257D5F486}"
6+
EndProject
7+
Global
8+
GlobalSection(SolutionConfigurationPlatforms) = preSolution
9+
Debug|Any CPU = Debug|Any CPU
10+
Debug|x64 = Debug|x64
11+
Debug|x86 = Debug|x86
12+
Release|Any CPU = Release|Any CPU
13+
Release|x64 = Release|x64
14+
Release|x86 = Release|x86
15+
EndGlobalSection
16+
GlobalSection(ProjectConfigurationPlatforms) = postSolution
17+
{7072A694-548F-4CAE-A58F-12D257D5F486}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
18+
{7072A694-548F-4CAE-A58F-12D257D5F486}.Debug|Any CPU.Build.0 = Debug|Any CPU
19+
{7072A694-548F-4CAE-A58F-12D257D5F486}.Debug|x64.ActiveCfg = Debug|x64
20+
{7072A694-548F-4CAE-A58F-12D257D5F486}.Debug|x64.Build.0 = Debug|x64
21+
{7072A694-548F-4CAE-A58F-12D257D5F486}.Debug|x86.ActiveCfg = Debug|x86
22+
{7072A694-548F-4CAE-A58F-12D257D5F486}.Debug|x86.Build.0 = Debug|x86
23+
{7072A694-548F-4CAE-A58F-12D257D5F486}.Release|Any CPU.ActiveCfg = Release|Any CPU
24+
{7072A694-548F-4CAE-A58F-12D257D5F486}.Release|Any CPU.Build.0 = Release|Any CPU
25+
{7072A694-548F-4CAE-A58F-12D257D5F486}.Release|x64.ActiveCfg = Release|x64
26+
{7072A694-548F-4CAE-A58F-12D257D5F486}.Release|x64.Build.0 = Release|x64
27+
{7072A694-548F-4CAE-A58F-12D257D5F486}.Release|x86.ActiveCfg = Release|x86
28+
{7072A694-548F-4CAE-A58F-12D257D5F486}.Release|x86.Build.0 = Release|x86
29+
EndGlobalSection
30+
GlobalSection(SolutionProperties) = preSolution
31+
HideSolutionNode = FALSE
32+
EndGlobalSection
33+
EndGlobal
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<Solution>
2+
<Configurations>
3+
<Platform Name="Any CPU" />
4+
<Platform Name="x64" />
5+
<Platform Name="x86" />
6+
</Configurations>
7+
<Project Path="App/App.csproj">
8+
<Platform Solution="*|x64" Project="x64" />
9+
<Platform Solution="*|x86" Project="x86" />
10+
</Project>
11+
</Solution>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
Microsoft Visual Studio Solution File, Format Version 12.00
2+
# Visual Studio 15
3+
VisualStudioVersion = 15.0.26006.2
4+
MinimumVisualStudioVersion = 10.0.40219.1
5+
Global
6+
GlobalSection(SolutionConfigurationPlatforms) = preSolution
7+
Debug|Any CPU = Debug|Any CPU
8+
Debug|x64 = Debug|x64
9+
Debug|x86 = Debug|x86
10+
Release|Any CPU = Release|Any CPU
11+
Release|x64 = Release|x64
12+
Release|x86 = Release|x86
13+
EndGlobalSection
14+
GlobalSection(SolutionProperties) = preSolution
15+
HideSolutionNode = FALSE
16+
EndGlobalSection
17+
EndGlobal
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<Solution>
2+
<Configurations>
3+
<Platform Name="Any CPU" />
4+
<Platform Name="x64" />
5+
<Platform Name="x86" />
6+
</Configurations>
7+
</Solution>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
Microsoft Visual Studio Solution File, Format Version 12.00
2+
# Visual Studio 15
3+
VisualStudioVersion = 15.0.26006.2
4+
MinimumVisualStudioVersion = 10.0.40219.1
5+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "App", "App.csproj", "{7072A694-548F-4CAE-A58F-12D257D5F486}"
6+
EndProject
7+
Global
8+
GlobalSection(SolutionConfigurationPlatforms) = preSolution
9+
Debug|Any CPU = Debug|Any CPU
10+
Debug|x64 = Debug|x64
11+
Debug|x86 = Debug|x86
12+
Release|Any CPU = Release|Any CPU
13+
Release|x64 = Release|x64
14+
Release|x86 = Release|x86
15+
EndGlobalSection
16+
GlobalSection(ProjectConfigurationPlatforms) = postSolution
17+
{7072A694-548F-4CAE-A58F-12D257D5F486}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
18+
{7072A694-548F-4CAE-A58F-12D257D5F486}.Debug|Any CPU.Build.0 = Debug|Any CPU
19+
{7072A694-548F-4CAE-A58F-12D257D5F486}.Debug|x64.ActiveCfg = Debug|x64
20+
{7072A694-548F-4CAE-A58F-12D257D5F486}.Debug|x64.Build.0 = Debug|x64
21+
{7072A694-548F-4CAE-A58F-12D257D5F486}.Debug|x86.ActiveCfg = Debug|x86
22+
{7072A694-548F-4CAE-A58F-12D257D5F486}.Debug|x86.Build.0 = Debug|x86
23+
{7072A694-548F-4CAE-A58F-12D257D5F486}.Release|Any CPU.ActiveCfg = Release|Any CPU
24+
{7072A694-548F-4CAE-A58F-12D257D5F486}.Release|Any CPU.Build.0 = Release|Any CPU
25+
{7072A694-548F-4CAE-A58F-12D257D5F486}.Release|x64.ActiveCfg = Release|x64
26+
{7072A694-548F-4CAE-A58F-12D257D5F486}.Release|x64.Build.0 = Release|x64
27+
{7072A694-548F-4CAE-A58F-12D257D5F486}.Release|x86.ActiveCfg = Release|x86
28+
{7072A694-548F-4CAE-A58F-12D257D5F486}.Release|x86.Build.0 = Release|x86
29+
EndGlobalSection
30+
GlobalSection(SolutionProperties) = preSolution
31+
HideSolutionNode = FALSE
32+
EndGlobalSection
33+
EndGlobal
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<Solution>
2+
<Configurations>
3+
<Platform Name="Any CPU" />
4+
<Platform Name="x64" />
5+
<Platform Name="x86" />
6+
</Configurations>
7+
<Project Path="App.csproj">
8+
<Platform Solution="*|x64" Project="x64" />
9+
<Platform Solution="*|x86" Project="x86" />
10+
</Project>
11+
</Solution>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
Microsoft Visual Studio Solution File, Format Version 12.00
2+
# Visual Studio 15
3+
VisualStudioVersion = 15.0.26006.2
4+
MinimumVisualStudioVersion = 10.0.40219.1
5+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "App", "App.csproj", "{7072A694-548F-4CAE-A58F-12D257D5F486}"
6+
EndProject
7+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{7B86CE74-F620-4B32-99FE-82D40F8D6BF2}"
8+
EndProject
9+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Lib", "Lib", "{EAB71280-AF32-4531-8703-43CDBA261AA3}"
10+
EndProject
11+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Lib", "src\Lib\Lib.csproj", "{84A45D44-B677-492D-A6DA-B3A71135AB8E}"
12+
EndProject
13+
Global
14+
GlobalSection(SolutionConfigurationPlatforms) = preSolution
15+
Debug|Any CPU = Debug|Any CPU
16+
Debug|x64 = Debug|x64
17+
Debug|x86 = Debug|x86
18+
Release|Any CPU = Release|Any CPU
19+
Release|x64 = Release|x64
20+
Release|x86 = Release|x86
21+
EndGlobalSection
22+
GlobalSection(ProjectConfigurationPlatforms) = postSolution
23+
{7072A694-548F-4CAE-A58F-12D257D5F486}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
24+
{7072A694-548F-4CAE-A58F-12D257D5F486}.Debug|Any CPU.Build.0 = Debug|Any CPU
25+
{7072A694-548F-4CAE-A58F-12D257D5F486}.Debug|x64.ActiveCfg = Debug|x64
26+
{7072A694-548F-4CAE-A58F-12D257D5F486}.Debug|x64.Build.0 = Debug|x64
27+
{7072A694-548F-4CAE-A58F-12D257D5F486}.Debug|x86.ActiveCfg = Debug|x86
28+
{7072A694-548F-4CAE-A58F-12D257D5F486}.Debug|x86.Build.0 = Debug|x86
29+
{7072A694-548F-4CAE-A58F-12D257D5F486}.Release|Any CPU.ActiveCfg = Release|Any CPU
30+
{7072A694-548F-4CAE-A58F-12D257D5F486}.Release|Any CPU.Build.0 = Release|Any CPU
31+
{7072A694-548F-4CAE-A58F-12D257D5F486}.Release|x64.ActiveCfg = Release|x64
32+
{7072A694-548F-4CAE-A58F-12D257D5F486}.Release|x64.Build.0 = Release|x64
33+
{7072A694-548F-4CAE-A58F-12D257D5F486}.Release|x86.ActiveCfg = Release|x86
34+
{7072A694-548F-4CAE-A58F-12D257D5F486}.Release|x86.Build.0 = Release|x86
35+
{84A45D44-B677-492D-A6DA-B3A71135AB8E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
36+
{84A45D44-B677-492D-A6DA-B3A71135AB8E}.Debug|Any CPU.Build.0 = Debug|Any CPU
37+
{84A45D44-B677-492D-A6DA-B3A71135AB8E}.Debug|x64.ActiveCfg = Debug|x64
38+
{84A45D44-B677-492D-A6DA-B3A71135AB8E}.Debug|x64.Build.0 = Debug|x64
39+
{84A45D44-B677-492D-A6DA-B3A71135AB8E}.Debug|x86.ActiveCfg = Debug|x86
40+
{84A45D44-B677-492D-A6DA-B3A71135AB8E}.Debug|x86.Build.0 = Debug|x86
41+
{84A45D44-B677-492D-A6DA-B3A71135AB8E}.Release|Any CPU.ActiveCfg = Release|Any CPU
42+
{84A45D44-B677-492D-A6DA-B3A71135AB8E}.Release|Any CPU.Build.0 = Release|Any CPU
43+
{84A45D44-B677-492D-A6DA-B3A71135AB8E}.Release|x64.ActiveCfg = Release|x64
44+
{84A45D44-B677-492D-A6DA-B3A71135AB8E}.Release|x64.Build.0 = Release|x64
45+
{84A45D44-B677-492D-A6DA-B3A71135AB8E}.Release|x86.ActiveCfg = Release|x86
46+
{84A45D44-B677-492D-A6DA-B3A71135AB8E}.Release|x86.Build.0 = Release|x86
47+
EndGlobalSection
48+
GlobalSection(SolutionProperties) = preSolution
49+
HideSolutionNode = FALSE
50+
EndGlobalSection
51+
GlobalSection(NestedProjects) = preSolution
52+
{EAB71280-AF32-4531-8703-43CDBA261AA3} = {7B86CE74-F620-4B32-99FE-82D40F8D6BF2}
53+
{84A45D44-B677-492D-A6DA-B3A71135AB8E} = {EAB71280-AF32-4531-8703-43CDBA261AA3}
54+
EndGlobalSection
55+
EndGlobal
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<Solution>
2+
<Configurations>
3+
<Platform Name="Any CPU" />
4+
<Platform Name="x64" />
5+
<Platform Name="x86" />
6+
</Configurations>
7+
<Folder Name="/src/" />
8+
<Folder Name="/src/Lib/">
9+
<Project Path="src/Lib/Lib.csproj">
10+
<Platform Solution="*|x64" Project="x64" />
11+
<Platform Solution="*|x86" Project="x86" />
12+
</Project>
13+
</Folder>
14+
<Project Path="App.csproj">
15+
<Platform Solution="*|x64" Project="x64" />
16+
<Platform Solution="*|x86" Project="x86" />
17+
</Project>
18+
</Solution>

0 commit comments

Comments
 (0)