Skip to content

Commit 90570e7

Browse files
committed
Added support for converting projects within sln files on the command line
1 parent c939764 commit 90570e7

9 files changed

Lines changed: 307 additions & 93 deletions

File tree

.github/workflows/build.yml

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,12 @@ on:
1313
description: 'Publish extension to the Visual Studio Marketplace?'
1414
required: true
1515
default: 'false'
16-
publishNupkg:
17-
description: 'Publish NuGet package to nuget.org?'
16+
skipPublishNupkg:
17+
description: 'Skip publish NuGet package?'
18+
required: true
19+
default: 'false'
20+
stableRelease:
21+
description: 'Is NuGet package a stable release?'
1822
required: true
1923
default: 'false'
2024

@@ -54,7 +58,7 @@ jobs:
5458
echo "SEM_VERSION=$majorVersion.$minorVersion.$buildVersion" >> $env:GITHUB_ENV
5559
5660
- name: Set version number for pre-release
57-
if: ${{ github.event.inputs.publishNupkg == '' || github.event.inputs.publishNupkg == 'false' }}
61+
if: ${{ github.event.inputs.stableRelease == '' || github.event.inputs.stableRelease == 'false' }}
5862
run: |
5963
echo "SEM_VERSION=${{ env.SEM_VERSION }}-build-${{ github.RUN_NUMBER }}" >> $env:GITHUB_ENV
6064
@@ -153,7 +157,7 @@ jobs:
153157
if-no-files-found: error
154158

155159
- name: Publish GitHub Release
156-
if: ${{ matrix.Configuration == 'Release' && (github.event.inputs.publishNupkg == 'true' || github.event.inputs.publishVsix == 'true') }}
160+
if: ${{ matrix.Configuration == 'Release' && github.event.inputs.publishVsix == 'true' }}
157161
uses: softprops/action-gh-release@v2.0.8
158162
with:
159163
name: v${{ env.VERSION }}
@@ -164,14 +168,13 @@ jobs:
164168
fail_on_unmatched_files: true
165169
files: |
166170
./src/PackageReferenceVersionToAttributeExtension/bin/${{matrix.Configuration}}/PackageReferenceVersionToAttributeExtension.vsix
167-
./src/PackageReferenceVersionToAttributeTool/bin/${{matrix.Configuration}}/PackageReferenceVersionToAttribute.Tool.${{ env.SEM_VERSION }}.nupkg
168171
169172
- name: Publish NuGet Package
170-
if: ${{ matrix.Configuration == 'Release' && github.event_name != 'pull_request' && github.event.inputs.publishNupkg == 'true' }}
173+
if: ${{ matrix.Configuration == 'Release' && github.event_name != 'pull_request' && (github.event.inputs.skipPublishNupkg == '' || github.event.inputs.skipPublishNupkg == 'false') }}
171174
run: dotnet nuget push .\src\PackageReferenceVersionToAttributeTool\bin\${{matrix.Configuration}}\PackageReferenceVersionToAttribute.Tool.${{ env.SEM_VERSION }}.nupkg --api-key ${{ secrets.NUGET_KEY }} --source https://api.nuget.org/v3/index.json
172175

173176
- name: Publish to Open VSIX
174-
if: ${{ matrix.Configuration == 'Release' && github.event_name != 'pull_request' && github.event.inputs.publishNupkg == 'true' }}
177+
if: ${{ matrix.Configuration == 'Release' && github.event_name != 'pull_request' && (github.event.inputs.skipPublishNupkg == '' || github.event.inputs.skipPublishNupkg == 'false') }}
175178
run: |
176179
[Reflection.Assembly]::LoadWithPartialName("System.Web") | Out-Null
177180
$vsixFile = ".\src\PackageReferenceVersionToAttributeExtension\bin\${{matrix.Configuration}}\PackageReferenceVersionToAttributeExtension.vsix"

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Change Log
22

3+
## TBD
4+
5+
- Added support for converting projects within sln files on the command line
6+
37
## v1.0.1104.33 (November 4<sup>th</sup>, 2024)
48

59
- Added input validation for the mutually exclusive --backup and --dry-run command line options

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
# PackageReference Version to Attribute
22

3-
This Visual Studio extension and dotnet tool converts PackageReference Version child elements to attributes across your projects.
3+
This Visual Studio extension and dotnet tool converts `PackageReference` `Version` child elements to attributes in C# project files (`csproj`).
44

5-
It works with C# csproj Visual Studio project files.
5+
It can also convert all projects in a Visual Studio solution file (`sln`).
66

77
## Getting started with the Visual Studio Extension
88

99
1. [Download and install the extension from the Visual Studio Marketplace](https://marketplace.visualstudio.com/items?itemName=RamiAbughazaleh.PackageReferenceVersionToAttributeExtension).
10-
2. Right-click on a project and select `Convert PackageReference Version elements to attributes...`.
10+
2. Right-click on the root `Solution` node, or one or more projects, and select `Convert PackageReference Version elements to attributes...`.
1111

1212
![Preview](Preview.png)
1313

@@ -31,7 +31,7 @@ It works with C# csproj Visual Studio project files.
3131

3232
## Technical details
3333

34-
The extension will first create a backup of the project file.
34+
The extension will create a backup of each project file.
3535
For example, `MyProject.csproj` will be copied to `MyProject.csproj.bak`.
3636

3737
If the project file is source controlled, it will be checked out for modification.

src/PackageReferenceVersionToAttribute/ProjectConverter.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ namespace PackageReferenceVersionToAttribute
1111
using System.Threading.Tasks;
1212
using System.Xml;
1313
using System.Xml.Linq;
14-
using System.Xml.XPath;
1514
using Microsoft.Extensions.Logging;
1615
using Microsoft.Extensions.Options;
1716

src/PackageReferenceVersionToAttributeTool/PackageReferenceVersionToAttributeTool.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
<PackageReference Include="Microsoft.Extensions.FileSystemGlobbing" Version="8.0.0" />
3535
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
3636
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="8.0.0" />
37+
<PackageReference Include="SlnParser" Version="4.0.0" />
3738
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556">
3839
<PrivateAssets>all</PrivateAssets>
3940
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

src/PackageReferenceVersionToAttributeTool/ProgramCommand.cs

Lines changed: 5 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,8 @@
44

55
namespace PackageReferenceVersionToAttributeTool
66
{
7-
using System;
8-
using System.Collections.Generic;
97
using System.CommandLine;
108
using System.CommandLine.NamingConventionBinder;
11-
using System.Reflection;
12-
using System.Threading.Tasks;
13-
using Microsoft.Extensions.DependencyInjection;
14-
using Microsoft.Extensions.Logging;
15-
using PackageReferenceVersionToAttribute;
169

1710
/// <summary>
1811
/// Program command.
@@ -51,82 +44,13 @@ public ProgramCommand()
5144
this.Add(forceOption);
5245
this.Add(dryRunOption);
5346

54-
// Add validation
55-
this.AddValidator(result =>
56-
{
57-
bool backup = result.GetValueForOption(backupOption);
58-
bool force = result.GetValueForOption(forceOption);
59-
bool dryRun = result.GetValueForOption(dryRunOption);
60-
61-
var options = new ProjectConverterOptions
62-
{
63-
Backup = backup,
64-
Force = force,
65-
DryRun = dryRun,
66-
};
67-
68-
var validator = new ProjectConverterOptionsValidator();
69-
var validationResult = validator.Validate(nameof(ProjectConverterOptions), options);
70-
if (validationResult.Failed)
71-
{
72-
result.ErrorMessage = validationResult.FailureMessage;
73-
}
74-
});
47+
// validate
48+
var commandValidator = new ProgramCommandLineOptionsValidator(backupOption, forceOption, dryRunOption);
49+
this.AddValidator(commandValidator.Validate);
7550

7651
// Set the handler for the command
77-
this.Handler = CommandHandler.Create<ProgramCommandLineOptions>(async (options) =>
78-
{
79-
if (options.Version)
80-
{
81-
var version = Assembly.GetExecutingAssembly().GetName().Version;
82-
Console.WriteLine($"Version: {version}");
83-
return;
84-
}
85-
86-
foreach (var input in options.Inputs)
87-
{
88-
await ConvertPackageReferencesAsync(input, options);
89-
}
90-
});
91-
}
92-
93-
private static async Task ConvertPackageReferencesAsync(
94-
string input, ProjectConverterOptions options)
95-
{
96-
FilePatternMatcher filePatternMatcher = new();
97-
List<string> matchingFiles = filePatternMatcher.GetMatchingFiles(input);
98-
if (matchingFiles.Count == 0)
99-
{
100-
Console.WriteLine($"No matching files found for pattern: {input}");
101-
return;
102-
}
103-
104-
if (options.Backup)
105-
{
106-
Console.WriteLine("Backup option is enabled.");
107-
}
108-
109-
if (options.Force)
110-
{
111-
Console.WriteLine("Force option is enabled.");
112-
}
113-
114-
if (options.DryRun)
115-
{
116-
Console.WriteLine("Dry run mode is enabled. No changes will be made.");
117-
}
118-
119-
using var serviceProvider = new ServiceCollection()
120-
.AddSingleton(Microsoft.Extensions.Options.Options.Create(options))
121-
.AddLogging(configure => configure.AddConsole())
122-
.AddSingleton<ProjectConverter>()
123-
.AddSingleton<IFileService, FileService>()
124-
.AddSingleton<ISourceControlService, NullSourceControlService>()
125-
.BuildServiceProvider();
126-
127-
var projectConverter = serviceProvider.GetRequiredService<ProjectConverter>();
128-
129-
await projectConverter.ConvertAsync(matchingFiles);
52+
this.Handler = CommandHandler.Create<ProgramCommandLineOptions>(
53+
ProgramCommandHandler.HandleAsync);
13054
}
13155
}
13256
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
// <copyright file="ProgramCommandHandler.cs" company="Rami Abughazaleh">
2+
// Copyright (c) Rami Abughazaleh. All rights reserved.
3+
// </copyright>
4+
5+
namespace PackageReferenceVersionToAttributeTool
6+
{
7+
using System;
8+
using System.Collections.Generic;
9+
using System.Linq;
10+
using System.Reflection;
11+
using System.Threading.Tasks;
12+
using Microsoft.Extensions.DependencyInjection;
13+
using Microsoft.Extensions.Logging;
14+
using PackageReferenceVersionToAttribute;
15+
using SlnParser;
16+
using SlnParser.Contracts;
17+
18+
/// <summary>
19+
/// Program command handler.
20+
/// </summary>
21+
internal class ProgramCommandHandler
22+
{
23+
/// <summary>
24+
/// Executes the command logic asynchronously based on the provided command line options.
25+
/// </summary>
26+
/// <param name="options">An instance of <see cref="ProgramCommandLineOptions"/> containing the parsed options from the command line.</param>
27+
/// <returns>A task representing the asynchronous operation.</returns>
28+
public static async Task HandleAsync(ProgramCommandLineOptions options)
29+
{
30+
if (options.Version)
31+
{
32+
var version = Assembly.GetExecutingAssembly().GetName().Version;
33+
Console.WriteLine($"Version: {version}");
34+
return;
35+
}
36+
37+
foreach (var input in options.Inputs)
38+
{
39+
await ConvertPackageReferencesAsync(input, options);
40+
}
41+
}
42+
43+
private static async Task ConvertPackageReferencesAsync(
44+
string input, ProjectConverterOptions options)
45+
{
46+
FilePatternMatcher filePatternMatcher = new();
47+
48+
// get matching csproj and sln files
49+
List<string> matchingFiles = filePatternMatcher.GetMatchingFiles(input)
50+
.Where(x => x.EndsWith(".csproj", StringComparison.OrdinalIgnoreCase)
51+
|| x.EndsWith(".sln", StringComparison.OrdinalIgnoreCase))
52+
.ToList();
53+
54+
// parse sln files to get only csproj files
55+
List<string> projectFiles = GetCsprojFiles(matchingFiles);
56+
if (projectFiles.Count == 0)
57+
{
58+
Console.WriteLine($"No matching project files found for pattern: {input}");
59+
return;
60+
}
61+
62+
if (options.Backup)
63+
{
64+
Console.WriteLine("Backup option is enabled.");
65+
}
66+
67+
if (options.Force)
68+
{
69+
Console.WriteLine("Force option is enabled.");
70+
}
71+
72+
if (options.DryRun)
73+
{
74+
Console.WriteLine("Dry run mode is enabled. No changes will be made.");
75+
}
76+
77+
using var serviceProvider = new ServiceCollection()
78+
.AddSingleton(Microsoft.Extensions.Options.Options.Create(options))
79+
.AddLogging(configure => configure.AddConsole())
80+
.AddSingleton<ProjectConverter>()
81+
.AddSingleton<IFileService, FileService>()
82+
.AddSingleton<ISourceControlService, NullSourceControlService>()
83+
.BuildServiceProvider();
84+
85+
var projectConverter = serviceProvider.GetRequiredService<ProjectConverter>();
86+
87+
await projectConverter.ConvertAsync(projectFiles);
88+
}
89+
90+
private static List<string> GetCsprojFiles(List<string> files)
91+
{
92+
var csprojFiles = new List<string>();
93+
94+
foreach (var file in files)
95+
{
96+
if (file.EndsWith(".csproj", StringComparison.OrdinalIgnoreCase))
97+
{
98+
csprojFiles.Add(file);
99+
}
100+
else if (file.EndsWith(".sln", StringComparison.OrdinalIgnoreCase))
101+
{
102+
csprojFiles.AddRange(GetCsprojFiles(file));
103+
}
104+
}
105+
106+
return csprojFiles;
107+
}
108+
109+
private static IEnumerable<string> GetCsprojFiles(string solutionFilePath)
110+
{
111+
SolutionParser solutionParser = new SolutionParser();
112+
ISolution solution = solutionParser.Parse(solutionFilePath);
113+
114+
return solution.AllProjects
115+
.OfType<SolutionProject>()
116+
.Where(x => x.File.FullName.EndsWith(".csproj"))
117+
.Select(x => x.File.FullName);
118+
}
119+
}
120+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// <copyright file="ProgramCommandLineOptionsValidator.cs" company="Rami Abughazaleh">
2+
// Copyright (c) Rami Abughazaleh. All rights reserved.
3+
// </copyright>
4+
5+
namespace PackageReferenceVersionToAttributeTool
6+
{
7+
using System.CommandLine;
8+
using System.CommandLine.Parsing;
9+
using PackageReferenceVersionToAttribute;
10+
11+
/// <summary>
12+
/// Validates command-line options for the program.
13+
/// </summary>
14+
/// <remarks>
15+
/// Initializes a new instance of the <see cref="ProgramCommandLineOptionsValidator"/> class.
16+
/// </remarks>
17+
/// <param name="backupOption">The backup option.</param>
18+
/// <param name="forceOption">The force option.</param>
19+
/// <param name="dryRunOption">The dry run option.</param>
20+
internal class ProgramCommandLineOptionsValidator(
21+
Option<bool> backupOption,
22+
Option<bool> forceOption,
23+
Option<bool> dryRunOption)
24+
{
25+
private readonly Option<bool> backupOption = backupOption;
26+
private readonly Option<bool> forceOption = forceOption;
27+
private readonly Option<bool> dryRunOption = dryRunOption;
28+
29+
/// <summary>
30+
/// Validates the specified <see cref="CommandResult"/>.
31+
/// </summary>
32+
/// <param name="result">The <see cref="CommandResult"/> containing the parsed command-line options.</param>
33+
public void Validate(CommandResult result)
34+
{
35+
// Extract option values
36+
bool backup = result.GetValueForOption(this.backupOption);
37+
bool force = result.GetValueForOption(this.forceOption);
38+
bool dryRun = result.GetValueForOption(this.dryRunOption);
39+
40+
var options = new ProjectConverterOptions
41+
{
42+
Backup = backup,
43+
Force = force,
44+
DryRun = dryRun,
45+
};
46+
47+
var validator = new ProjectConverterOptionsValidator();
48+
var validationResult = validator.Validate(nameof(ProjectConverterOptions), options);
49+
if (validationResult.Failed)
50+
{
51+
result.ErrorMessage = validationResult.FailureMessage;
52+
}
53+
}
54+
}
55+
}

0 commit comments

Comments
 (0)