diff --git a/.editorconfig b/.editorconfig index 4073a7db08..f199d80f9e 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,7 +1,7 @@ +root = true # http://editorconfig.org # top-most EditorConfig file -root = true [*] indent_style = space @@ -10,6 +10,12 @@ end_of_line = lf trim_trailing_whitespace = true insert_final_newline = true +# Microsoft .NET properties +dotnet_style_qualification_for_event = false:none +dotnet_style_qualification_for_field = false:none +dotnet_style_qualification_for_method = false:none +dotnet_style_qualification_for_property = false:none + [*.yml] indent_size = 2 @@ -156,13 +162,13 @@ resharper_csharp_align_multiline_parameter = true resharper_csharp_instance_members_qualify_members = field # IDE0005: Using directive is unnecessary. -dotnet_diagnostic.IDE0005.severity = warning +dotnet_diagnostic.ide0005.severity = warning # RCS1037: Remove trailing white-space. -dotnet_diagnostic.RCS1037.severity = error +dotnet_diagnostic.rcs1037.severity = error # RCS1036: Remove redundant empty line. -dotnet_diagnostic.RCS1036.severity = error +dotnet_diagnostic.rcs1036.severity = error xml_space_before_self_closing = true @@ -170,4 +176,4 @@ resharper_arrange_object_creation_when_type_not_evident_highlighting = none resharper_unused_auto_property_accessor_global_highlighting = none -resharper_unused_method_return_value_global_highlighting = none \ No newline at end of file +resharper_unused_method_return_value_global_highlighting = none diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index f3cc9ffc04..f05e80535c 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -48,12 +48,6 @@ jobs: - name: Run Format 'ci' solution run: dotnet format ./build/ --verify-no-changes - - - name: Build 'new-cli' solution - run: dotnet build ./new-cli - - - name: Run Format 'new-cli' solution - run: dotnet format ./new-cli/ --exclude ~/.nuget/packages --verify-no-changes - name: Run Format 'GitVersion' solution run: dotnet format ./src/ --exclude **/AddFormats/ --verify-no-changes diff --git a/.github/workflows/new-cli.yml b/.github/workflows/new-cli.yml new file mode 100644 index 0000000000..aa466cc455 --- /dev/null +++ b/.github/workflows/new-cli.yml @@ -0,0 +1,56 @@ +name: Build (new-cli) +on: + push: + branches: + - main + - 'fix/*' + - 'feature/*' + - 'poc/*' + - 'support/*' + paths: + - '**' + - '!docs/**' + - '!.github/**' + - .github/workflows/new-cli.yml + + pull_request: + branches: + - main + - 'support/*' + paths: + - '**' + - '!docs/**' + - '!.github/**' + - .github/workflows/new-cli.yml + +permissions: + contents: read + +env: + DOTNET_ROLL_FORWARD: "Major" + DOTNET_CLI_TELEMETRY_OPTOUT: 1 + DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1 + DOTNET_NOLOGO: 1 + +jobs: + format: + runs-on: ubuntu-24.04 + name: Build & Test (new-cli) + steps: + - + name: Checkout + uses: actions/checkout@v4 + - + name: Setup .NET SDK + uses: actions/setup-dotnet@v4 + with: + global-json-file: global.json + - + name: Build 'new-cli' solution + run: dotnet build ./new-cli + - + name: Run Format 'new-cli' solution + run: dotnet format ./new-cli --exclude ~/.nuget/packages --verify-no-changes + - + name: Test 'new-cli' solution + run: dotnet test ./new-cli --no-build --verbosity normal diff --git a/build/CI.slnx b/build/CI.slnx index 4e7ad23da5..1d84f0ec53 100644 --- a/build/CI.slnx +++ b/build/CI.slnx @@ -42,6 +42,7 @@ + diff --git a/new-cli/.run/TestCommand.run.xml b/new-cli/.run/TestCommand.run.xml new file mode 100644 index 0000000000..6d8c05c0fb --- /dev/null +++ b/new-cli/.run/TestCommand.run.xml @@ -0,0 +1,25 @@ + + + + \ No newline at end of file diff --git a/new-cli/Directory.Packages.props b/new-cli/Directory.Packages.props index ff081d944f..2b256c612a 100644 --- a/new-cli/Directory.Packages.props +++ b/new-cli/Directory.Packages.props @@ -7,11 +7,21 @@ + + - + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + @@ -24,4 +34,4 @@ - + \ No newline at end of file diff --git a/new-cli/GitVersion.Calculation/CalculateCommand.cs b/new-cli/GitVersion.Calculation/CalculateCommand.cs index 56dae32c90..6b3dbb798a 100644 --- a/new-cli/GitVersion.Calculation/CalculateCommand.cs +++ b/new-cli/GitVersion.Calculation/CalculateCommand.cs @@ -1,28 +1,28 @@ using GitVersion.Extensions; using GitVersion.Git; -using GitVersion.Infrastructure; +using Microsoft.Extensions.Logging; namespace GitVersion.Commands; public record CalculateSettings : GitVersionSettings; [Command("calculate", "Calculates the version object from the git history.")] -public class CalculateCommand(ILogger logger, IService service, IGitRepository repository) : ICommand +public class CalculateCommand(ILogger logger, IService service, IGitRepository repository) : ICommand { - private readonly ILogger logger = logger.NotNull(); - private readonly IService service = service.NotNull(); - private readonly IGitRepository repository = repository.NotNull(); + private readonly ILogger _logger = logger.NotNull(); + private readonly IService _service = service.NotNull(); + private readonly IGitRepository _repository = repository.NotNull(); public Task InvokeAsync(CalculateSettings settings, CancellationToken cancellationToken = default) { - var value = service.Call(); + var value = _service.Call(); if (settings.WorkDir != null) { - this.repository.DiscoverRepository(settings.WorkDir.FullName); - var branches = this.repository.Branches.ToList(); - this.logger.LogInformation("Command : 'calculate', LogFile : '{logFile}', WorkDir : '{workDir}' ", + _repository.DiscoverRepository(settings.WorkDir.FullName); + var branches = _repository.Branches.ToList(); + _logger.LogInformation("Command : 'calculate', LogFile : '{logFile}', WorkDir : '{workDir}' ", settings.LogFile, settings.WorkDir); - this.logger.LogInformation("Found {count} branches", branches.Count); + _logger.LogInformation("Found {count} branches", branches.Count); } return Task.FromResult(value); diff --git a/new-cli/GitVersion.Cli.Generator.Tests/GitVersion.Cli.Generator.Tests.csproj b/new-cli/GitVersion.Cli.Generator.Tests/GitVersion.Cli.Generator.Tests.csproj new file mode 100644 index 0000000000..13dd134bbf --- /dev/null +++ b/new-cli/GitVersion.Cli.Generator.Tests/GitVersion.Cli.Generator.Tests.csproj @@ -0,0 +1,36 @@ + + + + GitVersion.Cli.Generator.Tests + false + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + diff --git a/new-cli/GitVersion.Cli.Generator.Tests/SystemCommandlineGeneratorTests.cs b/new-cli/GitVersion.Cli.Generator.Tests/SystemCommandlineGeneratorTests.cs new file mode 100644 index 0000000000..6e3775e48d --- /dev/null +++ b/new-cli/GitVersion.Cli.Generator.Tests/SystemCommandlineGeneratorTests.cs @@ -0,0 +1,288 @@ +using System.CommandLine; +using GitVersion.Infrastructure; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Testing; +using Microsoft.CodeAnalysis.Testing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace GitVersion.Cli.Generator.Tests; + +public class SystemCommandlineGeneratorTests +{ + /*language=cs*/ + private const string GlobalUsingsCode = +""" +// +global using global::System; +global using global::System.Collections.Generic; +global using global::System.IO; +global using global::System.Linq; +global using global::System.Net.Http; +global using global::System.Threading; +global using global::System.Threading.Tasks; +"""; + + /*language=cs*/ + private const string ExpectedCommandImplText = +$$""" +{{Constants.GeneratedHeader}} +using System.CommandLine; +using System.CommandLine.Binding; + +using {{Constants.CommandNamespaceName}}; + +namespace {{Constants.GeneratedNamespaceName}}; + +public class TestCommandImpl : Command, ICommandImpl +{ + public string CommandImplName => nameof(TestCommandImpl); + public string ParentCommandImplName => string.Empty; + // Options list + protected readonly Option LogFileOption; + protected readonly Option OutputFileOption; + protected readonly Option VerbosityOption; + protected readonly Option WorkDirOption; + + public TestCommandImpl(TestCommand command) + : base("test", "Test description.") + { + LogFileOption = new Option("--log-file", "-l") + { + Required = false, + Description = "The log file", + }; + OutputFileOption = new Option("--output-file") + { + Required = true, + Description = "The output file", + }; + VerbosityOption = new Option("--verbosity") + { + Required = false, + Description = "The verbosity of the logging information", + }; + WorkDirOption = new Option("--work-dir") + { + Required = false, + Description = "The working directory with the git repository", + }; + Add(LogFileOption); + Add(OutputFileOption); + Add(VerbosityOption); + Add(WorkDirOption); + + this.SetAction(Run); + return; + + Task Run(ParseResult parseResult, CancellationToken cancellationToken) + { + var settings = new TestCommandSettings + { + LogFile = parseResult.GetValue(LogFileOption), + OutputFile = parseResult.GetValue(OutputFileOption)!, + Verbosity = parseResult.GetValue(VerbosityOption), + WorkDir = parseResult.GetValue(WorkDirOption), + }; + return command.InvokeAsync(settings, cancellationToken); + } + } +} +"""; + + /*language=cs*/ + private const string ExpectedCommandsModuleText = +$$""" +{{Constants.GeneratedHeader}} +using System.CommandLine; +using {{Constants.InfrastructureNamespaceName}}; +using {{Constants.CommandNamespaceName}}; +using {{Constants.CommonNamespaceName}}; +using Microsoft.Extensions.DependencyInjection; + +namespace {{Constants.GeneratedNamespaceName}}; + +public class CommandsModule : IGitVersionModule +{ + public IServiceCollection RegisterTypes(IServiceCollection services) + { + services.AddSingleton(); + services.AddSingleton(); + + services.AddSingleton(); + services.AddSingleton(); + return services; + } +} +"""; + + /*language=cs*/ + private const string ExpectedRootCommandImplText = +$$""" +{{Constants.GeneratedHeader}} +using System.CommandLine; + +using {{Constants.CommonNamespaceName}}; +using {{Constants.ExtensionsNamespaceName}}; + +namespace {{Constants.GeneratedNamespaceName}}; + +public class RootCommandImpl(IEnumerable commands) : RootCommand +{ + private readonly IEnumerable _commands = commands.NotNull(); + + public void Configure() + { + var map = _commands.ToDictionary(c => c.CommandImplName); + foreach (var command in map.Values) + { + AddCommand(command, map); + } + } + + private void AddCommand(ICommandImpl command, Dictionary map) + { + if (!string.IsNullOrWhiteSpace(command.ParentCommandImplName)) + { + var parent = map[command.ParentCommandImplName] as Command; + parent?.Add((Command)command); + } + else + { + Add((Command)command); + } + } +} +"""; + + /*language=cs*/ + private const string ExpectedCliAppImplText = +$$""" +{{Constants.GeneratedHeader}} +using System.CommandLine; +using {{Constants.ExtensionsNamespaceName}}; +using {{Constants.InfrastructureNamespaceName}}; + +namespace {{Constants.GeneratedNamespaceName}}; + +internal class CliAppImpl : ICliApp +{ + private readonly RootCommandImpl _rootCommand; + + public CliAppImpl(RootCommandImpl rootCommand) + { + _rootCommand = rootCommand.NotNull(); + _rootCommand.Configure(); + } + + public Task RunAsync(string[] args, CancellationToken cancellationToken) + { + // Note: there are 2 locations to watch for the dotnet-suggest tool + // - sentinel file: + // $env:TEMP\system-commandline-sentinel-files\ and + // - registration file: + // $env:LOCALAPPDATA\.dotnet-suggest-registration.txt or $HOME/.dotnet-suggest-registration.txt + + var parseResult = _rootCommand.Parse(args); + + var logFile = parseResult.GetValue(GitVersionSettings.LogFileOption); + var verbosity = parseResult.GetValue(GitVersionSettings.VerbosityOption) ?? Verbosity.Normal; + + LoggingEnricher.Configure(logFile?.FullName, verbosity); + + return parseResult.InvokeAsync(cancellationToken: cancellationToken); + } +} +"""; + + /*language=cs*/ + private const string TestCommandSourceCode = +$$""" +using {{Constants.InfrastructureNamespaceName}}; +using Microsoft.Extensions.Logging; + +namespace {{Constants.CommandNamespaceName}}; + +[CommandAttribute("test", "Test description.")] +public class TestCommand(ILogger logger): ICommand +{ + public Task InvokeAsync(TestCommandSettings settings, CancellationToken cancellationToken = default) + { + return Task.FromResult(0); + } +} + +"""; + + /*language=cs*/ + private const string TestCommandSettingsSourceCode = +$$""" +using {{Constants.InfrastructureNamespaceName}}; +using Microsoft.Extensions.Logging; + +namespace {{Constants.CommandNamespaceName}}; + +public record TestCommandSettings : GitVersionSettings +{ + [Option("--output-file", "The output file")] + public required string OutputFile { get; init; } +} + +"""; + + [Test] + public async Task ValidateGeneratedCommandImplementation() + { + var generatorType = typeof(SystemCommandlineGenerator); + var sourceGeneratorTest = new CSharpSourceGeneratorTest + { + TestState = + { + Sources = + { + (generatorType, "GlobalUsings.cs", GlobalUsingsCode), + (generatorType, "TestCommand.cs", TestCommandSourceCode), + (generatorType, "TestCommandSettings.cs", TestCommandSettingsSourceCode) + }, + GeneratedSources = + { + (generatorType,"TestCommandImpl.g.cs", ExpectedCommandImplText), + (generatorType,"CommandsModule.g.cs", ExpectedCommandsModuleText), + (generatorType,"RootCommandImpl.g.cs", ExpectedRootCommandImplText), + (generatorType,"CliAppImpl.g.cs", ExpectedCliAppImplText), + }, + ReferenceAssemblies = ReferenceAssemblies.Net.Net90, + AdditionalReferences = + { + MetadataReference.CreateFromFile(typeof(ILogger).Assembly.Location), + MetadataReference.CreateFromFile(typeof(IServiceCollection).Assembly.Location), + MetadataReference.CreateFromFile(typeof(RootCommand).Assembly.Location), + MetadataReference.CreateFromFile(typeof(CommandAttribute).Assembly.Location), + MetadataReference.CreateFromFile(typeof(IGitVersionModule).Assembly.Location), + MetadataReference.CreateFromFile(typeof(LoggingEnricher).Assembly.Location), + } + } + }; + + sourceGeneratorTest.SolutionTransforms.Add( + // make sure the ImplicitUsage is enabled + (solution, projectId) => + { + var project = solution.GetProject(projectId)!; + + // Enable ImplicitUsings + var parseOptions = (CSharpParseOptions)project.ParseOptions!; + var compilationOptions = (CSharpCompilationOptions)project.CompilationOptions!; + + // Enable implicit usings (same as `enable` in .csproj) + compilationOptions = compilationOptions.WithNullableContextOptions(NullableContextOptions.Enable); + + return project + .WithParseOptions(parseOptions.WithLanguageVersion(LanguageVersion.Latest)) + .WithCompilationOptions(compilationOptions) + .Solution; + }); + await sourceGeneratorTest.RunAsync(); + } +} diff --git a/new-cli/GitVersion.Cli.Generator/CommandImplGenerator.cs b/new-cli/GitVersion.Cli.Generator/CommandBaseGenerator.cs similarity index 57% rename from new-cli/GitVersion.Cli.Generator/CommandImplGenerator.cs rename to new-cli/GitVersion.Cli.Generator/CommandBaseGenerator.cs index 7e2001a0dd..1afd040bd9 100644 --- a/new-cli/GitVersion.Cli.Generator/CommandImplGenerator.cs +++ b/new-cli/GitVersion.Cli.Generator/CommandBaseGenerator.cs @@ -1,19 +1,13 @@ using GitVersion.Polyfill; -// ReSharper disable InconsistentNaming namespace GitVersion; -[Generator(LanguageNames.CSharp)] -public class CommandImplGenerator : IIncrementalGenerator +public abstract class CommandBaseGenerator : IIncrementalGenerator { - private const string GeneratedNamespaceName = "GitVersion.Generated"; - private const string InfraNamespaceName = "GitVersion"; - private const string DependencyInjectionNamespaceName = "GitVersion.Infrastructure"; - private const string CommandNamespaceName = "GitVersion.Commands"; - private const string CommandInterfaceFullName = $"{InfraNamespaceName}.ICommand"; - private const string CommandAttributeFullName = $"{InfraNamespaceName}.CommandAttribute"; - private const string CommandAttributeGenericFullName = $"{InfraNamespaceName}.CommandAttribute"; - private const string OptionAttributeFullName = $"{InfraNamespaceName}.OptionAttribute"; + private const string CommandInterfaceFullName = $"{Constants.CommonNamespaceName}.ICommand"; + private const string CommandAttributeFullName = $"{Constants.CommonNamespaceName}.CommandAttribute"; + private const string CommandAttributeGenericFullName = $"{Constants.CommonNamespaceName}.CommandAttribute"; + private const string OptionAttributeFullName = $"{Constants.CommonNamespaceName}.OptionAttribute"; public void Initialize(IncrementalGeneratorInitializationContext context) { @@ -22,6 +16,8 @@ public void Initialize(IncrementalGeneratorInitializationContext context) context.RegisterImplementationSourceOutput(commandTypes, GenerateSourceCode); } + internal abstract void GenerateSourceCode(SourceProductionContext context, ImmutableArray commandInfos); + private static ImmutableArray SelectCommandTypes(Compilation compilation, CancellationToken ct) { ct.ThrowIfCancellationRequested(); @@ -38,43 +34,7 @@ static bool SearchQuery(INamedTypeSymbol typeSymbol) return attributeData is not null; } } - private static void GenerateSourceCode(SourceProductionContext context, ImmutableArray commandInfos) - { - foreach (var commandInfo in commandInfos) - { - if (commandInfo == null) - continue; - - var commandHandlerTemplate = Template.Parse(Content.CommandImplContent); - - var commandHandlerSource = commandHandlerTemplate.Render(new - { - Model = commandInfo, - Namespace = GeneratedNamespaceName - }, member => member.Name); - context.AddSource($"{commandInfo.CommandTypeName}Impl.g.cs", string.Join("\n", commandHandlerSource)); - } - - var commandHandlersModuleTemplate = Template.Parse(Content.CommandsModuleContent); - var commandHandlersModuleSource = commandHandlersModuleTemplate.Render(new - { - Model = commandInfos, - Namespace = GeneratedNamespaceName, - InfraNamespaceName, - DependencyInjectionNamespaceName, - CommandNamespaceName - }, member => member.Name); - context.AddSource("CommandsModule.g.cs", string.Join("\n", commandHandlersModuleSource)); - - var rootCommandHandlerTemplate = Template.Parse(Content.RootCommandImplContent); - var rootCommandHandlerSource = rootCommandHandlerTemplate.Render(new - { - Namespace = GeneratedNamespaceName, - InfraNamespaceName - }, member => member.Name); - context.AddSource("RootCommandImpl.g.cs", string.Join("\n", rootCommandHandlerSource)); - } private static CommandInfo? MapToCommandInfo(ITypeSymbol classSymbol, CancellationToken ct) { ct.ThrowIfCancellationRequested(); @@ -118,7 +78,7 @@ where optionAttribute is not null CommandDescription = description, SettingsTypeName = settingsType.Name, SettingsTypeNamespace = settingsType.ContainingNamespace.ToDisplayString(), - SettingsProperties = settingsPropertyInfos.ToArray() + SettingsProperties = [.. settingsPropertyInfos] }; return commandInfo; } @@ -132,23 +92,22 @@ private static SettingsPropertyInfo MapToPropertyInfo(IPropertySymbol propertySy name.NotNull(); description.NotNull(); - string alias = string.Empty; + string[] aliases = []; if (ctorArguments.Length == 3) { var aliasesArgs = ctorArguments[2]; - var aliases = (aliasesArgs.Kind == TypedConstantKind.Array + aliases = (aliasesArgs.Kind == TypedConstantKind.Array ? aliasesArgs.Values.Select(x => Convert.ToString(x.Value)).ToArray() - : [Convert.ToString(aliasesArgs.Value)]).Select(x => $@"""{x?.Trim()}"""); - alias = string.Join(", ", aliases); + : [Convert.ToString(aliasesArgs.Value)]); } - var isRequired = propertySymbol.Type.NullableAnnotation == NullableAnnotation.NotAnnotated; + var isRequired = propertySymbol.IsRequired; return new() { Name = propertySymbol.Name, TypeName = propertySymbol.Type.ToDisplayString(), OptionName = name, - Aliases = alias, + Aliases = aliases, Description = description, Required = isRequired }; diff --git a/new-cli/GitVersion.Cli.Generator/Constants.cs b/new-cli/GitVersion.Cli.Generator/Constants.cs new file mode 100644 index 0000000000..0732f0ee48 --- /dev/null +++ b/new-cli/GitVersion.Cli.Generator/Constants.cs @@ -0,0 +1,24 @@ +namespace GitVersion; + +public static class Constants +{ + internal const string GeneratedNamespaceName = "GitVersion.Generated"; + internal const string CommonNamespaceName = "GitVersion"; + internal const string InfrastructureNamespaceName = "GitVersion.Infrastructure"; + internal const string CommandNamespaceName = "GitVersion.Commands"; + internal const string ExtensionsNamespaceName = "GitVersion.Extensions"; + + /*language=cs*/ + internal const string GeneratedHeader = +""" +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ +#nullable enable +"""; +} diff --git a/new-cli/GitVersion.Cli.Generator/Content.cs b/new-cli/GitVersion.Cli.Generator/Content.cs deleted file mode 100644 index edde296b9f..0000000000 --- a/new-cli/GitVersion.Cli.Generator/Content.cs +++ /dev/null @@ -1,132 +0,0 @@ -namespace GitVersion; - -public static class Content -{ - /*language=cs*/ - private const string GeneratedHeader = """ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ -#nullable enable -"""; - - /*language=cs*/ - public const string CommandImplContent = $$$""" -{{{GeneratedHeader}}} -using System.CommandLine; -using System.CommandLine.Binding; - -using {{Model.CommandTypeNamespace}}; -{{- if Model.SettingsTypeNamespace != Model.CommandTypeNamespace }} -using {{Model.SettingsTypeNamespace}};{{ end }} - -namespace {{Namespace}}; - -public class {{Model.CommandTypeName}}Impl : Command, ICommandImpl -{ - public string CommandName => nameof({{Model.CommandTypeName}}Impl); - {{- if (Model.ParentCommand | string.empty) }} - public string ParentCommandName => string.Empty; - {{- else }} - public string ParentCommandName => nameof({{Model.ParentCommand}}Impl); - {{ end }} - {{- $settingsProperties = Model.SettingsProperties | array.sort "Name" }} - // Options list - {{~ for $prop in $settingsProperties ~}} - protected readonly Option<{{$prop.TypeName}}> {{$prop.Name}}Option; - {{~ end ~}} - - public {{Model.CommandTypeName}}Impl({{Model.CommandTypeName}} command) - : base("{{Model.CommandName}}", "{{Model.CommandDescription}}") - { - {{~ for $prop in $settingsProperties ~}} - {{$prop.Name}}Option = new Option<{{$prop.TypeName}}>("{{$prop.OptionName}}", [{{$prop.Aliases}}]) - { - Required = {{$prop.Required}}, - Description = "{{$prop.Description}}", - }; - {{~ end ~}} - - {{- for $prop in $settingsProperties ~}} - Add({{$prop.Name}}Option); - {{~ end ~}} - - this.SetAction(Run); - return; - - Task Run(ParseResult parseResult, CancellationToken cancellationToken) - { - var settings = new {{Model.SettingsTypeName}} - { - {{~ for $prop in $settingsProperties ~}} - {{$prop.Name}} = parseResult.GetValue({{$prop.Name}}Option){{ if $prop.Required }}!{{ end}}, - {{~ end ~}} - }; - return command.InvokeAsync(settings, cancellationToken); - } - } -} -"""; - - /*language=cs*/ - public const string RootCommandImplContent = $$$""" -{{{GeneratedHeader}}} -using System.CommandLine; -using {{InfraNamespaceName}}; -namespace {{Namespace}}; - -public class RootCommandImpl : RootCommand -{ - public RootCommandImpl(IEnumerable commands) - { - var map = commands.ToDictionary(c => c.CommandName); - foreach (var command in map.Values) - { - AddCommand(command, map); - } - } - private void AddCommand(ICommandImpl command, IDictionary map) - { - if (!string.IsNullOrWhiteSpace(command.ParentCommandName)) - { - var parent = map[command.ParentCommandName] as Command; - parent?.Add((Command)command); - } - else - { - Add((Command)command); - } - } -} -"""; - - /*language=cs*/ - public const string CommandsModuleContent = $$$""" -{{{GeneratedHeader}}} -using System.CommandLine; -using {{DependencyInjectionNamespaceName}}; -using {{CommandNamespaceName}}; -using {{InfraNamespaceName}}; - -namespace {{Namespace}}; - -public class CommandsImplModule : IGitVersionModule -{ - public void RegisterTypes(IContainerRegistrar services) - { - {{- $commands = Model | array.sort "CommandTypeName" }} - services.AddSingleton(); - {{~ for $command in $commands ~}} - services.AddSingleton<{{$command.CommandTypeName}}>(); - services.AddSingleton(); - - {{~ end ~}} - } -} -"""; -} diff --git a/new-cli/GitVersion.Cli.Generator/GitVersion.Cli.Generator.csproj b/new-cli/GitVersion.Cli.Generator/GitVersion.Cli.Generator.csproj index 41689234e5..d93d7009bb 100644 --- a/new-cli/GitVersion.Cli.Generator/GitVersion.Cli.Generator.csproj +++ b/new-cli/GitVersion.Cli.Generator/GitVersion.Cli.Generator.csproj @@ -15,4 +15,8 @@ + + + + diff --git a/new-cli/GitVersion.Cli.Generator/Models.cs b/new-cli/GitVersion.Cli.Generator/Models.cs index 6b91007d9b..8e75c59fb9 100644 --- a/new-cli/GitVersion.Cli.Generator/Models.cs +++ b/new-cli/GitVersion.Cli.Generator/Models.cs @@ -17,7 +17,7 @@ internal record SettingsPropertyInfo public required string Name { get; init; } public required string TypeName { get; init; } public required string OptionName { get; init; } - public required string Aliases { get; init; } + public required string[] Aliases { get; init; } public required string Description { get; init; } public required bool Required { get; init; } } diff --git a/new-cli/GitVersion.Cli.Generator/Properties/launchSettings.json b/new-cli/GitVersion.Cli.Generator/Properties/launchSettings.json index c184e457e2..5470135d61 100644 --- a/new-cli/GitVersion.Cli.Generator/Properties/launchSettings.json +++ b/new-cli/GitVersion.Cli.Generator/Properties/launchSettings.json @@ -2,7 +2,7 @@ "profiles": { "SourceGenerator": { "commandName": "DebugRoslynComponent", - "targetProject": "..\\GitVersion.Cli\\GitVersion.Cli.csproj" + "targetProject": "../GitVersion.Cli/GitVersion.Cli.csproj" } } -} \ No newline at end of file +} diff --git a/new-cli/GitVersion.Cli.Generator/SystemCommandlineContent.cs b/new-cli/GitVersion.Cli.Generator/SystemCommandlineContent.cs new file mode 100644 index 0000000000..ce2fa4fd75 --- /dev/null +++ b/new-cli/GitVersion.Cli.Generator/SystemCommandlineContent.cs @@ -0,0 +1,166 @@ +namespace GitVersion; + +public static class SystemCommandlineContent +{ + /*language=cs*/ + public const string CommandImplContent = $$$""" +{{{Constants.GeneratedHeader}}} +using System.CommandLine; +using System.CommandLine.Binding; + +using {{Model.CommandTypeNamespace}}; + +namespace {{GeneratedNamespaceName}}; + +public class {{Model.CommandTypeName}}Impl : Command, ICommandImpl +{ + public string CommandImplName => nameof({{Model.CommandTypeName}}Impl); + {{- if (Model.ParentCommand | string.empty) }} + public string ParentCommandImplName => string.Empty; + {{- else }} + public string ParentCommandImplName => nameof({{Model.ParentCommand}}Impl); + {{ end }} + {{- $settingsProperties = Model.SettingsProperties | array.sort "Name" }} + // Options list + {{~ for $prop in $settingsProperties ~}} + protected readonly Option<{{$prop.TypeName}}> {{$prop.Name}}Option; + {{~ end ~}} + + public {{Model.CommandTypeName}}Impl({{Model.CommandTypeName}} command) + : base("{{Model.CommandName}}", "{{Model.CommandDescription}}") + { + {{~ for $prop in $settingsProperties ~}} + {{$prop.Name}}Option = new Option<{{$prop.TypeName}}>("{{$prop.OptionName}}"{{if $prop.Aliases.size == 0}}{{else}}, {{for $alias in $prop.Aliases}}"{{$alias}}"{{if !for.last}}, {{end}}{{end}}{{end}}) + { + Required = {{$prop.Required}}, + Description = "{{$prop.Description}}", + }; + {{~ end ~}} + + {{- for $prop in $settingsProperties ~}} + Add({{$prop.Name}}Option); + {{~ end ~}} + + this.SetAction(Run); + return; + + Task Run(ParseResult parseResult, CancellationToken cancellationToken) + { + var settings = new {{ if Model.SettingsTypeNamespace != Model.CommandTypeNamespace }}{{Model.SettingsTypeNamespace}}.{{ end }}{{Model.SettingsTypeName}} + { + {{~ for $prop in $settingsProperties ~}} + {{$prop.Name}} = parseResult.GetValue({{$prop.Name}}Option){{ if $prop.Required }}!{{ end}}, + {{~ end ~}} + }; + return command.InvokeAsync(settings, cancellationToken); + } + } +} +"""; + + /*language=cs*/ + public const string RootCommandImplContent = $$$""" +{{{Constants.GeneratedHeader}}} +using System.CommandLine; + +using {{CommonNamespaceName}}; +using {{ExtensionsNamespaceName}}; + +namespace {{GeneratedNamespaceName}}; + +public class RootCommandImpl(IEnumerable commands) : RootCommand +{ + private readonly IEnumerable _commands = commands.NotNull(); + + public void Configure() + { + var map = _commands.ToDictionary(c => c.CommandImplName); + foreach (var command in map.Values) + { + AddCommand(command, map); + } + } + + private void AddCommand(ICommandImpl command, Dictionary map) + { + if (!string.IsNullOrWhiteSpace(command.ParentCommandImplName)) + { + var parent = map[command.ParentCommandImplName] as Command; + parent?.Add((Command)command); + } + else + { + Add((Command)command); + } + } +} +"""; + + /*language=cs*/ + public const string CommandsModuleContent = $$$""" +{{{Constants.GeneratedHeader}}} +using System.CommandLine; +using {{InfrastructureNamespaceName}}; +using {{CommandNamespaceName}}; +using {{CommonNamespaceName}}; +using Microsoft.Extensions.DependencyInjection; + +namespace {{GeneratedNamespaceName}}; + +public class CommandsModule : IGitVersionModule +{ + public IServiceCollection RegisterTypes(IServiceCollection services) + { + services.AddSingleton(); + services.AddSingleton(); + + {{- $commands = Model | array.sort "CommandTypeName" }} + + {{~ for $command in $commands ~}} + services.AddSingleton<{{ if $command.CommandTypeNamespace != CommandNamespaceName }}{{$command.CommandTypeNamespace}}.{{ end }}{{$command.CommandTypeName}}>(); + services.AddSingleton(); + {{~ end ~}} + return services; + } +} +"""; + + /*language=cs*/ + public const string CliAppContent = $$$""" +{{{Constants.GeneratedHeader}}} +using System.CommandLine; +using {{ExtensionsNamespaceName}}; +using {{InfrastructureNamespaceName}}; + +namespace {{GeneratedNamespaceName}}; + +internal class CliAppImpl : ICliApp +{ + private readonly RootCommandImpl _rootCommand; + + public CliAppImpl(RootCommandImpl rootCommand) + { + _rootCommand = rootCommand.NotNull(); + _rootCommand.Configure(); + } + + public Task RunAsync(string[] args, CancellationToken cancellationToken) + { + // Note: there are 2 locations to watch for the dotnet-suggest tool + // - sentinel file: + // $env:TEMP\system-commandline-sentinel-files\ and + // - registration file: + // $env:LOCALAPPDATA\.dotnet-suggest-registration.txt or $HOME/.dotnet-suggest-registration.txt + + var parseResult = _rootCommand.Parse(args); + + var logFile = parseResult.GetValue(GitVersionSettings.LogFileOption); + var verbosity = parseResult.GetValue(GitVersionSettings.VerbosityOption) ?? Verbosity.Normal; + + LoggingEnricher.Configure(logFile?.FullName, verbosity); + + return parseResult.InvokeAsync(cancellationToken: cancellationToken); + } +} +"""; +} diff --git a/new-cli/GitVersion.Cli.Generator/SystemCommandlineGenerator.cs b/new-cli/GitVersion.Cli.Generator/SystemCommandlineGenerator.cs new file mode 100644 index 0000000000..6902afaf44 --- /dev/null +++ b/new-cli/GitVersion.Cli.Generator/SystemCommandlineGenerator.cs @@ -0,0 +1,54 @@ +namespace GitVersion; + +[Generator(LanguageNames.CSharp)] +public class SystemCommandlineGenerator : CommandBaseGenerator +{ + internal override void GenerateSourceCode(SourceProductionContext context, ImmutableArray commandInfos) + { + foreach (var commandInfo in commandInfos) + { + if (commandInfo == null) + continue; + + var commandHandlerTemplate = Template.Parse(SystemCommandlineContent.CommandImplContent); + + var commandHandlerSource = commandHandlerTemplate.Render(new + { + Model = commandInfo, + Constants.GeneratedNamespaceName + }, member => member.Name); + + context.AddSource($"{commandInfo.CommandTypeName}Impl.g.cs", string.Join("\n", commandHandlerSource)); + } + + var commandHandlersModuleTemplate = Template.Parse(SystemCommandlineContent.CommandsModuleContent); + var commandHandlersModuleSource = commandHandlersModuleTemplate.Render(new + { + Model = commandInfos, + Constants.GeneratedNamespaceName, + Constants.CommonNamespaceName, + Constants.InfrastructureNamespaceName, + Constants.CommandNamespaceName + }, member => member.Name); + context.AddSource("CommandsModule.g.cs", string.Join("\n", commandHandlersModuleSource)); + + var rootCommandHandlerTemplate = Template.Parse(SystemCommandlineContent.RootCommandImplContent); + var rootCommandHandlerSource = rootCommandHandlerTemplate.Render(new + { + Constants.GeneratedNamespaceName, + Constants.CommonNamespaceName, + Constants.ExtensionsNamespaceName + }, member => member.Name); + context.AddSource("RootCommandImpl.g.cs", string.Join("\n", rootCommandHandlerSource)); + + var cliAppTemplate = Template.Parse(SystemCommandlineContent.CliAppContent); + var cliAppSource = cliAppTemplate.Render(new + { + Constants.GeneratedNamespaceName, + Constants.InfrastructureNamespaceName, + Constants.CommonNamespaceName, + Constants.ExtensionsNamespaceName + }, member => member.Name); + context.AddSource("CliAppImpl.g.cs", string.Join("\n", cliAppSource)); + } +} diff --git a/new-cli/GitVersion.Cli.Generator/TypeVisitor.cs b/new-cli/GitVersion.Cli.Generator/TypeVisitor.cs index fbd42a3b0c..6507afdd7d 100644 --- a/new-cli/GitVersion.Cli.Generator/TypeVisitor.cs +++ b/new-cli/GitVersion.Cli.Generator/TypeVisitor.cs @@ -5,7 +5,7 @@ internal class TypeVisitor(Func searchQuery, Cancellatio { private readonly HashSet _exportedTypes = new(SymbolEqualityComparer.Default); - public ImmutableArray GetResults() => [.. this._exportedTypes]; + public ImmutableArray GetResults() => [.. _exportedTypes]; public override void VisitAssembly(IAssemblySymbol symbol) { @@ -28,7 +28,7 @@ public override void VisitNamedType(INamedTypeSymbol type) if (searchQuery(type)) { - this._exportedTypes.Add(type); + _exportedTypes.Add(type); } } } diff --git a/new-cli/GitVersion.Cli/GitVersionApp.cs b/new-cli/GitVersion.Cli/GitVersionApp.cs deleted file mode 100644 index 218bd44c79..0000000000 --- a/new-cli/GitVersion.Cli/GitVersionApp.cs +++ /dev/null @@ -1,40 +0,0 @@ -using GitVersion.Extensions; -using GitVersion.Generated; -using GitVersion.Infrastructure; -using Serilog.Events; - -namespace GitVersion; - -// ReSharper disable once ClassNeverInstantiated.Global -internal class GitVersionApp(RootCommandImpl rootCommand) -{ - private readonly RootCommandImpl _rootCommand = rootCommand.NotNull(); - - public Task RunAsync(string[] args, CancellationToken cancellationToken) - { - var parseResult = this._rootCommand.Parse(args); - - var logFile = parseResult.GetValue(GitVersionSettings.LogFileOption); - var verbosity = parseResult.GetValue(GitVersionSettings.VerbosityOption) ?? Verbosity.Normal; - - if (logFile?.FullName != null) LoggingEnricher.Path = logFile.FullName; - LoggingEnricher.LogLevel.MinimumLevel = GetLevelForVerbosity(verbosity); - - return parseResult.InvokeAsync(cancellationToken: cancellationToken); - } - - // Note: there are 2 locations to watch for dotnet-suggest - // - sentinel file: $env:TEMP\system-commandline-sentinel-files\ and - // - registration file: $env:LOCALAPPDATA\.dotnet-suggest-registration.txt or $HOME/.dotnet-suggest-registration.txt - - private static LogEventLevel GetLevelForVerbosity(Verbosity verbosity) => VerbosityMaps[verbosity]; - - private static readonly Dictionary VerbosityMaps = new() - { - { Verbosity.Verbose, LogEventLevel.Verbose }, - { Verbosity.Diagnostic, LogEventLevel.Debug }, - { Verbosity.Normal, LogEventLevel.Information }, - { Verbosity.Minimal, LogEventLevel.Warning }, - { Verbosity.Quiet, LogEventLevel.Error }, - }; -} diff --git a/new-cli/GitVersion.Cli/Program.cs b/new-cli/GitVersion.Cli/Program.cs index 46cbdc1ad8..fa31299e2d 100644 --- a/new-cli/GitVersion.Cli/Program.cs +++ b/new-cli/GitVersion.Cli/Program.cs @@ -1,34 +1,33 @@ using GitVersion; using GitVersion.Extensions; -using GitVersion.Generated; using GitVersion.Git; using GitVersion.Infrastructure; +using Microsoft.Extensions.DependencyInjection; var modules = new IGitVersionModule[] { new CoreModule(), new LibGit2SharpCoreModule(), - new CommandsImplModule() + new GitVersion.Generated.CommandsModule() }; var cts = new CancellationTokenSource(); Console.CancelKeyPress += (_, _) => cts.Cancel(); -using var serviceProvider = RegisterModules(modules); -var app = serviceProvider.GetRequiredService(); -var result = await app.RunAsync(args, cts.Token).ConfigureAwait(false); +await using var serviceProvider = RegisterModules(modules); +var app = serviceProvider.GetRequiredService(); +var result = await app.RunAsync(args, cts.Token).ConfigureAwait(false); if (!Console.IsInputRedirected) Console.ReadKey(); return result; -static IContainer RegisterModules(IEnumerable gitVersionModules) +static ServiceProvider RegisterModules(IEnumerable gitVersionModules) { - var serviceProvider = new ContainerRegistrar() + var serviceProvider = new ServiceCollection() .RegisterModules(gitVersionModules) - .AddSingleton() - .AddLogging() - .Build(); + .RegisterLogging() + .BuildServiceProvider(); return serviceProvider; } diff --git a/new-cli/GitVersion.Cli/TestCommand.cs b/new-cli/GitVersion.Cli/TestCommand.cs new file mode 100644 index 0000000000..15c8452124 --- /dev/null +++ b/new-cli/GitVersion.Cli/TestCommand.cs @@ -0,0 +1,13 @@ +using GitVersion.Commands.Test.Settings; + +namespace GitVersion.Commands.Test; + +[Command("test", "Test command.")] +public class TestCommand : ICommand +{ + public Task InvokeAsync(TestCommandSettings settings, CancellationToken cancellationToken = default) + { + Console.WriteLine("Input file: {0}", settings.InputFile); + return Task.FromResult(0); + } +} diff --git a/new-cli/GitVersion.Cli/TestCommandSettings.cs b/new-cli/GitVersion.Cli/TestCommandSettings.cs new file mode 100644 index 0000000000..cd6166e187 --- /dev/null +++ b/new-cli/GitVersion.Cli/TestCommandSettings.cs @@ -0,0 +1,7 @@ +namespace GitVersion.Commands.Test.Settings; + +public record TestCommandSettings : GitVersionSettings +{ + [Option("--input-file", description: "The input version file", aliases: ["-i"])] + public required string InputFile { get; init; } +} diff --git a/new-cli/GitVersion.Common.Command/ICommand.cs b/new-cli/GitVersion.Common.Command/ICommand.cs index 6f2723e5cf..d8bcb1eebf 100644 --- a/new-cli/GitVersion.Common.Command/ICommand.cs +++ b/new-cli/GitVersion.Common.Command/ICommand.cs @@ -7,6 +7,6 @@ public interface ICommand public interface ICommandImpl { - string CommandName { get; } - string ParentCommandName { get; } + string CommandImplName { get; } + string ParentCommandImplName { get; } } diff --git a/new-cli/GitVersion.Common/GitVersion.Common.csproj b/new-cli/GitVersion.Common/GitVersion.Common.csproj index 91e48ec0d7..61737d8507 100644 --- a/new-cli/GitVersion.Common/GitVersion.Common.csproj +++ b/new-cli/GitVersion.Common/GitVersion.Common.csproj @@ -1,20 +1,22 @@ - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + diff --git a/new-cli/GitVersion.Common/Infrastructure/ICliApp.cs b/new-cli/GitVersion.Common/Infrastructure/ICliApp.cs new file mode 100644 index 0000000000..fc2a63aafe --- /dev/null +++ b/new-cli/GitVersion.Common/Infrastructure/ICliApp.cs @@ -0,0 +1,6 @@ +namespace GitVersion.Infrastructure; + +public interface ICliApp +{ + Task RunAsync(string[] args, CancellationToken cancellationToken); +} diff --git a/new-cli/GitVersion.Common/Infrastructure/IContainer.cs b/new-cli/GitVersion.Common/Infrastructure/IContainer.cs deleted file mode 100644 index 3e26810630..0000000000 --- a/new-cli/GitVersion.Common/Infrastructure/IContainer.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace GitVersion.Infrastructure; - -public interface IContainer : IDisposable -{ - T? GetService(); - T GetRequiredService() where T : notnull; - object? GetService(Type type); - object GetRequiredService(Type type); -} diff --git a/new-cli/GitVersion.Common/Infrastructure/IContainerRegistrar.cs b/new-cli/GitVersion.Common/Infrastructure/IContainerRegistrar.cs deleted file mode 100644 index cf11b727fe..0000000000 --- a/new-cli/GitVersion.Common/Infrastructure/IContainerRegistrar.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace GitVersion.Infrastructure; - -public interface IContainerRegistrar -{ - IContainerRegistrar AddSingleton() where TService : class; - - IContainerRegistrar AddSingleton() - where TService : class where TImplementation : class, TService; - - IContainerRegistrar AddTransient() where TService : class; - - IContainerRegistrar AddTransient() - where TService : class where TImplementation : class, TService; - - IContainerRegistrar AddLogging(); - - IContainer Build(); -} diff --git a/new-cli/GitVersion.Common/Infrastructure/IGitVersionModule.cs b/new-cli/GitVersion.Common/Infrastructure/IGitVersionModule.cs index 7f8aa0addb..bbac2d24a2 100644 --- a/new-cli/GitVersion.Common/Infrastructure/IGitVersionModule.cs +++ b/new-cli/GitVersion.Common/Infrastructure/IGitVersionModule.cs @@ -1,6 +1,8 @@ +using Microsoft.Extensions.DependencyInjection; + namespace GitVersion.Infrastructure; public interface IGitVersionModule { - void RegisterTypes(IContainerRegistrar services); + IServiceCollection RegisterTypes(IServiceCollection services); } diff --git a/new-cli/GitVersion.Common/Infrastructure/ILogger.cs b/new-cli/GitVersion.Common/Infrastructure/ILogger.cs deleted file mode 100644 index 57df75da40..0000000000 --- a/new-cli/GitVersion.Common/Infrastructure/ILogger.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace GitVersion.Infrastructure; - -public interface ILogger -{ - void LogTrace(string? message, params object?[] args); - void LogDebug(string? message, params object?[] args); - void LogInformation(string? message, params object?[] args); - void LogWarning(string? message, params object?[] args); - void LogError(string? message, params object?[] args); - void LogCritical(string? message, params object?[] args); -} diff --git a/new-cli/GitVersion.Configuration/ConfigCommand.cs b/new-cli/GitVersion.Configuration/ConfigCommand.cs index 7f3112f214..7fde5361e2 100644 --- a/new-cli/GitVersion.Configuration/ConfigCommand.cs +++ b/new-cli/GitVersion.Configuration/ConfigCommand.cs @@ -1,20 +1,20 @@ using GitVersion.Extensions; -using GitVersion.Infrastructure; +using Microsoft.Extensions.Logging; namespace GitVersion.Commands; public record ConfigSettings : GitVersionSettings; [Command("config", "Manages the GitVersion configuration file.")] -public class ConfigCommand(ILogger logger, IService service) : ICommand +public class ConfigCommand(ILogger logger, IService service) : ICommand { - private readonly ILogger logger = logger.NotNull(); - private readonly IService service = service.NotNull(); + private readonly ILogger _logger = logger.NotNull(); + private readonly IService _service = service.NotNull(); public Task InvokeAsync(ConfigSettings settings, CancellationToken cancellationToken = default) { - var value = service.Call(); - logger.LogInformation($"Command : 'config', LogFile : '{settings.LogFile}', WorkDir : '{settings.WorkDir}' "); + var value = _service.Call(); + _logger.LogInformation($"Command : 'config', LogFile : '{settings.LogFile}', WorkDir : '{settings.WorkDir}' "); return Task.FromResult(value); } } diff --git a/new-cli/GitVersion.Configuration/Init/ConfigInitCommand.cs b/new-cli/GitVersion.Configuration/Init/ConfigInitCommand.cs index a636b36647..1a1298e3be 100644 --- a/new-cli/GitVersion.Configuration/Init/ConfigInitCommand.cs +++ b/new-cli/GitVersion.Configuration/Init/ConfigInitCommand.cs @@ -1,20 +1,20 @@ using GitVersion.Extensions; -using GitVersion.Infrastructure; +using Microsoft.Extensions.Logging; namespace GitVersion.Commands; public record ConfigInitSettings : ConfigSettings; [Command("init", "Inits the configuration for current repository.")] -public class ConfigInitCommand(ILogger logger, IService service) : ICommand +public class ConfigInitCommand(ILogger logger, IService service) : ICommand { - private readonly ILogger logger = logger.NotNull(); - private readonly IService service = service.NotNull(); + private readonly ILogger _logger = logger.NotNull(); + private readonly IService _service = service.NotNull(); public Task InvokeAsync(ConfigInitSettings settings, CancellationToken cancellationToken = default) { - var value = service.Call(); - logger.LogInformation($"Command : 'config init', LogFile : '{settings.LogFile}', WorkDir : '{settings.WorkDir}' "); + var value = _service.Call(); + _logger.LogInformation($"Command : 'config init', LogFile : '{settings.LogFile}', WorkDir : '{settings.WorkDir}' "); return Task.FromResult(value); } } diff --git a/new-cli/GitVersion.Configuration/Show/ConfigShowCommand.cs b/new-cli/GitVersion.Configuration/Show/ConfigShowCommand.cs index 585e7cfe59..6da714ca22 100644 --- a/new-cli/GitVersion.Configuration/Show/ConfigShowCommand.cs +++ b/new-cli/GitVersion.Configuration/Show/ConfigShowCommand.cs @@ -1,20 +1,20 @@ using GitVersion.Extensions; -using GitVersion.Infrastructure; +using Microsoft.Extensions.Logging; namespace GitVersion.Commands; public record ConfigShowSettings : ConfigSettings; [Command("show", "Shows the effective configuration.")] -public class ConfigShowCommand(ILogger logger, IService service) : ICommand +public class ConfigShowCommand(ILogger logger, IService service) : ICommand { - private readonly ILogger logger = logger.NotNull(); - private readonly IService service = service.NotNull(); + private readonly ILogger _logger = logger.NotNull(); + private readonly IService _service = service.NotNull(); public Task InvokeAsync(ConfigShowSettings settings, CancellationToken cancellationToken = default) { - var value = service.Call(); - logger.LogInformation($"Command : 'config show', LogFile : '{settings.LogFile}', WorkDir : '{settings.WorkDir}' "); + var value = _service.Call(); + _logger.LogInformation($"Command : 'config show', LogFile : '{settings.LogFile}', WorkDir : '{settings.WorkDir}' "); return Task.FromResult(value); } } diff --git a/new-cli/GitVersion.Core.Libgit2Sharp/LibGit2SharpCoreModule.cs b/new-cli/GitVersion.Core.Libgit2Sharp/LibGit2SharpCoreModule.cs index bc06d9bd32..40572492dc 100644 --- a/new-cli/GitVersion.Core.Libgit2Sharp/LibGit2SharpCoreModule.cs +++ b/new-cli/GitVersion.Core.Libgit2Sharp/LibGit2SharpCoreModule.cs @@ -1,8 +1,9 @@ using GitVersion.Infrastructure; +using Microsoft.Extensions.DependencyInjection; namespace GitVersion.Git; public class LibGit2SharpCoreModule : IGitVersionModule { - public void RegisterTypes(IContainerRegistrar services) => services.AddSingleton(); + public IServiceCollection RegisterTypes(IServiceCollection services) => services.AddSingleton(); } diff --git a/new-cli/GitVersion.Core.Tester/GitVersionApp.cs b/new-cli/GitVersion.Core.Tester/GitVersionApp.cs index 83e4a7a31a..e4696ff783 100644 --- a/new-cli/GitVersion.Core.Tester/GitVersionApp.cs +++ b/new-cli/GitVersion.Core.Tester/GitVersionApp.cs @@ -1,21 +1,12 @@ using GitVersion.Git; using GitVersion.Infrastructure; +using Microsoft.Extensions.Logging; namespace GitVersion; -public class GitVersionApp +public class GitVersionApp(ILogger logger, IGitRepository repository) : ICliApp { - private readonly ILogger logger; - private readonly IGitRepository repository; - public GitVersionApp(ILogger logger, IGitRepository repository) - { - this.logger = logger; - this.repository = repository; - } - -#pragma warning disable IDE0060 - public Task RunAsync(string[] args) -#pragma warning restore IDE0060 + public Task RunAsync(string[] args, CancellationToken cancellationToken) { repository.DiscoverRepository(Directory.GetCurrentDirectory()); var branches = repository.Branches.ToList(); diff --git a/new-cli/GitVersion.Core.Tester/Program.cs b/new-cli/GitVersion.Core.Tester/Program.cs index 2325f7d0d2..f09d3b3be3 100644 --- a/new-cli/GitVersion.Core.Tester/Program.cs +++ b/new-cli/GitVersion.Core.Tester/Program.cs @@ -2,28 +2,32 @@ using GitVersion.Extensions; using GitVersion.Git; using GitVersion.Infrastructure; +using Microsoft.Extensions.DependencyInjection; -var assemblies = new IGitVersionModule[] +var modules = new IGitVersionModule[] { new CoreModule(), new LibGit2SharpCoreModule(), }; -using var serviceProvider = RegisterModules(assemblies); -var app = serviceProvider.GetRequiredService(); -var result = await app.RunAsync(args); +var cts = new CancellationTokenSource(); +Console.CancelKeyPress += (_, _) => cts.Cancel(); +await using var serviceProvider = RegisterModules(modules); +var app = serviceProvider.GetRequiredService(); + +var result = await app.RunAsync(args, cts.Token).ConfigureAwait(false); if (!Console.IsInputRedirected) Console.ReadKey(); return result; -static IContainer RegisterModules(IEnumerable gitVersionModules) +static ServiceProvider RegisterModules(IEnumerable gitVersionModules) { - var serviceProvider = new ContainerRegistrar() + var serviceProvider = new ServiceCollection() .RegisterModules(gitVersionModules) - .AddSingleton() - .AddLogging() - .Build(); + .AddSingleton() + .RegisterLogging() + .BuildServiceProvider(); return serviceProvider; } diff --git a/new-cli/GitVersion.Core/CoreModule.cs b/new-cli/GitVersion.Core/CoreModule.cs index f3312b5dd2..eddf57e325 100644 --- a/new-cli/GitVersion.Core/CoreModule.cs +++ b/new-cli/GitVersion.Core/CoreModule.cs @@ -1,14 +1,16 @@ using System.IO.Abstractions; using GitVersion.Infrastructure; +using Microsoft.Extensions.DependencyInjection; namespace GitVersion; public class CoreModule : IGitVersionModule { - public void RegisterTypes(IContainerRegistrar services) + public IServiceCollection RegisterTypes(IServiceCollection services) { services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + return services; } } diff --git a/new-cli/GitVersion.Core/Extensions/ContainerRegistrarExtensions.cs b/new-cli/GitVersion.Core/Extensions/ContainerRegistrarExtensions.cs deleted file mode 100644 index c961b7f823..0000000000 --- a/new-cli/GitVersion.Core/Extensions/ContainerRegistrarExtensions.cs +++ /dev/null @@ -1,15 +0,0 @@ -using GitVersion.Infrastructure; - -namespace GitVersion.Extensions; - -public static class ContainerRegistrarExtensions -{ - public static IContainerRegistrar RegisterModules(this IContainerRegistrar containerRegistrar, IEnumerable gitVersionModules) - => gitVersionModules.Aggregate(containerRegistrar, (current, gitVersionModule) => current.RegisterModule(gitVersionModule)); - - public static IContainerRegistrar RegisterModule(this IContainerRegistrar containerRegistrar, IGitVersionModule gitVersionModule) - { - gitVersionModule.RegisterTypes(containerRegistrar); - return containerRegistrar; - } -} diff --git a/new-cli/GitVersion.Core/Extensions/ServiceCollectionExtensions.cs b/new-cli/GitVersion.Core/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 0000000000..e7e5ac36a9 --- /dev/null +++ b/new-cli/GitVersion.Core/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,41 @@ +using GitVersion.Infrastructure; +using Microsoft.Extensions.DependencyInjection; +using Serilog; + +namespace GitVersion.Extensions; + +public static class ServiceCollectionExtensions +{ + public static IServiceCollection RegisterModules(this IServiceCollection services, IEnumerable gitVersionModules) + => gitVersionModules.Aggregate(services, (current, gitVersionModule) => gitVersionModule.RegisterTypes(current)); + + public static IServiceCollection RegisterLogging(this IServiceCollection services) + { + services.AddLogging(builder => + { + var logger = CreateLogger(); + builder.AddSerilog(logger, dispose: true); + }); + + return services; + } + + private static Serilog.Core.Logger CreateLogger() + { + var logger = new LoggerConfiguration() + // log level will dynamically be controlled by our log interceptor upon running + .MinimumLevel.ControlledBy(LoggingEnricher.LogLevel) + // the log enricher will add a new property with the log file path from the settings + // that we can use to set the path dynamically + .Enrich.With() + // serilog.sinks.map will defer the configuration of the sink to be on demand, + // allowing us to look at the properties set by the enricher to set the path appropriately + .WriteTo.Console() + .WriteTo.Map(LoggingEnricher.LogFilePathPropertyName, (logFilePath, sinkConfiguration) => + { + if (!string.IsNullOrEmpty(logFilePath)) sinkConfiguration.File(logFilePath); + }, 1) + .CreateLogger(); + return logger; + } +} diff --git a/new-cli/GitVersion.Core/Infrastructure/Container.cs b/new-cli/GitVersion.Core/Infrastructure/Container.cs deleted file mode 100644 index efddeaf46e..0000000000 --- a/new-cli/GitVersion.Core/Infrastructure/Container.cs +++ /dev/null @@ -1,30 +0,0 @@ -using GitVersion.Extensions; -using Microsoft.Extensions.DependencyInjection; - -namespace GitVersion.Infrastructure; - -public sealed class Container(ServiceProvider serviceProvider) : IContainer -{ - private readonly ServiceProvider serviceProvider = serviceProvider.NotNull(); - - public T? GetService() => serviceProvider.GetService(); - public T GetRequiredService() where T : notnull => serviceProvider.GetRequiredService(); - - public object GetService(Type type) => serviceProvider.GetRequiredService(type); - public object GetRequiredService(Type type) => serviceProvider.GetRequiredService(type); - - public void Dispose() - { - Dispose(true); - // ReSharper disable once GCSuppressFinalizeForTypeWithoutDestructor - GC.SuppressFinalize(this); // Violates rule - } - - private void Dispose(bool disposing) - { - if (disposing) - { - serviceProvider.Dispose(); - } - } -} diff --git a/new-cli/GitVersion.Core/Infrastructure/ContainerRegistrar.cs b/new-cli/GitVersion.Core/Infrastructure/ContainerRegistrar.cs deleted file mode 100644 index 262f92a652..0000000000 --- a/new-cli/GitVersion.Core/Infrastructure/ContainerRegistrar.cs +++ /dev/null @@ -1,66 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Serilog; - -namespace GitVersion.Infrastructure; - -public sealed class ContainerRegistrar : IContainerRegistrar -{ - private readonly ServiceCollection services = []; - - public IContainerRegistrar AddSingleton() - where TService : class - where TImplementation : class, TService - { - services.AddSingleton(); - return this; - } - - public IContainerRegistrar AddSingleton() - where TService : class - => AddSingleton(); - - public IContainerRegistrar AddTransient() - where TService : class - where TImplementation : class, TService - { - services.AddTransient(); - return this; - } - - public IContainerRegistrar AddTransient() - where TService : class - => AddTransient(); - - public IContainerRegistrar AddLogging() - { - services.AddLogging(builder => - { - var logger = CreateLogger(); - builder.AddSerilog(logger, dispose: true); - }); - services.AddSingleton(provider => new Logger(provider.GetRequiredService>())); - return this; - } - - public IContainer Build() => new Container(services.BuildServiceProvider()); - - private static Serilog.Core.Logger CreateLogger() - { - var logger = new LoggerConfiguration() - // log level will be dynamically be controlled by our log interceptor upon running - .MinimumLevel.ControlledBy(LoggingEnricher.LogLevel) - // the log enricher will add a new property with the log file path from the settings - // that we can use to set the path dynamically - .Enrich.With() - // serilog.sinks.map will defer the configuration of the sink to be on demand - // allowing us to look at the properties set by the enricher to set the path appropriately - .WriteTo.Console() - .WriteTo.Map(LoggingEnricher.LogFilePathPropertyName, (logFilePath, wt) => - { - if (!string.IsNullOrEmpty(logFilePath)) wt.File(logFilePath); - }, 1) - .CreateLogger(); - return logger; - } -} diff --git a/new-cli/GitVersion.Core/Infrastructure/Logger.cs b/new-cli/GitVersion.Core/Infrastructure/Logger.cs deleted file mode 100644 index 33b94d9909..0000000000 --- a/new-cli/GitVersion.Core/Infrastructure/Logger.cs +++ /dev/null @@ -1,16 +0,0 @@ -using GitVersion.Extensions; -using Microsoft.Extensions.Logging; - -namespace GitVersion.Infrastructure; - -public sealed class Logger(Microsoft.Extensions.Logging.ILogger logger) : ILogger -{ - private readonly Microsoft.Extensions.Logging.ILogger logger = logger.NotNull(); - - public void LogTrace(string? message, params object?[] args) => logger.LogTrace(message, args); - public void LogDebug(string? message, params object?[] args) => logger.LogDebug(message, args); - public void LogInformation(string? message, params object?[] args) => logger.LogInformation(message, args); - public void LogWarning(string? message, params object?[] args) => logger.LogWarning(message, args); - public void LogError(string? message, params object?[] args) => logger.LogError(message, args); - public void LogCritical(string? message, params object?[] args) => logger.LogCritical(message, args); -} diff --git a/new-cli/GitVersion.Core/Infrastructure/LoggingEnricher.cs b/new-cli/GitVersion.Core/Infrastructure/LoggingEnricher.cs index 453f59eaba..c20dadce14 100644 --- a/new-cli/GitVersion.Core/Infrastructure/LoggingEnricher.cs +++ b/new-cli/GitVersion.Core/Infrastructure/LoggingEnricher.cs @@ -7,32 +7,49 @@ public class LoggingEnricher : ILogEventEnricher { public static readonly LoggingLevelSwitch LogLevel = new(); private string? _cachedLogFilePath; - private LogEventProperty? _cachedLogFilePathProperty; + private LogEventProperty? _cachedLogFilePathProp; // this path and level will be set by the LogInterceptor.cs after parsing the settings - public static string Path = string.Empty; + private static string _path = string.Empty; public const string LogFilePathPropertyName = "LogFilePath"; - public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) + public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propFactory) { - // the settings might not have a path, or we might not be within a command in which case - // we won't have the setting so a default value for the log file will be required - LogEventProperty logFilePathProperty; + // the settings might not have a path, or we might not be within a command, in which case + // we won't have the setting, so a default value for the log file will be required + LogEventProperty logFilePathProp; - if (this._cachedLogFilePathProperty != null && Path.Equals(this._cachedLogFilePath)) + if (_cachedLogFilePathProp != null && _path.Equals(_cachedLogFilePath)) { - // Path hasn't changed, so let's use the cached property - logFilePathProperty = this._cachedLogFilePathProperty; + // The Path hasn't changed, so let's use the cached property + logFilePathProp = _cachedLogFilePathProp; } else { // We've got a new path for the log. Let's create a new property // and cache it for future log events to use - this._cachedLogFilePath = Path; - this._cachedLogFilePathProperty = logFilePathProperty = propertyFactory.CreateProperty(LogFilePathPropertyName, Path); + _cachedLogFilePath = _path; + _cachedLogFilePathProp = logFilePathProp = propFactory.CreateProperty(LogFilePathPropertyName, _path); } - logEvent.AddPropertyIfAbsent(logFilePathProperty); + logEvent.AddPropertyIfAbsent(logFilePathProp); } + + public static void Configure(string? logFile, Verbosity verbosity) + { + if (!string.IsNullOrWhiteSpace(logFile)) _path = logFile; + LogLevel.MinimumLevel = GetLevelForVerbosity(verbosity); + } + + private static LogEventLevel GetLevelForVerbosity(Verbosity verbosity) => VerbosityMaps[verbosity]; + + private static readonly Dictionary VerbosityMaps = new() + { + { Verbosity.Verbose, LogEventLevel.Verbose }, + { Verbosity.Diagnostic, LogEventLevel.Debug }, + { Verbosity.Normal, LogEventLevel.Information }, + { Verbosity.Minimal, LogEventLevel.Warning }, + { Verbosity.Quiet, LogEventLevel.Error }, + }; } diff --git a/new-cli/GitVersion.Normalization/NormalizeCommand.cs b/new-cli/GitVersion.Normalization/NormalizeCommand.cs index 3709700f25..ed38ea327b 100644 --- a/new-cli/GitVersion.Normalization/NormalizeCommand.cs +++ b/new-cli/GitVersion.Normalization/NormalizeCommand.cs @@ -1,20 +1,20 @@ using GitVersion.Extensions; -using GitVersion.Infrastructure; +using Microsoft.Extensions.Logging; namespace GitVersion.Commands; public record NormalizeSettings : GitVersionSettings; [Command("normalize", "Normalizes the git repository for GitVersion calculations.")] -public class NormalizeCommand(ILogger logger, IService service) : ICommand +public class NormalizeCommand(ILogger logger, IService service) : ICommand { - private readonly ILogger logger = logger.NotNull(); - private readonly IService service = service.NotNull(); + private readonly ILogger _logger = logger.NotNull(); + private readonly IService _service = service.NotNull(); public Task InvokeAsync(NormalizeSettings settings, CancellationToken cancellationToken = default) { - var value = service.Call(); - logger.LogInformation($"Command : 'normalize', LogFile : '{settings.LogFile}', WorkDir : '{settings.WorkDir}' "); + var value = _service.Call(); + _logger.LogInformation($"Command : 'normalize', LogFile : '{settings.LogFile}', WorkDir : '{settings.WorkDir}' "); return Task.FromResult(value); } } diff --git a/new-cli/GitVersion.Output/AssemblyInfo/OutputAssemblyInfoCommand.cs b/new-cli/GitVersion.Output/AssemblyInfo/OutputAssemblyInfoCommand.cs index 43e8187eb2..44c373ef46 100644 --- a/new-cli/GitVersion.Output/AssemblyInfo/OutputAssemblyInfoCommand.cs +++ b/new-cli/GitVersion.Output/AssemblyInfo/OutputAssemblyInfoCommand.cs @@ -1,20 +1,20 @@ using GitVersion.Extensions; -using GitVersion.Infrastructure; +using Microsoft.Extensions.Logging; namespace GitVersion.Commands; [Command("assemblyinfo", "Outputs version to assembly")] -public class OutputAssemblyInfoCommand(ILogger logger, IService service) : ICommand +public class OutputAssemblyInfoCommand(ILogger logger, IService service) : ICommand { - private readonly ILogger logger = logger.NotNull(); - private readonly IService service = service.NotNull(); + private readonly ILogger _logger = logger.NotNull(); + private readonly IService _service = service.NotNull(); public Task InvokeAsync(OutputAssemblyInfoSettings settings, CancellationToken cancellationToken = default) { - var value = service.Call(); + var value = _service.Call(); var versionInfo = settings.VersionInfo.Value; - logger.LogInformation($"Command : 'output assemblyinfo', LogFile : '{settings.LogFile}', WorkDir : '{settings.OutputDir}', InputFile: '{settings.InputFile}', AssemblyInfo: '{settings.AssemblyinfoFile}' "); - logger.LogInformation($"Version info: {versionInfo}"); + _logger.LogInformation($"Command : 'output assemblyinfo', LogFile : '{settings.LogFile}', WorkDir : '{settings.OutputDir}', InputFile: '{settings.InputFile}', AssemblyInfo: '{settings.AssemblyinfoFile}' "); + _logger.LogInformation($"Version info: {versionInfo}"); return Task.FromResult(value); } } diff --git a/new-cli/GitVersion.Output/OutputCommand.cs b/new-cli/GitVersion.Output/OutputCommand.cs index 3e3f5796a3..2791d211cf 100644 --- a/new-cli/GitVersion.Output/OutputCommand.cs +++ b/new-cli/GitVersion.Output/OutputCommand.cs @@ -1,18 +1,18 @@ using GitVersion.Extensions; -using GitVersion.Infrastructure; +using Microsoft.Extensions.Logging; namespace GitVersion.Commands; [Command("output", "Outputs the version object.")] -public class OutputCommand(ILogger logger, IService service) : ICommand +public class OutputCommand(ILogger logger, IService service) : ICommand { - private readonly ILogger logger = logger.NotNull(); - private readonly IService service = service.NotNull(); + private readonly ILogger _logger = logger.NotNull(); + private readonly IService _service = service.NotNull(); public Task InvokeAsync(OutputSettings settings, CancellationToken cancellationToken = default) { - var value = service.Call(); - logger.LogInformation($"Command : 'output', LogFile : '{settings.LogFile}', WorkDir : '{settings.WorkDir}' "); + var value = _service.Call(); + _logger.LogInformation($"Command : 'output', LogFile : '{settings.LogFile}', WorkDir : '{settings.WorkDir}' "); return Task.FromResult(value); } } diff --git a/new-cli/GitVersion.Output/Project/OutputProjectCommand.cs b/new-cli/GitVersion.Output/Project/OutputProjectCommand.cs index d02df30d89..2608bc51fb 100644 --- a/new-cli/GitVersion.Output/Project/OutputProjectCommand.cs +++ b/new-cli/GitVersion.Output/Project/OutputProjectCommand.cs @@ -1,18 +1,18 @@ using GitVersion.Extensions; -using GitVersion.Infrastructure; +using Microsoft.Extensions.Logging; namespace GitVersion.Commands; [Command("project", "Outputs version to project")] -public class OutputProjectCommand(ILogger logger, IService service) : ICommand +public class OutputProjectCommand(ILogger logger, IService service) : ICommand { - private readonly ILogger logger = logger.NotNull(); - private readonly IService service = service.NotNull(); + private readonly ILogger _logger = logger.NotNull(); + private readonly IService _service = service.NotNull(); public Task InvokeAsync(OutputProjectSettings settings, CancellationToken cancellationToken = default) { - var value = service.Call(); - logger.LogInformation($"Command : 'output project', LogFile : '{settings.LogFile}', WorkDir : '{settings.OutputDir}', InputFile: '{settings.InputFile}', Project: '{settings.ProjectFile}' "); + var value = _service.Call(); + _logger.LogInformation($"Command : 'output project', LogFile : '{settings.LogFile}', WorkDir : '{settings.OutputDir}', InputFile: '{settings.InputFile}', Project: '{settings.ProjectFile}' "); return Task.FromResult(value); } } diff --git a/new-cli/GitVersion.Output/Wix/OutputWixCommand.cs b/new-cli/GitVersion.Output/Wix/OutputWixCommand.cs index 179b7fc489..abe89f7e9e 100644 --- a/new-cli/GitVersion.Output/Wix/OutputWixCommand.cs +++ b/new-cli/GitVersion.Output/Wix/OutputWixCommand.cs @@ -1,18 +1,18 @@ using GitVersion.Extensions; -using GitVersion.Infrastructure; +using Microsoft.Extensions.Logging; namespace GitVersion.Commands; [Command("wix", "Outputs version to wix file")] -public class OutputWixCommand(ILogger logger, IService service) : ICommand +public class OutputWixCommand(ILogger logger, IService service) : ICommand { - private readonly ILogger logger = logger.NotNull(); - private readonly IService service = service.NotNull(); + private readonly ILogger _logger = logger.NotNull(); + private readonly IService _service = service.NotNull(); public Task InvokeAsync(OutputWixSettings settings, CancellationToken cancellationToken = default) { - var value = service.Call(); - logger.LogInformation($"Command : 'output wix', LogFile : '{settings.LogFile}', WorkDir : '{settings.OutputDir}', InputFile: '{settings.InputFile}', WixFile: '{settings.WixFile}' "); + var value = _service.Call(); + _logger.LogInformation($"Command : 'output wix', LogFile : '{settings.LogFile}', WorkDir : '{settings.OutputDir}', InputFile: '{settings.InputFile}', WixFile: '{settings.WixFile}' "); return Task.FromResult(value); } } diff --git a/new-cli/GitVersion.slnx b/new-cli/GitVersion.slnx index 5569cfa721..995eb6f857 100644 --- a/new-cli/GitVersion.slnx +++ b/new-cli/GitVersion.slnx @@ -27,6 +27,7 @@ +