diff --git a/TestAssets/NonRestoredTestProjects/PJAppWithSlnAndSolutionItemsToMoveToBackup/NoSolutionItemsAfterMigration.sln b/TestAssets/NonRestoredTestProjects/PJAppWithSlnAndSolutionItemsToMoveToBackup/NoSolutionItemsAfterMigration.sln new file mode 100644 index 000000000..da0ae5935 --- /dev/null +++ b/TestAssets/NonRestoredTestProjects/PJAppWithSlnAndSolutionItemsToMoveToBackup/NoSolutionItemsAfterMigration.sln @@ -0,0 +1,39 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.26006.2 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestApp", "TestApp\TestApp.xproj", "{D65E5A1F-719F-4F95-8835-88BDD67AD457}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{FAACC4BE-31AE-4EB7-A4C8-5BB4617EB4AF}" + ProjectSection(SolutionItems) = preProject + global.json = global.json + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {D65E5A1F-719F-4F95-8835-88BDD67AD457}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D65E5A1F-719F-4F95-8835-88BDD67AD457}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D65E5A1F-719F-4F95-8835-88BDD67AD457}.Debug|x64.ActiveCfg = Debug|x64 + {D65E5A1F-719F-4F95-8835-88BDD67AD457}.Debug|x64.Build.0 = Debug|x64 + {D65E5A1F-719F-4F95-8835-88BDD67AD457}.Debug|x86.ActiveCfg = Debug|x86 + {D65E5A1F-719F-4F95-8835-88BDD67AD457}.Debug|x86.Build.0 = Debug|x86 + {D65E5A1F-719F-4F95-8835-88BDD67AD457}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D65E5A1F-719F-4F95-8835-88BDD67AD457}.Release|Any CPU.Build.0 = Release|Any CPU + {D65E5A1F-719F-4F95-8835-88BDD67AD457}.Release|x64.ActiveCfg = Release|x64 + {D65E5A1F-719F-4F95-8835-88BDD67AD457}.Release|x64.Build.0 = Release|x64 + {D65E5A1F-719F-4F95-8835-88BDD67AD457}.Release|x86.ActiveCfg = Release|x86 + {D65E5A1F-719F-4F95-8835-88BDD67AD457}.Release|x86.Build.0 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/TestAssets/NonRestoredTestProjects/PJAppWithSlnAndSolutionItemsToMoveToBackup/ReadmeSolutionItemAfterMigration.sln b/TestAssets/NonRestoredTestProjects/PJAppWithSlnAndSolutionItemsToMoveToBackup/ReadmeSolutionItemAfterMigration.sln new file mode 100644 index 000000000..05aecb75d --- /dev/null +++ b/TestAssets/NonRestoredTestProjects/PJAppWithSlnAndSolutionItemsToMoveToBackup/ReadmeSolutionItemAfterMigration.sln @@ -0,0 +1,40 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.26006.2 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestApp", "TestApp\TestApp.xproj", "{D65E5A1F-719F-4F95-8835-88BDD67AD457}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{FAACC4BE-31AE-4EB7-A4C8-5BB4617EB4AF}" + ProjectSection(SolutionItems) = preProject + global.json = global.json + readme.txt = readme.txt + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {D65E5A1F-719F-4F95-8835-88BDD67AD457}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D65E5A1F-719F-4F95-8835-88BDD67AD457}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D65E5A1F-719F-4F95-8835-88BDD67AD457}.Debug|x64.ActiveCfg = Debug|x64 + {D65E5A1F-719F-4F95-8835-88BDD67AD457}.Debug|x64.Build.0 = Debug|x64 + {D65E5A1F-719F-4F95-8835-88BDD67AD457}.Debug|x86.ActiveCfg = Debug|x86 + {D65E5A1F-719F-4F95-8835-88BDD67AD457}.Debug|x86.Build.0 = Debug|x86 + {D65E5A1F-719F-4F95-8835-88BDD67AD457}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D65E5A1F-719F-4F95-8835-88BDD67AD457}.Release|Any CPU.Build.0 = Release|Any CPU + {D65E5A1F-719F-4F95-8835-88BDD67AD457}.Release|x64.ActiveCfg = Release|x64 + {D65E5A1F-719F-4F95-8835-88BDD67AD457}.Release|x64.Build.0 = Release|x64 + {D65E5A1F-719F-4F95-8835-88BDD67AD457}.Release|x86.ActiveCfg = Release|x86 + {D65E5A1F-719F-4F95-8835-88BDD67AD457}.Release|x86.Build.0 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/TestAssets/NonRestoredTestProjects/PJAppWithSlnAndSolutionItemsToMoveToBackup/TestApp/Program.cs b/TestAssets/NonRestoredTestProjects/PJAppWithSlnAndSolutionItemsToMoveToBackup/TestApp/Program.cs new file mode 100644 index 000000000..2289ac741 --- /dev/null +++ b/TestAssets/NonRestoredTestProjects/PJAppWithSlnAndSolutionItemsToMoveToBackup/TestApp/Program.cs @@ -0,0 +1,15 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +namespace TestApp +{ + public class Program + { + public static int Main(string[] args) + { + Console.WriteLine("Hello World!"); + return 0; + } + } +} diff --git a/TestAssets/NonRestoredTestProjects/PJAppWithSlnAndSolutionItemsToMoveToBackup/TestApp/TestApp.xproj b/TestAssets/NonRestoredTestProjects/PJAppWithSlnAndSolutionItemsToMoveToBackup/TestApp/TestApp.xproj new file mode 100644 index 000000000..d18702195 --- /dev/null +++ b/TestAssets/NonRestoredTestProjects/PJAppWithSlnAndSolutionItemsToMoveToBackup/TestApp/TestApp.xproj @@ -0,0 +1,18 @@ + + + + 14.0.23107 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + 0138cb8f-4aa9-4029-a21e-c07c30f425ba + TestAppWithContents + ..\..\..\artifacts\obj\$(MSBuildProjectName) + ..\..\..\artifacts\ + + + 2.0 + + + diff --git a/TestAssets/NonRestoredTestProjects/PJAppWithSlnAndSolutionItemsToMoveToBackup/TestApp/project.json b/TestAssets/NonRestoredTestProjects/PJAppWithSlnAndSolutionItemsToMoveToBackup/TestApp/project.json new file mode 100644 index 000000000..166d41c2b --- /dev/null +++ b/TestAssets/NonRestoredTestProjects/PJAppWithSlnAndSolutionItemsToMoveToBackup/TestApp/project.json @@ -0,0 +1,26 @@ +{ + "version": "1.0.0-*", + "buildOptions": { + "emitEntryPoint": true, + "preserveCompilationContext": true + }, + "dependencies": { + "Microsoft.NETCore.App": "1.0.1" + }, + "frameworks": { + "netcoreapp1.0": {} + }, + "runtimes": { + "win7-x64": {}, + "win7-x86": {}, + "osx.10.10-x64": {}, + "osx.10.11-x64": {}, + "ubuntu.14.04-x64": {}, + "ubuntu.16.04-x64": {}, + "centos.7-x64": {}, + "rhel.7.2-x64": {}, + "debian.8-x64": {}, + "fedora.23-x64": {}, + "opensuse.13.2-x64": {} + } +} diff --git a/TestAssets/NonRestoredTestProjects/PJAppWithSlnAndSolutionItemsToMoveToBackup/global.json b/TestAssets/NonRestoredTestProjects/PJAppWithSlnAndSolutionItemsToMoveToBackup/global.json new file mode 100644 index 000000000..22936715c --- /dev/null +++ b/TestAssets/NonRestoredTestProjects/PJAppWithSlnAndSolutionItemsToMoveToBackup/global.json @@ -0,0 +1,3 @@ +{ + "projects": [ "." ] +} diff --git a/TestAssets/NonRestoredTestProjects/PJAppWithSlnAndSolutionItemsToMoveToBackup/readme.txt b/TestAssets/NonRestoredTestProjects/PJAppWithSlnAndSolutionItemsToMoveToBackup/readme.txt new file mode 100644 index 000000000..6e3eb33f4 --- /dev/null +++ b/TestAssets/NonRestoredTestProjects/PJAppWithSlnAndSolutionItemsToMoveToBackup/readme.txt @@ -0,0 +1 @@ +This is just for our test to verify that we do not remove the readme.txt link from the solution. diff --git a/src/dotnet/SlnFileExtensions.cs b/src/dotnet/SlnFileExtensions.cs new file mode 100644 index 000000000..7fbf0fb70 --- /dev/null +++ b/src/dotnet/SlnFileExtensions.cs @@ -0,0 +1,330 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.Build.Construction; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Execution; +using Microsoft.DotNet.Cli.Sln.Internal; +using Microsoft.DotNet.Cli.Utils; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace Microsoft.DotNet.Tools.Common +{ + internal static class SlnFileExtensions + { + public static void AddProject(this SlnFile slnFile, string fullProjectPath) + { + if (string.IsNullOrEmpty(fullProjectPath)) + { + throw new ArgumentException(); + } + + var relativeProjectPath = PathUtility.GetRelativePath( + PathUtility.EnsureTrailingSlash(slnFile.BaseDirectory), + fullProjectPath); + + if (slnFile.Projects.Any((p) => + string.Equals(p.FilePath, relativeProjectPath, StringComparison.OrdinalIgnoreCase))) + { + Reporter.Output.WriteLine(string.Format( + CommonLocalizableStrings.SolutionAlreadyContainsProject, + slnFile.FullPath, + relativeProjectPath)); + } + else + { + var projectInstance = new ProjectInstance(fullProjectPath); + + var slnProject = new SlnProject + { + Id = projectInstance.GetProjectId(), + TypeGuid = projectInstance.GetProjectTypeGuid(), + Name = Path.GetFileNameWithoutExtension(relativeProjectPath), + FilePath = relativeProjectPath + }; + + slnFile.AddDefaultBuildConfigurations(slnProject); + + slnFile.AddSolutionFolders(slnProject); + + slnFile.Projects.Add(slnProject); + + Reporter.Output.WriteLine( + string.Format(CommonLocalizableStrings.ProjectAddedToTheSolution, relativeProjectPath)); + } + } + + public static void AddDefaultBuildConfigurations(this SlnFile slnFile, SlnProject slnProject) + { + if (slnProject == null) + { + throw new ArgumentException(); + } + + var defaultConfigurations = new List() + { + "Debug|Any CPU", + "Debug|x64", + "Debug|x86", + "Release|Any CPU", + "Release|x64", + "Release|x86", + }; + + // NOTE: The order you create the sections determines the order they are written to the sln + // file. In the case of an empty sln file, in order to make sure the solution configurations + // section comes first we need to add it first. This doesn't affect correctness but does + // stop VS from re-ordering things later on. Since we are keeping the SlnFile class low-level + // it shouldn't care about the VS implementation details. That's why we handle this here. + AddDefaultSolutionConfigurations(defaultConfigurations, slnFile.SolutionConfigurationsSection); + AddDefaultProjectConfigurations( + defaultConfigurations, + slnFile.ProjectConfigurationsSection.GetOrCreatePropertySet(slnProject.Id)); + } + + private static void AddDefaultSolutionConfigurations( + List defaultConfigurations, + SlnPropertySet solutionConfigs) + { + foreach (var config in defaultConfigurations) + { + if (!solutionConfigs.ContainsKey(config)) + { + solutionConfigs[config] = config; + } + } + } + + private static void AddDefaultProjectConfigurations( + List defaultConfigurations, + SlnPropertySet projectConfigs) + { + foreach (var config in defaultConfigurations) + { + var activeCfgKey = $"{config}.ActiveCfg"; + if (!projectConfigs.ContainsKey(activeCfgKey)) + { + projectConfigs[activeCfgKey] = config; + } + + var build0Key = $"{config}.Build.0"; + if (!projectConfigs.ContainsKey(build0Key)) + { + projectConfigs[build0Key] = config; + } + } + } + + public static void AddSolutionFolders(this SlnFile slnFile, SlnProject slnProject) + { + if (slnProject == null) + { + throw new ArgumentException(); + } + + var solutionFolders = slnProject.GetSolutionFoldersFromProject(); + + if (solutionFolders.Any()) + { + var nestedProjectsSection = slnFile.Sections.GetOrCreateSection( + "NestedProjects", + SlnSectionType.PreProcess); + + var pathToGuidMap = slnFile.GetSolutionFolderPaths(nestedProjectsSection.Properties); + + string parentDirGuid = null; + var solutionFolderHierarchy = string.Empty; + foreach (var dir in solutionFolders) + { + solutionFolderHierarchy = Path.Combine(solutionFolderHierarchy, dir); + if (pathToGuidMap.ContainsKey(solutionFolderHierarchy)) + { + parentDirGuid = pathToGuidMap[solutionFolderHierarchy]; + } + else + { + var solutionFolder = new SlnProject + { + Id = Guid.NewGuid().ToString("B").ToUpper(), + TypeGuid = ProjectTypeGuids.SolutionFolderGuid, + Name = dir, + FilePath = dir + }; + + slnFile.Projects.Add(solutionFolder); + + if (parentDirGuid != null) + { + nestedProjectsSection.Properties[solutionFolder.Id] = parentDirGuid; + } + parentDirGuid = solutionFolder.Id; + } + } + + nestedProjectsSection.Properties[slnProject.Id] = parentDirGuid; + } + } + + private static IDictionary GetSolutionFolderPaths( + this SlnFile slnFile, + SlnPropertySet nestedProjects) + { + var solutionFolderPaths = new Dictionary(); + + var solutionFolderProjects = slnFile.Projects.GetProjectsByType(ProjectTypeGuids.SolutionFolderGuid); + foreach (var slnProject in solutionFolderProjects) + { + var path = slnProject.FilePath; + var id = slnProject.Id; + while (nestedProjects.ContainsKey(id)) + { + id = nestedProjects[id]; + var parentSlnProject = solutionFolderProjects.Where(p => p.Id == id).Single(); + path = Path.Combine(parentSlnProject.FilePath, path); + } + + solutionFolderPaths[path] = slnProject.Id; + } + + return solutionFolderPaths; + } + + public static bool RemoveProject(this SlnFile slnFile, string projectPath) + { + if (string.IsNullOrEmpty(projectPath)) + { + throw new ArgumentException(); + } + + var projectPathNormalized = PathUtility.GetPathWithDirectorySeparator(projectPath); + + var projectsToRemove = slnFile.Projects.Where((p) => + string.Equals(p.FilePath, projectPathNormalized, StringComparison.OrdinalIgnoreCase)).ToList(); + + bool projectRemoved = false; + if (projectsToRemove.Count == 0) + { + Reporter.Output.WriteLine(string.Format( + CommonLocalizableStrings.ProjectReferenceCouldNotBeFound, + projectPath)); + } + else + { + foreach (var slnProject in projectsToRemove) + { + var buildConfigsToRemove = slnFile.ProjectConfigurationsSection.GetPropertySet(slnProject.Id); + if (buildConfigsToRemove != null) + { + slnFile.ProjectConfigurationsSection.Remove(buildConfigsToRemove); + } + + var nestedProjectsSection = slnFile.Sections.GetSection( + "NestedProjects", + SlnSectionType.PreProcess); + if (nestedProjectsSection != null && nestedProjectsSection.Properties.ContainsKey(slnProject.Id)) + { + nestedProjectsSection.Properties.Remove(slnProject.Id); + } + + slnFile.Projects.Remove(slnProject); + Reporter.Output.WriteLine( + string.Format(CommonLocalizableStrings.ProjectReferenceRemoved, slnProject.FilePath)); + } + + projectRemoved = true; + } + + return projectRemoved; + } + + public static void RemoveEmptyConfigurationSections(this SlnFile slnFile) + { + if (slnFile.Projects.Count == 0) + { + var solutionConfigs = slnFile.Sections.GetSection("SolutionConfigurationPlatforms"); + if (solutionConfigs != null) + { + slnFile.Sections.Remove(solutionConfigs); + } + + var projectConfigs = slnFile.Sections.GetSection("ProjectConfigurationPlatforms"); + if (projectConfigs != null) + { + slnFile.Sections.Remove(projectConfigs); + } + } + } + + public static void RemoveEmptySolutionFolders(this SlnFile slnFile) + { + var solutionFolderProjects = slnFile.Projects + .GetProjectsByType(ProjectTypeGuids.SolutionFolderGuid) + .ToList(); + + if (solutionFolderProjects.Any()) + { + var nestedProjectsSection = slnFile.Sections.GetSection( + "NestedProjects", + SlnSectionType.PreProcess); + + if (nestedProjectsSection == null) + { + foreach (var solutionFolderProject in solutionFolderProjects) + { + if (solutionFolderProject.Sections.Count() == 0) + { + slnFile.Projects.Remove(solutionFolderProject); + } + } + } + else + { + var solutionFoldersInUse = slnFile.GetSolutionFoldersThatContainProjectsInItsHierarchy( + nestedProjectsSection.Properties); + + foreach (var solutionFolderProject in solutionFolderProjects) + { + if (!solutionFoldersInUse.Contains(solutionFolderProject.Id)) + { + nestedProjectsSection.Properties.Remove(solutionFolderProject.Id); + if (solutionFolderProject.Sections.Count() == 0) + { + slnFile.Projects.Remove(solutionFolderProject); + } + } + } + + if (nestedProjectsSection.IsEmpty) + { + slnFile.Sections.Remove(nestedProjectsSection); + } + } + } + } + + private static HashSet GetSolutionFoldersThatContainProjectsInItsHierarchy( + this SlnFile slnFile, + SlnPropertySet nestedProjects) + { + var solutionFoldersInUse = new HashSet(); + + var nonSolutionFolderProjects = slnFile.Projects.GetProjectsNotOfType( + ProjectTypeGuids.SolutionFolderGuid); + + foreach (var nonSolutionFolderProject in nonSolutionFolderProjects) + { + var id = nonSolutionFolderProject.Id; + while (nestedProjects.ContainsKey(id)) + { + id = nestedProjects[id]; + solutionFoldersInUse.Add(id); + } + } + + return solutionFoldersInUse; + } + } +} diff --git a/src/dotnet/SlnProjectCollectionExtensions.cs b/src/dotnet/SlnProjectCollectionExtensions.cs index d99b34eee..09499e5a2 100644 --- a/src/dotnet/SlnProjectCollectionExtensions.cs +++ b/src/dotnet/SlnProjectCollectionExtensions.cs @@ -8,7 +8,7 @@ using System.Linq; namespace Microsoft.DotNet.Tools.Common { - public static class SlnProjectCollectionExtensions + internal static class SlnProjectCollectionExtensions { public static IEnumerable GetProjectsByType( this SlnProjectCollection projects, diff --git a/src/dotnet/SlnProjectExtensions.cs b/src/dotnet/SlnProjectExtensions.cs index 925089beb..80ebf5d3e 100644 --- a/src/dotnet/SlnProjectExtensions.cs +++ b/src/dotnet/SlnProjectExtensions.cs @@ -8,7 +8,7 @@ using System.Linq; namespace Microsoft.DotNet.Tools.Common { - public static class SlnProjectExtensions + internal static class SlnProjectExtensions { public static IList GetSolutionFoldersFromProject(this SlnProject project) { diff --git a/src/dotnet/commands/dotnet-add/dotnet-add-reference/xlf/LocalizableStrings.de.xlf b/src/dotnet/commands/dotnet-add/dotnet-add-reference/xlf/LocalizableStrings.de.xlf index becde7aba..ab216036c 100644 --- a/src/dotnet/commands/dotnet-add/dotnet-add-reference/xlf/LocalizableStrings.de.xlf +++ b/src/dotnet/commands/dotnet-add/dotnet-add-reference/xlf/LocalizableStrings.de.xlf @@ -10,7 +10,7 @@ Command to add project to project reference - Befehl zum Hinzufügen des Projekts zum Projektverweis + Befehl zum Hinzufügen eines Projekt-zu-Projekt-Verweises diff --git a/src/dotnet/commands/dotnet-migrate/MigrateCommand.cs b/src/dotnet/commands/dotnet-migrate/MigrateCommand.cs index 34c8bbc53..b50aca8d1 100644 --- a/src/dotnet/commands/dotnet-migrate/MigrateCommand.cs +++ b/src/dotnet/commands/dotnet-migrate/MigrateCommand.cs @@ -162,6 +162,8 @@ namespace Microsoft.DotNet.Tools.Migrate _slnFile.MinimumVisualStudioVersion = MinimumVisualStudioVersion; } + RemoveReferencesToMigratedFiles(_slnFile); + _slnFile.Write(); foreach (var csprojFile in csprojFilesToAdd) @@ -170,6 +172,26 @@ namespace Microsoft.DotNet.Tools.Migrate } } + private void RemoveReferencesToMigratedFiles(SlnFile slnFile) + { + var solutionFolders = slnFile.Projects.GetProjectsByType(ProjectTypeGuids.SolutionFolderGuid); + + foreach (var solutionFolder in solutionFolders) + { + var solutionItems = solutionFolder.Sections.GetSection("SolutionItems"); + if (solutionItems != null && solutionItems.Properties.ContainsKey("global.json")) + { + solutionItems.Properties.Remove("global.json"); + if (solutionItems.IsEmpty) + { + solutionFolder.Sections.Remove(solutionItems); + } + } + } + + slnFile.RemoveEmptySolutionFolders(); + } + private void AddProject(string slnPath, string csprojPath) { List args = new List() diff --git a/src/dotnet/commands/dotnet-sln/add/Program.cs b/src/dotnet/commands/dotnet-sln/add/Program.cs index 889506662..6b6124a29 100644 --- a/src/dotnet/commands/dotnet-sln/add/Program.cs +++ b/src/dotnet/commands/dotnet-sln/add/Program.cs @@ -1,9 +1,6 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -using Microsoft.Build.Construction; -using Microsoft.Build.Evaluation; -using Microsoft.Build.Execution; using Microsoft.DotNet.Cli; using Microsoft.DotNet.Cli.Sln.Internal; using Microsoft.DotNet.Cli.Utils; @@ -49,7 +46,7 @@ namespace Microsoft.DotNet.Tools.Sln.Add int preAddProjectCount = slnFile.Projects.Count; foreach (var fullProjectPath in fullProjectPaths) { - AddProject(slnFile, fullProjectPath); + slnFile.AddProject(fullProjectPath); } if (slnFile.Projects.Count > preAddProjectCount) @@ -59,165 +56,5 @@ namespace Microsoft.DotNet.Tools.Sln.Add return 0; } - - private void AddProject(SlnFile slnFile, string fullProjectPath) - { - var relativeProjectPath = PathUtility.GetRelativePath( - PathUtility.EnsureTrailingSlash(slnFile.BaseDirectory), - fullProjectPath); - - if (slnFile.Projects.Any((p) => - string.Equals(p.FilePath, relativeProjectPath, StringComparison.OrdinalIgnoreCase))) - { - Reporter.Output.WriteLine(string.Format( - CommonLocalizableStrings.SolutionAlreadyContainsProject, - slnFile.FullPath, - relativeProjectPath)); - } - else - { - var projectInstance = new ProjectInstance(fullProjectPath); - - var slnProject = new SlnProject - { - Id = projectInstance.GetProjectId(), - TypeGuid = projectInstance.GetProjectTypeGuid(), - Name = Path.GetFileNameWithoutExtension(relativeProjectPath), - FilePath = relativeProjectPath - }; - - AddDefaultBuildConfigurations(slnFile, slnProject); - - AddSolutionFolders(slnFile, slnProject); - - slnFile.Projects.Add(slnProject); - - Reporter.Output.WriteLine( - string.Format(CommonLocalizableStrings.ProjectAddedToTheSolution, relativeProjectPath)); - } - } - - private void AddDefaultBuildConfigurations(SlnFile slnFile, SlnProject slnProject) - { - var defaultConfigurations = new List() - { - "Debug|Any CPU", - "Debug|x64", - "Debug|x86", - "Release|Any CPU", - "Release|x64", - "Release|x86", - }; - - // NOTE: The order you create the sections determines the order they are written to the sln - // file. In the case of an empty sln file, in order to make sure the solution configurations - // section comes first we need to add it first. This doesn't affect correctness but does - // stop VS from re-ordering things later on. Since we are keeping the SlnFile class low-level - // it shouldn't care about the VS implementation details. That's why we handle this here. - AddDefaultSolutionConfigurations(defaultConfigurations, slnFile.SolutionConfigurationsSection); - AddDefaultProjectConfigurations( - defaultConfigurations, - slnFile.ProjectConfigurationsSection.GetOrCreatePropertySet(slnProject.Id)); - } - - private void AddDefaultSolutionConfigurations( - List defaultConfigurations, - SlnPropertySet solutionConfigs) - { - foreach (var config in defaultConfigurations) - { - if (!solutionConfigs.ContainsKey(config)) - { - solutionConfigs[config] = config; - } - } - } - - private void AddDefaultProjectConfigurations( - List defaultConfigurations, - SlnPropertySet projectConfigs) - { - foreach (var config in defaultConfigurations) - { - var activeCfgKey = $"{config}.ActiveCfg"; - if (!projectConfigs.ContainsKey(activeCfgKey)) - { - projectConfigs[activeCfgKey] = config; - } - - var build0Key = $"{config}.Build.0"; - if (!projectConfigs.ContainsKey(build0Key)) - { - projectConfigs[build0Key] = config; - } - } - } - - private void AddSolutionFolders(SlnFile slnFile, SlnProject slnProject) - { - var solutionFolders = slnProject.GetSolutionFoldersFromProject(); - - if (solutionFolders.Any()) - { - var nestedProjectsSection = slnFile.Sections.GetOrCreateSection( - "NestedProjects", - SlnSectionType.PreProcess); - - var pathToGuidMap = GetSolutionFolderPaths(slnFile, nestedProjectsSection.Properties); - - string parentDirGuid = null; - var solutionFolderHierarchy = string.Empty; - foreach (var dir in solutionFolders) - { - solutionFolderHierarchy = Path.Combine(solutionFolderHierarchy, dir); - if (pathToGuidMap.ContainsKey(solutionFolderHierarchy)) - { - parentDirGuid = pathToGuidMap[solutionFolderHierarchy]; - } - else - { - var solutionFolder = new SlnProject - { - Id = Guid.NewGuid().ToString("B").ToUpper(), - TypeGuid = ProjectTypeGuids.SolutionFolderGuid, - Name = dir, - FilePath = dir - }; - - slnFile.Projects.Add(solutionFolder); - - if (parentDirGuid != null) - { - nestedProjectsSection.Properties[solutionFolder.Id] = parentDirGuid; - } - parentDirGuid = solutionFolder.Id; - } - } - - nestedProjectsSection.Properties[slnProject.Id] = parentDirGuid; - } - } - - private IDictionary GetSolutionFolderPaths(SlnFile slnFile, SlnPropertySet nestedProjects) - { - var solutionFolderPaths = new Dictionary(); - - var solutionFolderProjects = slnFile.Projects.GetProjectsByType(ProjectTypeGuids.SolutionFolderGuid); - foreach (var slnProject in solutionFolderProjects) - { - var path = slnProject.FilePath; - var id = slnProject.Id; - while (nestedProjects.ContainsKey(id)) - { - id = nestedProjects[id]; - var parentSlnProject = solutionFolderProjects.Where(p => p.Id == id).Single(); - path = Path.Combine(parentSlnProject.FilePath, path); - } - - solutionFolderPaths[path] = slnProject.Id; - } - - return solutionFolderPaths; - } } } diff --git a/src/dotnet/commands/dotnet-sln/remove/Program.cs b/src/dotnet/commands/dotnet-sln/remove/Program.cs index 18f3866e5..c147052cd 100644 --- a/src/dotnet/commands/dotnet-sln/remove/Program.cs +++ b/src/dotnet/commands/dotnet-sln/remove/Program.cs @@ -48,12 +48,12 @@ namespace Microsoft.DotNet.Tools.Sln.Remove bool slnChanged = false; foreach (var path in relativeProjectPaths) { - slnChanged |= RemoveProject(slnFile, path); + slnChanged |= slnFile.RemoveProject(path); } - RemoveEmptyConfigurationSections(slnFile); + slnFile.RemoveEmptyConfigurationSections(); - RemoveEmptySolutionFolders(slnFile); + slnFile.RemoveEmptySolutionFolders(); if (slnChanged) { @@ -62,120 +62,5 @@ namespace Microsoft.DotNet.Tools.Sln.Remove return 0; } - - private bool RemoveProject(SlnFile slnFile, string projectPath) - { - var projectPathNormalized = PathUtility.GetPathWithDirectorySeparator(projectPath); - - var projectsToRemove = slnFile.Projects.Where((p) => - string.Equals(p.FilePath, projectPathNormalized, StringComparison.OrdinalIgnoreCase)).ToList(); - - bool projectRemoved = false; - if (projectsToRemove.Count == 0) - { - Reporter.Output.WriteLine(string.Format( - CommonLocalizableStrings.ProjectReferenceCouldNotBeFound, - projectPath)); - } - else - { - foreach (var slnProject in projectsToRemove) - { - var buildConfigsToRemove = slnFile.ProjectConfigurationsSection.GetPropertySet(slnProject.Id); - if (buildConfigsToRemove != null) - { - slnFile.ProjectConfigurationsSection.Remove(buildConfigsToRemove); - } - - var nestedProjectsSection = slnFile.Sections.GetSection( - "NestedProjects", - SlnSectionType.PreProcess); - if (nestedProjectsSection != null && nestedProjectsSection.Properties.ContainsKey(slnProject.Id)) - { - nestedProjectsSection.Properties.Remove(slnProject.Id); - } - - slnFile.Projects.Remove(slnProject); - Reporter.Output.WriteLine( - string.Format(CommonLocalizableStrings.ProjectReferenceRemoved, slnProject.FilePath)); - } - - projectRemoved = true; - } - - return projectRemoved; - } - - private void RemoveEmptyConfigurationSections(SlnFile slnFile) - { - if (slnFile.Projects.Count == 0) - { - var solutionConfigs = slnFile.Sections.GetSection("SolutionConfigurationPlatforms"); - if (solutionConfigs != null) - { - slnFile.Sections.Remove(solutionConfigs); - } - - var projectConfigs = slnFile.Sections.GetSection("ProjectConfigurationPlatforms"); - if (projectConfigs != null) - { - slnFile.Sections.Remove(projectConfigs); - } - } - } - - private void RemoveEmptySolutionFolders(SlnFile slnFile) - { - var solutionFolderProjects = slnFile.Projects - .GetProjectsByType(ProjectTypeGuids.SolutionFolderGuid) - .ToList(); - - if (solutionFolderProjects.Any()) - { - var nestedProjectsSection = slnFile.Sections.GetSection( - "NestedProjects", - SlnSectionType.PreProcess); - - var solutionFoldersInUse = GetSolutionFoldersThatContainProjectsInItsHierarchy( - slnFile, - nestedProjectsSection.Properties); - - foreach (var solutionFolderProject in solutionFolderProjects) - { - if (!solutionFoldersInUse.Contains(solutionFolderProject.Id)) - { - slnFile.Projects.Remove(solutionFolderProject); - nestedProjectsSection.Properties.Remove(solutionFolderProject.Id); - } - } - - if (nestedProjectsSection.IsEmpty) - { - slnFile.Sections.Remove(nestedProjectsSection); - } - } - } - - private HashSet GetSolutionFoldersThatContainProjectsInItsHierarchy( - SlnFile slnFile, - SlnPropertySet nestedProjects) - { - var solutionFoldersInUse = new HashSet(); - - var nonSolutionFolderProjects = slnFile.Projects.GetProjectsNotOfType( - ProjectTypeGuids.SolutionFolderGuid); - - foreach (var nonSolutionFolderProject in nonSolutionFolderProjects) - { - var id = nonSolutionFolderProject.Id; - while (nestedProjects.ContainsKey(id)) - { - id = nestedProjects[id]; - solutionFoldersInUse.Add(id); - } - } - - return solutionFoldersInUse; - } } } diff --git a/test/dotnet-migrate.Tests/GivenThatIWantToMigrateSolutions.cs b/test/dotnet-migrate.Tests/GivenThatIWantToMigrateSolutions.cs index 095518dae..cbfa8ba50 100644 --- a/test/dotnet-migrate.Tests/GivenThatIWantToMigrateSolutions.cs +++ b/test/dotnet-migrate.Tests/GivenThatIWantToMigrateSolutions.cs @@ -149,6 +149,41 @@ namespace Microsoft.DotNet.Migration.Tests cmd.StdErr.Should().BeEmpty(); } + [Theory] + [InlineData("NoSolutionItemsAfterMigration.sln", false)] + [InlineData("ReadmeSolutionItemAfterMigration.sln", true)] + public void WhenMigratingAnSlnLinksReferencingItemsMovedToBackupAreRemoved( + string slnFileName, + bool solutionItemsContainsReadme) + { + var projectDirectory = TestAssets + .GetProjectJson(TestAssetKinds.NonRestoredTestProjects, "PJAppWithSlnAndSolutionItemsToMoveToBackup") + .CreateInstance(Path.GetFileNameWithoutExtension(slnFileName)) + .WithSourceFiles() + .Root + .FullName; + + new DotnetCommand() + .WithWorkingDirectory(projectDirectory) + .Execute($"migrate \"{slnFileName}\"") + .Should().Pass(); + + var slnFile = SlnFile.Read(Path.Combine(projectDirectory, slnFileName)); + var solutionFolders = slnFile.Projects.Where(p => p.TypeGuid == ProjectTypeGuids.SolutionFolderGuid); + if (solutionItemsContainsReadme) + { + solutionFolders.Count().Should().Be(1); + var solutionItems = solutionFolders.Single().Sections.GetSection("SolutionItems"); + solutionItems.Should().NotBeNull(); + solutionItems.Properties.Count().Should().Be(1); + solutionItems.Properties["readme.txt"].Should().Be("readme.txt"); + } + else + { + solutionFolders.Count().Should().Be(0); + } + } + private void MigrateAndBuild(string groupName, string projectName, [CallerMemberName] string callingMethod = "", string identifier = "") { var projectDirectory = TestAssets