diff --git a/src/ConsoleApp/App.config b/src/ConsoleApp/App.config new file mode 100644 index 0000000..8324aa6 --- /dev/null +++ b/src/ConsoleApp/App.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/ConsoleApp/AssemblyReferenceRegularExpressions.cs b/src/ConsoleApp/AssemblyReferenceRegularExpressions.cs new file mode 100644 index 0000000..b0d699c --- /dev/null +++ b/src/ConsoleApp/AssemblyReferenceRegularExpressions.cs @@ -0,0 +1,13 @@ +using NuGet.Packaging.Core; +using System.Text.RegularExpressions; + +namespace ConsoleApp +{ + internal class AssemblyReferenceRegularExpressions : RegularExpressionsForPackagesBase + { + protected override Regex GetRegularExpression(PackageIdentity packageIdentity) + { + return new Regex($@".*{Regex.Escape(packageIdentity.Id)}\.{Regex.Escape(packageIdentity.Version.ToString())}\\.+", RegexOptions.IgnoreCase); + } + } +} \ No newline at end of file diff --git a/src/ConsoleApp/ConsoleApp.csproj b/src/ConsoleApp/ConsoleApp.csproj new file mode 100644 index 0000000..2cb0be6 --- /dev/null +++ b/src/ConsoleApp/ConsoleApp.csproj @@ -0,0 +1,68 @@ + + + + + Debug + AnyCPU + {1FE13D69-D7BE-44D9-96ED-190E7D379704} + Exe + ConsoleApp + PackagesConfigProjectConverter + v4.6 + 512 + true + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + + + + + + + 15.3.409 + + + 1.1.0 + + + 4.4.0 + + + + \ No newline at end of file diff --git a/src/ConsoleApp/ExtensionsMethods.cs b/src/ConsoleApp/ExtensionsMethods.cs new file mode 100644 index 0000000..adc7d69 --- /dev/null +++ b/src/ConsoleApp/ExtensionsMethods.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace ConsoleApp +{ + internal static class ExtensionsMethods + { + private static readonly char[] ArgumentValueSeparators = { ';', ',' }; + private static readonly char[] ArgumentNameSeparators = { ':' }; + + public static IEnumerable GetCommandLineArgumentValues(this string[] args, params string[] argumentNames) + { + foreach (string arg in args.Where(i => argumentNames.Any(x => i.StartsWith(x, StringComparison.OrdinalIgnoreCase))).Select(i => i.Split(ArgumentNameSeparators, 2).LastOrDefault()).Where(y => !String.IsNullOrWhiteSpace(y))) + { + foreach (string item in arg.Split(ArgumentValueSeparators, StringSplitOptions.RemoveEmptyEntries).Select(i => i.Trim()).Where(i => !String.IsNullOrWhiteSpace(i))) + { + yield return item; + } + } + } + } +} \ No newline at end of file diff --git a/src/ConsoleApp/ImportRegularExpressions.cs b/src/ConsoleApp/ImportRegularExpressions.cs new file mode 100644 index 0000000..1702775 --- /dev/null +++ b/src/ConsoleApp/ImportRegularExpressions.cs @@ -0,0 +1,13 @@ +using System.Text.RegularExpressions; +using NuGet.Packaging.Core; + +namespace ConsoleApp +{ + internal class ImportRegularExpressions : RegularExpressionsForPackagesBase + { + protected override Regex GetRegularExpression(PackageIdentity packageIdentity) + { + return new Regex($@".*{Regex.Escape(packageIdentity.Id)}\.{Regex.Escape(packageIdentity.Version.ToString())}.+{Regex.Escape(packageIdentity.Id)}\.(props|targets)", RegexOptions.IgnoreCase); + } + } +} \ No newline at end of file diff --git a/src/ConsoleApp/Program.cs b/src/ConsoleApp/Program.cs new file mode 100644 index 0000000..8a4c79f --- /dev/null +++ b/src/ConsoleApp/Program.cs @@ -0,0 +1,97 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Reflection; + +namespace ConsoleApp +{ + internal class Program + { + // @"D:\Truman2\private\Libraries\ServiceClient\public\sample" + private static int Main(string[] args) + { + try + { + if (args.Any(i => i.Equals("/debug", StringComparison.OrdinalIgnoreCase))) + { + Debugger.Break(); + } + + if (args.Any(i => i.Equals("/?", StringComparison.OrdinalIgnoreCase))) + { + return PrintUsage(); + } + + bool quiet = args.Any(i => i.StartsWith("/q", StringComparison.OrdinalIgnoreCase)); + + string repositoryPath = args.SingleOrDefault(i => i[0] != '/'); + + if (String.IsNullOrWhiteSpace(repositoryPath)) + { + return PrintUsage("You must specify a repository path"); + } + + List exclusions = args.GetCommandLineArgumentValues("/e", "exclude").ToList(); + + Console.WriteLine($" EnlistmentRoot: '{repositoryPath}'"); + Console.WriteLine($" Exclusion: '{String.Join($"{Environment.NewLine} Exclusion: '", exclusions)}'"); + Console.WriteLine(); + + if (!quiet) + { + Console.Write("Ensure there are no files checked out in git before continuing! Continue? (Y/N) "); + if (!Console.ReadLine().StartsWith("Y", StringComparison.OrdinalIgnoreCase)) + { + return 0; + } + } + + using (ProjectConverter projectConverter = new ProjectConverter()) + { + Console.WriteLine("Converting..."); + + projectConverter.ConvertRepository(repositoryPath, exclusions); + + Console.WriteLine("Success!"); + } + } + catch (Exception e) + { + Console.Error.WriteLine(e.ToString()); + + return 1; + } + + return 0; + } + + private static int PrintUsage(string errorMessage = null) + { + if (!String.IsNullOrWhiteSpace(errorMessage)) + { + Console.Error.WriteLine(errorMessage); + } + + Console.WriteLine("Converts a repository from packages.config to PackageReference."); + Console.WriteLine(); + + Console.WriteLine($"{Assembly.GetExecutingAssembly().GetName().Name}.exe repositoryPath [/quiet] [/exclude:path1;path2]"); + Console.WriteLine(); + Console.WriteLine(" Repository Full path to the repository root to convert"); + Console.WriteLine(); + Console.WriteLine(" /quiet Do not prompt before converting the tree"); + Console.WriteLine(); + Console.WriteLine(" /exclude One or more full paths to any directories to exclude"); + Console.WriteLine(); + Console.WriteLine(" Example:"); + Console.WriteLine(); + Console.WriteLine(" /exclude:\"D:\\RepoA\\src\\ProjectX\""); + + Console.WriteLine(); + Console.WriteLine(" /debug Launch the debugger before executing"); + + return String.IsNullOrWhiteSpace(errorMessage) ? 0 : 1; + } + } +} \ No newline at end of file diff --git a/src/ConsoleApp/ProjectConverter.cs b/src/ConsoleApp/ProjectConverter.cs new file mode 100644 index 0000000..77d6160 --- /dev/null +++ b/src/ConsoleApp/ProjectConverter.cs @@ -0,0 +1,230 @@ +using Microsoft.Build.Construction; +using Microsoft.Build.Evaluation; +using NuGet.Packaging; +using NuGet.Packaging.Core; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using System.Xml.Linq; + +namespace ConsoleApp +{ + internal sealed class ProjectConverter : IDisposable + { + // ReSharper disable once CollectionNeverUpdated.Local + private static readonly AssemblyReferenceRegularExpressions AssemblyReferenceRegularExpressions = new AssemblyReferenceRegularExpressions(); + + // ReSharper disable once CollectionNeverUpdated.Local + private static readonly ImportRegularExpressions ImportRegularExpressions = new ImportRegularExpressions(); + + private static readonly string[] ItemsToRemove = {"packages.config"}; + private static readonly string[] ItemTypesToRemove = {"Analyzer"}; + private static readonly string[] PropertiesToRemove = {"NuGetPackageImportStamp"}; + private readonly ProjectCollection _projectCollection; + + public ProjectConverter() + : this(new ProjectCollection()) + { + } + + public ProjectConverter(ProjectCollection projectCollection) + { + _projectCollection = projectCollection ?? throw new ArgumentNullException(nameof(projectCollection)); + } + + public void ConvertProject(string projectPath) + { + string packagesConfigPath = Path.Combine(Path.GetDirectoryName(projectPath), "packages.config"); + + if (!File.Exists(packagesConfigPath)) + { + return; + } + + PackagesConfigReader packagesConfigReader = new PackagesConfigReader(XDocument.Load(packagesConfigPath)); + + List packages = packagesConfigReader.GetPackages(allowDuplicatePackageIds: true).Select(i => i.PackageIdentity).ToList(); + + ProjectRootElement project = ProjectRootElement.Open(projectPath, _projectCollection, preserveFormatting: true); + + try + { + RemoveImports(project, packages); + + RemoveTargets(project); + + RemoveProperties(project); + + RemoveItems(project); + + ReplaceReferences(project, packages); + + project.Save(); + + File.Delete(packagesConfigPath); + } + catch (Exception) + { + Console.WriteLine($"Failed to convert '{projectPath}'"); + } + } + + public void ConvertRepository(string repositoryPath, IEnumerable exclusions = null) + { + HashSet exlustionsHashSet = new HashSet(exclusions ?? Enumerable.Empty()); + + foreach (string file in Directory.EnumerateFiles(repositoryPath, "*.csproj", SearchOption.AllDirectories).Where(i => !exlustionsHashSet.Any(e => i.StartsWith(e, StringComparison.OrdinalIgnoreCase)))) + { + ConvertProject(file); + } + } + + public void Dispose() + { + _projectCollection?.Dispose(); + } + + private void RemoveImports(ProjectRootElement project, List packages) + { + var importsToRemove = new List(); + + foreach (ProjectImportElement importElement in project.Imports) + { + foreach (PackageIdentity package in packages) + { + Regex regex = ImportRegularExpressions[package]; + + if (regex.IsMatch(importElement.Project)) + { + importsToRemove.Add(importElement); + } + } + } + + foreach (ProjectImportElement projectImportElement in importsToRemove) + { + projectImportElement.Parent.RemoveChild(projectImportElement); + } + } + + private void RemoveItems(ProjectRootElement project) + { + foreach (string itemSpec in ItemsToRemove) + { + foreach (ProjectItemElement itemElement in project.Items.Where(i => i.Include.Equals(itemSpec, StringComparison.OrdinalIgnoreCase)).ToList()) + { + if (itemElement.Parent.Count == 1) + { + itemElement.Parent.Parent.RemoveChild(itemElement.Parent); + } + else + { + itemElement.Parent.RemoveChild(itemElement); + } + } + } + + foreach (string itemType in ItemTypesToRemove) + { + foreach (ProjectItemElement itemElement in project.Items.Where(i => i.ItemType.Equals(itemType, StringComparison.OrdinalIgnoreCase)).ToList()) + { + if (itemElement.Parent.Count == 1) + { + itemElement.Parent.Parent.RemoveChild(itemElement.Parent); + } + else + { + itemElement.Parent.RemoveChild(itemElement); + } + } + } + } + + private void RemoveProperties(ProjectRootElement project) + { + foreach (string propertyName in PropertiesToRemove) + { + foreach (ProjectPropertyElement propertyElement in project.Properties.Where(i => i.Name.Equals(propertyName, StringComparison.OrdinalIgnoreCase)).ToList()) + { + propertyElement.Parent.RemoveChild(propertyElement); + } + } + } + + private void RemoveTargets(ProjectRootElement project) + { + foreach (ProjectTargetElement targetElement in project.Targets.Where(i => i.Name.Equals("EnsureNuGetPackageBuildImports", StringComparison.OrdinalIgnoreCase)).ToList()) + { + targetElement.Parent.RemoveChild(targetElement); + } + } + + private void ReplaceReferences(ProjectRootElement project, List packages) + { + HashSet allPackages = new HashSet(packages); + + Dictionary itemsToReplace = new Dictionary(); + + foreach (ProjectItemElement itemElement in project.Items.Where(i => i.ItemType.Equals("Reference"))) + { + foreach (PackageIdentity packageIdentity in packages) + { + Regex regex = AssemblyReferenceRegularExpressions[packageIdentity]; + + ProjectMetadataElement metadatum = itemElement.Metadata.FirstOrDefault(i => i.Name.Equals("HintPath")); + + if (metadatum != null && regex.IsMatch(metadatum.Value)) + { + itemsToReplace.Add(itemElement, packageIdentity); + + allPackages.Remove(packageIdentity); + } + } + } + + List packagesAdded = new List(); + + ProjectItemElement lastItem = project.Items.First(i => i.ItemType.Equals("Reference")) ?? project.ItemGroups.First().Items.First(); + + foreach (KeyValuePair pair in itemsToReplace) + { + if (!packagesAdded.Contains(pair.Value)) + { + ProjectItemElement item = project.CreateItemElement("PackageReference", pair.Value.Id); + + pair.Key.Parent.InsertAfterChild(item, pair.Key); + + item.AddMetadata("Version", pair.Value.Version.ToString()); + + packagesAdded.Add(pair.Value); + + lastItem = item; + } + + pair.Key.Parent.RemoveChild(pair.Key); + } + + foreach (PackageIdentity package in allPackages) + { + if (lastItem == null) + { + var itemGroup = project.AddItemGroup(); + + lastItem = itemGroup.AddItem("PackageReference", package.Id, new List> {new KeyValuePair("Version", package.Version.ToString())}); + } + else + { + ProjectItemElement item = project.CreateItemElement("PackageReference", package.Id); + + lastItem.Parent.InsertAfterChild(item, lastItem); + + item.AddMetadata("Version", package.Version.ToString()); + + lastItem = item; + } + } + } + } +} \ No newline at end of file diff --git a/src/ConsoleApp/Properties/AssemblyInfo.cs b/src/ConsoleApp/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..2fb0dab --- /dev/null +++ b/src/ConsoleApp/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("ConsoleApp")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("ConsoleApp")] +[assembly: AssemblyCopyright("Copyright © 2017")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("1fe13d69-d7be-44d9-96ed-190e7d379704")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/src/ConsoleApp/RegularExpressionsForPackagesBase.cs b/src/ConsoleApp/RegularExpressionsForPackagesBase.cs new file mode 100644 index 0000000..50fd80e --- /dev/null +++ b/src/ConsoleApp/RegularExpressionsForPackagesBase.cs @@ -0,0 +1,92 @@ +using NuGet.Packaging.Core; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Text.RegularExpressions; + +namespace ConsoleApp +{ + internal abstract class RegularExpressionsForPackagesBase : IDictionary + { + private readonly Dictionary _regexes = new Dictionary(); + + public int Count => _regexes.Count; + + public bool IsReadOnly => false; + + public ICollection Keys => _regexes.Keys; + + public ICollection Values => _regexes.Values; + + public Regex this[PackageIdentity key] + { + get + { + if (!_regexes.ContainsKey(key)) + { + _regexes.Add(key, GetRegularExpression(key)); + } + + return _regexes[key]; + } + set => _regexes[key] = value; + } + + public void Add(KeyValuePair item) + { + _regexes.Add(item.Key, item.Value); + } + + public void Add(PackageIdentity key, Regex value) + { + _regexes.Add(key, value); + } + + public void Clear() + { + _regexes.Clear(); + } + + public bool Contains(KeyValuePair item) + { + return _regexes.ContainsKey(item.Key); + } + + public bool ContainsKey(PackageIdentity key) + { + return _regexes.ContainsKey(key); + } + + public void CopyTo(KeyValuePair[] array, int arrayIndex) + { + throw new NotImplementedException(); + } + + public IEnumerator> GetEnumerator() + { + return _regexes.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public bool Remove(KeyValuePair item) + { + throw new NotImplementedException(); + } + + public bool Remove(PackageIdentity key) + { + throw new NotImplementedException(); + } + + public bool TryGetValue(PackageIdentity key, out Regex value) + { + return _regexes.TryGetValue(key, out value); + } + + protected abstract Regex GetRegularExpression(PackageIdentity packageIdentity); + } +} \ No newline at end of file diff --git a/src/PackagesConfigConverter.sln b/src/PackagesConfigConverter.sln new file mode 100644 index 0000000..61779dd --- /dev/null +++ b/src/PackagesConfigConverter.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.27013.2 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleApp", "ConsoleApp\ConsoleApp.csproj", "{1FE13D69-D7BE-44D9-96ED-190E7D379704}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {1FE13D69-D7BE-44D9-96ED-190E7D379704}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1FE13D69-D7BE-44D9-96ED-190E7D379704}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1FE13D69-D7BE-44D9-96ED-190E7D379704}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1FE13D69-D7BE-44D9-96ED-190E7D379704}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {AD6227D3-772B-4734-82C6-ADA5639226B3} + EndGlobalSection +EndGlobal