diff --git a/src/ProjectDiff.Core/ProjectDiff.Core.csproj b/src/ProjectDiff.Core/ProjectDiff.Core.csproj index 8515e8e..2a3b2b1 100644 --- a/src/ProjectDiff.Core/ProjectDiff.Core.csproj +++ b/src/ProjectDiff.Core/ProjectDiff.Core.csproj @@ -10,9 +10,8 @@ - + all - runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/ProjectDiff.Tool/ExtendedConsole.cs b/src/ProjectDiff.Tool/ExtendedConsole.cs index cddac3f..12e9f2d 100644 --- a/src/ProjectDiff.Tool/ExtendedConsole.cs +++ b/src/ProjectDiff.Tool/ExtendedConsole.cs @@ -1,38 +1,23 @@ -using System.CommandLine; -using System.CommandLine.IO; +namespace ProjectDiff.Tool; -namespace ProjectDiff.Tool; - -public sealed class ExtendedConsole : IExtendedConsole +public sealed class SystemConsole : IConsole { - private readonly SystemConsole _console; - - public ExtendedConsole() - { - _console = new SystemConsole(); - WorkingDirectory = Directory.GetCurrentDirectory(); - } - - public IStandardStreamWriter Error => _console.Error; - - public bool IsErrorRedirected => _console.IsErrorRedirected; - - public IStandardStreamWriter Out => _console.Out; - - public bool IsOutputRedirected => _console.IsOutputRedirected; - - public bool IsInputRedirected => _console.IsInputRedirected; - - public string WorkingDirectory { get; } + public TextWriter Error { get; } = Console.Error; + public TextWriter Out { get; } = Console.Out; public Stream OpenStandardOutput() { return Console.OpenStandardOutput(); } + + public string WorkingDirectory { get; } = Directory.GetCurrentDirectory(); } -public interface IExtendedConsole : IConsole +public interface IConsole { + TextWriter Error { get; } + TextWriter Out { get; } + Stream OpenStandardOutput(); string WorkingDirectory { get; } } \ No newline at end of file diff --git a/src/ProjectDiff.Tool/Program.cs b/src/ProjectDiff.Tool/Program.cs index 0b89d1c..c75cfcc 100644 --- a/src/ProjectDiff.Tool/Program.cs +++ b/src/ProjectDiff.Tool/Program.cs @@ -1,8 +1,9 @@ -using Microsoft.Build.Locator; +using System.CommandLine; +using Microsoft.Build.Locator; using ProjectDiff.Tool; MSBuildLocator.RegisterDefaults(); -var tool = ProjectDiffTool.Create(new ExtendedConsole()); +var tool = ProjectDiffTool.Create(new SystemConsole()); return await tool.InvokeAsync(args); \ No newline at end of file diff --git a/src/ProjectDiff.Tool/ProjectDiff.Tool.csproj b/src/ProjectDiff.Tool/ProjectDiff.Tool.csproj index 952ccc8..e3b99ae 100644 --- a/src/ProjectDiff.Tool/ProjectDiff.Tool.csproj +++ b/src/ProjectDiff.Tool/ProjectDiff.Tool.csproj @@ -12,17 +12,16 @@ - + all - runtime; build; native; contentfiles; analyzers; buildtransitive all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + diff --git a/src/ProjectDiff.Tool/ProjectDiffCommand.cs b/src/ProjectDiff.Tool/ProjectDiffCommand.cs index 77ae9b3..55cd98e 100644 --- a/src/ProjectDiff.Tool/ProjectDiffCommand.cs +++ b/src/ProjectDiff.Tool/ProjectDiffCommand.cs @@ -1,6 +1,4 @@ using System.CommandLine; -using System.CommandLine.IO; -using System.CommandLine.NamingConventionBinder; using System.Text.Json; using System.Text.Json.Nodes; using System.Text.Json.Serialization; @@ -21,126 +19,149 @@ public sealed class ProjectDiffCommand : RootCommand } }; - private static readonly Argument SolutionArgument = new( - "solution", - "Path to solution file to derive projects from" - ) + private static readonly Argument SolutionArgument = new("solution") { - Arity = ArgumentArity.ExactlyOne - }; - - private static readonly Option BaseCommitOption = new( - ["--base-ref", "--base"], - () => "HEAD", - "Base git reference to compare against" - ) - { - IsRequired = true - }; - - private static readonly Option HeadCommitOption = new( - ["--head-ref", "--head"], - "Head git reference to compare against. If not specified current working tree will be used" - ) - { - IsRequired = false - }; - - private static readonly Option MergeBaseOption = new( - "--merge-base", - () => true, - "If true instead of using --base use the merge base of --base and --head as the --base reference, if --head is not specified 'HEAD' will be used" - ); - - private static readonly Option IncludeDeleted = new( - "--include-deleted", - () => false, - "If true deleted projects will be included in output" - ); - - private static readonly Option IncludeModified = new( - "--include-modified", - () => true, - "If true modified projects will be included in output" - ); - - private static readonly Option IncludeAdded = new( - "--include-added", - () => true, - "If true added projects will be included in output" - ); - - private static readonly Option IncludeReferencing = new( - "--include-referencing", - () => true, - "if true projects referencing modified/deleted/added projects will be included in output" - ); - - private static readonly Option Format = new( - ["--format", "-f"], - "Output format, if --output is specified format will be derived from file extension. Otherwise this defaults to 'plain'" - ); - - private static readonly Option AbsolutePaths = new( - "--absolute-paths", - () => false, - "Output absolute paths, if not specified paths will be relative to the working directory. Or relative to --output if specified. This option will not affect slnf format as this requires relative paths" - ); - - private static readonly Option OutputOption = new( - ["--output", "--out", "-o"], - "Output file, if not set stdout will be used" - ); - - private static readonly Option IgnoreChangedFilesOption = new( - "--ignore-changed-file", - () => [], - "Ignore changes in specific files. If these files are a part of the build evaluation process they will still be evaluated, however these files will be considered unchanged by the diff process" - ); - - private readonly IExtendedConsole _console; - - - public ProjectDiffCommand(IExtendedConsole console) - { - _console = console; - Name = "dotnet-proj-diff"; - Description = "Calculate which projects in a solution has changed since a specific commit"; - SolutionArgument.AddValidator( + Arity = ArgumentArity.ExactlyOne, + Description = "Path to solution file to derive projects from", + Validators = + { x => { var f = x.GetValueOrDefault(); if (f is null) { - x.ErrorMessage = $"{x.Argument.Name} must be specified"; + x.AddError("{x.Argument.Name} must be specified"); } else if (!f.Exists) { - x.ErrorMessage = $"File '{f.FullName}' does not exist."; + x.AddError($"File '{f.FullName}' does not exist."); } else if (f.Extension is not (".sln" or ".slnx")) { - x.ErrorMessage = $"File '{f.FullName}' is not a valid sln file."; + x.AddError($"File '{f.FullName}' is not a valid sln file."); } } - ); - AddArgument(SolutionArgument); - AddOption(BaseCommitOption); - AddOption(HeadCommitOption); - AddOption(MergeBaseOption); - AddOption(IncludeDeleted); - AddOption(IncludeModified); - AddOption(IncludeAdded); - AddOption(IncludeReferencing); - AddOption(AbsolutePaths); - AddOption(Format); - AddOption(OutputOption); - AddOption(IgnoreChangedFilesOption); - Handler = CommandHandler.Create(ExecuteAsync); + } + }; + + private static readonly Option BaseCommitOption = new( + "--base-ref", + "--base" + ) + { + Description = "Base git reference to compare against, if not specified 'HEAD' will be used", + DefaultValueFactory = _ => "HEAD", + Required = true + }; + + private static readonly Option HeadCommitOption = new("--head-ref", "--head") + + { + Description = "Head git reference to compare against. If not specified current working tree will be used", + Required = false + }; + + private static readonly Option MergeBaseOption = new("--merge-base") + { + Description = + "If true instead of using --base use the merge base of --base and --head as the --base reference, if --head is not specified 'HEAD' will be used", + DefaultValueFactory = _ => true, + }; + + private static readonly Option IncludeDeleted = new("--include-deleted") + { + DefaultValueFactory = _ => false, + Description = "If true deleted projects will be included in output" + }; + + private static readonly Option IncludeModified = new("--include-modified") + { + DefaultValueFactory = _ => true, + Description = "If true modified projects will be included in output" + }; + + private static readonly Option IncludeAdded = new("--include-added") + { + DefaultValueFactory = _ => true, + Description = "If true added projects will be included in output" + }; + + private static readonly Option IncludeReferencing = new("--include-referencing") + { + DefaultValueFactory = _ => true, + Description = "if true projects referencing modified/deleted/added projects will be included in output" + }; + + private static readonly Option Format = new("--format", "-f") + { + Description = + "Output format, if --output is specified format will be derived from file extension. Otherwise this defaults to 'plain'" + }; + + private static readonly Option AbsolutePaths = new("--absolute-paths") + { + DefaultValueFactory = _ => false, + Description = + "Output absolute paths, if not specified paths will be relative to the working directory. Or relative to --output if specified. This option will not affect slnf format as this requires relative paths" + }; + + private static readonly Option OutputOption = new("--output", "--out", "-o") + { + Description = "Output file, if not set stdout will be used" + }; + + private static readonly Option IgnoreChangedFilesOption = new("--ignore-changed-file") + { + + DefaultValueFactory = _ => [], + Description = + "Ignore changes in specific files. If these files are a part of the build evaluation process they will still be evaluated, however these files will be considered unchanged by the diff process" + }; + + private readonly IConsole _console; + + + public ProjectDiffCommand(IConsole console) + { + _console = console; + Description = "Calculate which projects in a solution has changed since a specific commit"; + Arguments.Add(SolutionArgument); + Options.Add(BaseCommitOption); + Options.Add(HeadCommitOption); + Options.Add(MergeBaseOption); + Options.Add(IncludeDeleted); + Options.Add(IncludeModified); + Options.Add(IncludeAdded); + Options.Add(IncludeReferencing); + Options.Add(AbsolutePaths); + Options.Add(Format); + Options.Add(OutputOption); + Options.Add(IgnoreChangedFilesOption); + SetAction(ExecuteAsync); } + private Task ExecuteAsync(ParseResult parseResult, CancellationToken cancellationToken) + { + var settings = new ProjectDiffSettings + { + Format = parseResult.GetValue(Format), + Output = parseResult.GetValue(OutputOption), + Solution = parseResult.GetRequiredValue(SolutionArgument), + BaseRef = parseResult.GetRequiredValue(BaseCommitOption), + HeadRef = parseResult.GetValue(HeadCommitOption), + MergeBase = parseResult.GetValue(MergeBaseOption), + IncludeDeleted = parseResult.GetValue(IncludeDeleted), + IncludeModified = parseResult.GetValue(IncludeModified), + IncludeAdded = parseResult.GetValue(IncludeAdded), + IncludeReferencing = parseResult.GetValue(IncludeReferencing), + AbsolutePaths = parseResult.GetValue(AbsolutePaths), + IgnoreChangedFile = parseResult.GetRequiredValue(IgnoreChangedFilesOption) + }; + + return ExecuteCoreAsync(settings, cancellationToken); + } - private async Task ExecuteAsync( + private async Task ExecuteCoreAsync( ProjectDiffSettings settings, CancellationToken cancellationToken ) @@ -239,8 +260,7 @@ private static async Task WriteJson( bool absolutePaths ) { - diff = diff.Select( - project => project with + diff = diff.Select(project => project with { Path = NormalizePath( output.RootDirectory, @@ -326,7 +346,7 @@ private static void WriteError(IConsole console, string error) }; - private sealed class DiffOutput(FileInfo? outputFile, IExtendedConsole console) + private sealed class DiffOutput(FileInfo? outputFile, IConsole console) { public string RootDirectory => outputFile?.DirectoryName ?? console.WorkingDirectory; diff --git a/src/ProjectDiff.Tool/ProjectDiffSettings.cs b/src/ProjectDiff.Tool/ProjectDiffSettings.cs index 8a3126f..4e02532 100644 --- a/src/ProjectDiff.Tool/ProjectDiffSettings.cs +++ b/src/ProjectDiff.Tool/ProjectDiffSettings.cs @@ -3,15 +3,15 @@ public sealed class ProjectDiffSettings { public required string BaseRef { get; init; } - public string? HeadRef { get; init; } + public required string? HeadRef { get; init; } public required FileInfo Solution { get; init; } - public bool MergeBase { get; init; } = true; - public bool IncludeDeleted { get; init; } - public bool IncludeModified { get; init; } - public bool IncludeAdded { get; init; } - public bool IncludeReferencing { get; init; } - public bool AbsolutePaths { get; init; } - public OutputFormat? Format { get; init; } - public FileInfo? Output { get; init; } - public FileInfo[] IgnoreChangedFile { get; init; } = []; + public required bool MergeBase { get; init; } = true; + public required bool IncludeDeleted { get; init; } + public required bool IncludeModified { get; init; } + public required bool IncludeAdded { get; init; } + public required bool IncludeReferencing { get; init; } + public required bool AbsolutePaths { get; init; } + public required OutputFormat? Format { get; init; } + public required FileInfo? Output { get; init; } + public required FileInfo[] IgnoreChangedFile { get; init; } = []; } \ No newline at end of file diff --git a/src/ProjectDiff.Tool/ProjectDiffTool.cs b/src/ProjectDiff.Tool/ProjectDiffTool.cs index e440d2b..988ccee 100644 --- a/src/ProjectDiff.Tool/ProjectDiffTool.cs +++ b/src/ProjectDiff.Tool/ProjectDiffTool.cs @@ -1,61 +1,37 @@ -using System.CommandLine.Builder; -using System.CommandLine.Invocation; -using System.CommandLine.Parsing; +using System.CommandLine; namespace ProjectDiff.Tool; -public class ProjectDiffTool +public sealed class ProjectDiffTool { - private readonly Parser _parser; - private readonly IExtendedConsole _console; + private readonly CommandLineConfiguration _cli; + private readonly IConsole _console; - private ProjectDiffTool(Parser parser, IExtendedConsole console) + private ProjectDiffTool(CommandLineConfiguration cli, IConsole console) { - _parser = parser; + _cli = cli; _console = console; } public Task InvokeAsync(string[] args) { - return _parser.InvokeAsync(args, _console); + return _cli.InvokeAsync(args); } - public static ProjectDiffTool Create(IExtendedConsole console) + public static ProjectDiffTool Create(IConsole console) { - var parser = BuildParser(console); + var parser = BuildCli(console); return new ProjectDiffTool(parser, console); } - private static Parser BuildParser(IExtendedConsole console) + private static CommandLineConfiguration BuildCli(IConsole console) { - return new CommandLineBuilder(new ProjectDiffCommand(console)) - .UseVersionOption() - .UseHelp() - .UseParseDirective() - .UseParseErrorReporting() - .UseExceptionHandler( - (ex, ctx) => - { - var exitCode = ex switch - { - OperationCanceledException => 125, - AggregateException aggregate when aggregate.InnerExceptions.All( - i => i is OperationCanceledException - ) => - 125, - _ => 1 - }; - if (exitCode == 1) - { - ctx.Console.Error.Write(ex.ToString()); - } - - ctx.ExitCode = exitCode; - } - ) - .CancelOnProcessTermination() - .Build(); + return new CommandLineConfiguration(new ProjectDiffCommand(console)) + { + Error = console.Error, + Output = console.Out, + }; } } \ No newline at end of file diff --git a/test/ProjectDiff.Tests/ProjectDiff.Tests.csproj b/test/ProjectDiff.Tests/ProjectDiff.Tests.csproj index c041054..3558219 100644 --- a/test/ProjectDiff.Tests/ProjectDiff.Tests.csproj +++ b/test/ProjectDiff.Tests/ProjectDiff.Tests.csproj @@ -1,4 +1,4 @@ - + net8.0;net9.0 @@ -11,6 +11,8 @@ + + diff --git a/test/ProjectDiff.Tests/Tool/ProjectDiffTests.cs b/test/ProjectDiff.Tests/Tool/ProjectDiffTests.cs index 902680d..fdd05ce 100644 --- a/test/ProjectDiff.Tests/Tool/ProjectDiffTests.cs +++ b/test/ProjectDiff.Tests/Tool/ProjectDiffTests.cs @@ -304,14 +304,15 @@ await Verify(output, GetExtension(format)) private async Task ExecuteAndReadStdout(TestRepository repository, params string[] args) { - var console = new ExtendedTestConsole(repository.WorkingDirectory); + var console = new TestConsole(repository.WorkingDirectory); var tool = ProjectDiffTool.Create(console); var exitCode = await tool.InvokeAsync(args); if (exitCode != 0) { - Assert.Fail($"Program exited with exit code {exitCode}: {console.Error}"); + var stderr = console.GetStandardError(); + Assert.Fail($"Program exited with exit code {exitCode}: {stderr}"); } - return console.Out.ToString() ?? ""; + return console.GetStandardOutput(); } } \ No newline at end of file diff --git a/test/ProjectDiff.Tests/Utils/ExtendedTestConsole.cs b/test/ProjectDiff.Tests/Utils/ExtendedTestConsole.cs deleted file mode 100644 index df721f2..0000000 --- a/test/ProjectDiff.Tests/Utils/ExtendedTestConsole.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System.CommandLine.IO; -using System.Text; -using ProjectDiff.Tool; - -namespace ProjectDiff.Tests.Utils; - -public class ExtendedTestConsole : IExtendedConsole -{ - private readonly TestWriter _out; - private readonly TestWriter _error; - - public ExtendedTestConsole(string workingDirectory) - { - _out = new TestWriter(); - _error = new TestWriter(); - WorkingDirectory = workingDirectory; - } - - public IStandardStreamWriter Out => _out; - public bool IsOutputRedirected => false; - public IStandardStreamWriter Error => _error; - public bool IsErrorRedirected => false; - public bool IsInputRedirected => false; - public string WorkingDirectory { get; } - - public Stream OpenStandardOutput() - { - return _out.GetStream(); - } -} - -public sealed class TestWriter : IStandardStreamWriter -{ - private readonly MemoryStream _stream = new(); - - public void Write(string? value) - { - if (value is null) - { - return; - } - - _stream.Write(Encoding.UTF8.GetBytes(value)); - } - - public Stream GetStream() - { - return _stream; - } - - - public string ReadToEnd() - { - return Encoding.UTF8.GetString(_stream.ToArray()); - } - - public override string ToString() - { - return ReadToEnd(); - } -} \ No newline at end of file diff --git a/test/ProjectDiff.Tests/Utils/TestConsole.cs b/test/ProjectDiff.Tests/Utils/TestConsole.cs new file mode 100644 index 0000000..5d33d07 --- /dev/null +++ b/test/ProjectDiff.Tests/Utils/TestConsole.cs @@ -0,0 +1,47 @@ +using System.IO.Pipelines; +using ProjectDiff.Tool; + +namespace ProjectDiff.Tests.Utils; + +public class TestConsole : IConsole +{ + private readonly MemoryStream _outStream = new MemoryStream(); + private readonly MemoryStream _errorStream = new MemoryStream(); + private readonly StreamWriter _out; + private readonly StreamWriter _error; + + public TestConsole(string workingDirectory) + { + _out = new StreamWriter(_outStream); + _error = new StreamWriter(_errorStream); + WorkingDirectory = workingDirectory; + } + + public string WorkingDirectory { get; } + + public TextWriter Error => _error; + public TextWriter Out => _out; + + public string GetStandardOutput() + { + _out.Flush(); + _outStream.Seek(0, SeekOrigin.Begin); + using var reader = new StreamReader(_outStream); + return reader.ReadToEnd(); + } + + public string GetStandardError() + { + _error.Flush(); + _errorStream.Seek(0, SeekOrigin.Begin); + using var reader = new StreamReader(_errorStream); + return reader.ReadToEnd(); + } + + public Stream OpenStandardOutput() + { + var writer = PipeWriter.Create(_outStream); + + return writer.AsStream(leaveOpen: true); + } +} \ No newline at end of file