diff --git a/src/GitReleaseManager.Cli/GitReleaseManager.Cli.csproj b/src/GitReleaseManager.Cli/GitReleaseManager.Cli.csproj index 43f1f73b..caa44e9b 100644 --- a/src/GitReleaseManager.Cli/GitReleaseManager.Cli.csproj +++ b/src/GitReleaseManager.Cli/GitReleaseManager.Cli.csproj @@ -1,4 +1,4 @@ - + 8.0 Exe diff --git a/src/GitReleaseManager.Cli/Program.cs b/src/GitReleaseManager.Cli/Program.cs index dc44e89c..c45e7051 100644 --- a/src/GitReleaseManager.Cli/Program.cs +++ b/src/GitReleaseManager.Cli/Program.cs @@ -110,6 +110,11 @@ private static void RegisterServices(BaseSubOptions options) RegisterVcsProvider(vcsOptions, serviceCollection); } + if (options is CreateSubOptions createOptions && !string.IsNullOrEmpty(createOptions.OutputPath)) + { + configuration.Create.AllowUpdateToPublishedRelease = false; + } + serviceCollection = serviceCollection .AddTransient((services) => new TemplateFactory(services.GetRequiredService(), services.GetRequiredService(), TemplateKind.Create)); @@ -197,21 +202,53 @@ private static Task ExecuteCommand(TOptions options) private static void LogOptions(BaseSubOptions options) => Log.Debug("{@Options}", options); + private static void RegisterKeyedVcsProvider(object provider, IServiceCollection serviceCollection) + where TVcsImplementation : class, IVcsProvider + { + static IVcsProvider ResolveService(IServiceProvider service, object key) => service.GetRequiredKeyedService(key); + + provider ??= "null"; + + Log.Debug("Registering {Type} with Service Key {Key}", typeof(IVcsProvider), provider); + + if (typeof(TVcsImplementation) != typeof(NullReleasesProvider)) + { + serviceCollection.AddKeyedSingleton(provider); + } + + serviceCollection + .AddKeyedTransient(provider, ResolveService) + .AddKeyedTransient(provider, ResolveService) + .AddKeyedTransient(provider, ResolveService) + .AddKeyedTransient(provider, ResolveService) + .AddKeyedTransient(provider, ResolveService); + } + private static void RegisterVcsProvider(BaseVcsOptions vcsOptions, IServiceCollection serviceCollection) { Log.Information("Using {Provider} as VCS Provider", vcsOptions.Provider); - if (vcsOptions.Provider == VcsProvider.GitLab) + + serviceCollection.AddKeyedSingleton("null", (service, _) => new NullReleasesProvider(vcsOptions.Provider.ToString(), service.GetRequiredService())); + RegisterKeyedVcsProvider(null, serviceCollection); + RegisterKeyedVcsProvider(VcsProvider.GitHub, serviceCollection); + + serviceCollection + .AddSingleton((_) => new GitLabClient("https://gitlab.com", vcsOptions.Token)); + + serviceCollection + .AddSingleton((_) => new GitHubClient(new ProductHeaderValue("GitReleaseManager")) { Credentials = new Credentials(vcsOptions.Token) }); + + serviceCollection.AddTransient((service) => service.GetKeyedService(vcsOptions.Provider) ?? service.GetRequiredKeyedService("null")); + + if (vcsOptions is CreateSubOptions createOptions && !string.IsNullOrEmpty(createOptions.OutputPath)) { - serviceCollection - .AddSingleton((_) => new GitLabClient("https://gitlab.com", vcsOptions.Token)) - .AddSingleton(); + serviceCollection.AddSingleton((service) => new LocalProvider( + service.GetRequiredService(), + createOptions.OutputPath)); } else { - // default to Github - serviceCollection - .AddSingleton((_) => new GitHubClient(new ProductHeaderValue("GitReleaseManager")) { Credentials = new Credentials(vcsOptions.Token) }) - .AddSingleton(); + serviceCollection.AddTransient((service) => service.GetKeyedService(vcsOptions.Provider) ?? service.GetRequiredKeyedService("null")); } } } diff --git a/src/GitReleaseManager.Core.Tests/VcsServiceTests.cs b/src/GitReleaseManager.Core.Tests/VcsServiceTests.cs index 86306af7..eb885feb 100644 --- a/src/GitReleaseManager.Core.Tests/VcsServiceTests.cs +++ b/src/GitReleaseManager.Core.Tests/VcsServiceTests.cs @@ -47,6 +47,7 @@ public class VcsServiceTests private IReleaseNotesBuilder _releaseNotesBuilder; private ILogger _logger; private IVcsProvider _vcsProvider; + private IReleasesProvider _releaseProvider; private Config _configuration; private VcsService _vcsService; @@ -110,8 +111,9 @@ public void Setup() _releaseNotesBuilder = Substitute.For(); _logger = Substitute.For(); _vcsProvider = Substitute.For(); + _releaseProvider = Substitute.For(); _configuration = new Config(); - _vcsService = new VcsService(_vcsProvider, _logger, _releaseNotesBuilder, _releaseNotesExporter, _configuration); + _vcsService = new VcsService(_vcsProvider, _releaseProvider, _logger, _releaseNotesBuilder, _releaseNotesExporter, _configuration); } [Test] @@ -121,12 +123,12 @@ public async Task Should_Add_Assets() var assetsCount = _assets.Count; - _vcsProvider.GetReleaseAsync(OWNER, REPOSITORY, TAG_NAME) + _releaseProvider.GetReleaseAsync(OWNER, REPOSITORY, TAG_NAME) .Returns(release); await _vcsService.AddAssetsAsync(OWNER, REPOSITORY, TAG_NAME, _assets).ConfigureAwait(false); - await _vcsProvider.Received(1).GetReleaseAsync(OWNER, REPOSITORY, TAG_NAME).ConfigureAwait(false); + await _releaseProvider.Received(1).GetReleaseAsync(OWNER, REPOSITORY, TAG_NAME).ConfigureAwait(false); await _vcsProvider.DidNotReceive().DeleteAssetAsync(OWNER, REPOSITORY, Arg.Any()).ConfigureAwait(false); await _vcsProvider.Received(assetsCount).UploadAssetAsync(release, Arg.Any()).ConfigureAwait(false); @@ -144,12 +146,12 @@ public async Task Should_Add_Assets_With_Deleting_Existing_Assets() var releaseAssetsCount = release.Assets.Count; var assetsCount = _assets.Count; - _vcsProvider.GetReleaseAsync(OWNER, REPOSITORY, TAG_NAME) + _releaseProvider.GetReleaseAsync(OWNER, REPOSITORY, TAG_NAME) .Returns(release); await _vcsService.AddAssetsAsync(OWNER, REPOSITORY, TAG_NAME, _assets).ConfigureAwait(false); - await _vcsProvider.Received(1).GetReleaseAsync(OWNER, REPOSITORY, TAG_NAME).ConfigureAwait(false); + await _releaseProvider.Received(1).GetReleaseAsync(OWNER, REPOSITORY, TAG_NAME).ConfigureAwait(false); await _vcsProvider.Received(releaseAssetsCount).DeleteAssetAsync(OWNER, REPOSITORY, releaseAsset).ConfigureAwait(false); await _vcsProvider.Received(assetsCount).UploadAssetAsync(release, Arg.Any()).ConfigureAwait(false); @@ -166,13 +168,13 @@ public async Task Should_Throw_Exception_On_Adding_Assets_When_Asset_File_Not_Ex _assets[0] = assetFilePath; - _vcsProvider.GetReleaseAsync(OWNER, REPOSITORY, TAG_NAME) + _releaseProvider.GetReleaseAsync(OWNER, REPOSITORY, TAG_NAME) .Returns(release); var ex = await Should.ThrowAsync(() => _vcsService.AddAssetsAsync(OWNER, REPOSITORY, TAG_NAME, _assets)).ConfigureAwait(false); ex.Message.ShouldContain(assetFilePath); - await _vcsProvider.Received(1).GetReleaseAsync(OWNER, REPOSITORY, TAG_NAME).ConfigureAwait(false); + await _releaseProvider.Received(1).GetReleaseAsync(OWNER, REPOSITORY, TAG_NAME).ConfigureAwait(false); await _vcsProvider.DidNotReceive().DeleteAssetAsync(OWNER, REPOSITORY, Arg.Any()).ConfigureAwait(false); await _vcsProvider.DidNotReceive().UploadAssetAsync(release, Arg.Any()).ConfigureAwait(false); } @@ -182,7 +184,7 @@ public async Task Should_Do_Nothing_On_Missing_Assets(IList assets) { await _vcsService.AddAssetsAsync(OWNER, REPOSITORY, TAG_NAME, assets).ConfigureAwait(false); - await _vcsProvider.DidNotReceive().GetReleaseAsync(OWNER, REPOSITORY, TAG_NAME).ConfigureAwait(false); + await _releaseProvider.DidNotReceive().GetReleaseAsync(OWNER, REPOSITORY, TAG_NAME).ConfigureAwait(false); await _vcsProvider.DidNotReceive().DeleteAssetAsync(OWNER, REPOSITORY, Arg.Any()).ConfigureAwait(false); await _vcsProvider.DidNotReceive().UploadAssetAsync(Arg.Any(), Arg.Any()).ConfigureAwait(false); } @@ -306,18 +308,18 @@ public async Task Should_Create_Release_From_Milestone() _releaseNotesBuilder.BuildReleaseNotesAsync(OWNER, REPOSITORY, MILESTONE_TITLE, ReleaseTemplates.DEFAULT_NAME) .Returns(Task.FromResult(RELEASE_NOTES)); - _vcsProvider.GetReleaseAsync(OWNER, REPOSITORY, MILESTONE_TITLE) + _releaseProvider.GetReleaseAsync(OWNER, REPOSITORY, MILESTONE_TITLE) .Returns(Task.FromResult(null)); - _vcsProvider.CreateReleaseAsync(OWNER, REPOSITORY, Arg.Any()) + _releaseProvider.CreateReleaseAsync(OWNER, REPOSITORY, Arg.Any()) .Returns(Task.FromResult(release)); var result = await _vcsService.CreateReleaseFromMilestoneAsync(OWNER, REPOSITORY, MILESTONE_TITLE, MILESTONE_TITLE, null, null, false, null).ConfigureAwait(false); result.ShouldBeSameAs(release); await _releaseNotesBuilder.Received(1).BuildReleaseNotesAsync(OWNER, REPOSITORY, MILESTONE_TITLE, ReleaseTemplates.DEFAULT_NAME).ConfigureAwait(false); - await _vcsProvider.Received(1).GetReleaseAsync(OWNER, REPOSITORY, MILESTONE_TITLE).ConfigureAwait(false); - await _vcsProvider.Received(1).CreateReleaseAsync(OWNER, REPOSITORY, Arg.Is(o => + await _releaseProvider.Received(1).GetReleaseAsync(OWNER, REPOSITORY, MILESTONE_TITLE).ConfigureAwait(false); + await _releaseProvider.Received(1).CreateReleaseAsync(OWNER, REPOSITORY, Arg.Is(o => o.Body == RELEASE_NOTES && o.Name == MILESTONE_TITLE && o.TagName == MILESTONE_TITLE)).ConfigureAwait(false); @@ -336,10 +338,10 @@ public async Task Should_Create_Release_From_Milestone_With_Assets() _releaseNotesBuilder.BuildReleaseNotesAsync(OWNER, REPOSITORY, MILESTONE_TITLE, ReleaseTemplates.DEFAULT_NAME) .Returns(Task.FromResult(RELEASE_NOTES)); - _vcsProvider.GetReleaseAsync(OWNER, REPOSITORY, MILESTONE_TITLE) + _releaseProvider.GetReleaseAsync(OWNER, REPOSITORY, MILESTONE_TITLE) .Returns(Task.FromResult(null)); - _vcsProvider.CreateReleaseAsync(OWNER, REPOSITORY, Arg.Any()) + _releaseProvider.CreateReleaseAsync(OWNER, REPOSITORY, Arg.Any()) .Returns(Task.FromResult(release)); var result = await _vcsService.CreateReleaseFromMilestoneAsync( @@ -355,8 +357,8 @@ public async Task Should_Create_Release_From_Milestone_With_Assets() result.ShouldBeSameAs(release); await _releaseNotesBuilder.Received(1).BuildReleaseNotesAsync(OWNER, REPOSITORY, MILESTONE_TITLE, ReleaseTemplates.DEFAULT_NAME).ConfigureAwait(false); - await _vcsProvider.Received(1).GetReleaseAsync(OWNER, REPOSITORY, MILESTONE_TITLE).ConfigureAwait(false); - await _vcsProvider.Received(1).CreateReleaseAsync(OWNER, REPOSITORY, Arg.Is(o => + await _releaseProvider.Received(1).GetReleaseAsync(OWNER, REPOSITORY, MILESTONE_TITLE).ConfigureAwait(false); + await _releaseProvider.Received(1).CreateReleaseAsync(OWNER, REPOSITORY, Arg.Is(o => o.Body == RELEASE_NOTES && o.Name == MILESTONE_TITLE && o.TagName == MILESTONE_TITLE)).ConfigureAwait(false); @@ -373,18 +375,18 @@ public async Task Should_Create_Release_From_Milestone_Using_Template_File() _releaseNotesBuilder.BuildReleaseNotesAsync(OWNER, REPOSITORY, MILESTONE_TITLE, _releaseNotesTemplate) .Returns(Task.FromResult(RELEASE_NOTES)); - _vcsProvider.GetReleaseAsync(OWNER, REPOSITORY, MILESTONE_TITLE) + _releaseProvider.GetReleaseAsync(OWNER, REPOSITORY, MILESTONE_TITLE) .Returns(Task.FromResult(null)); - _vcsProvider.CreateReleaseAsync(OWNER, REPOSITORY, Arg.Any()) + _releaseProvider.CreateReleaseAsync(OWNER, REPOSITORY, Arg.Any()) .Returns(Task.FromResult(release)); var result = await _vcsService.CreateReleaseFromMilestoneAsync(OWNER, REPOSITORY, MILESTONE_TITLE, MILESTONE_TITLE, null, null, false, _releaseNotesTemplateFilePath).ConfigureAwait(false); result.ShouldBeSameAs(release); await _releaseNotesBuilder.Received(1).BuildReleaseNotesAsync(OWNER, REPOSITORY, MILESTONE_TITLE, _releaseNotesTemplate).ConfigureAwait(false); - await _vcsProvider.Received(1).GetReleaseAsync(OWNER, REPOSITORY, MILESTONE_TITLE).ConfigureAwait(false); - await _vcsProvider.Received(1).CreateReleaseAsync(OWNER, REPOSITORY, Arg.Is(o => + await _releaseProvider.Received(1).GetReleaseAsync(OWNER, REPOSITORY, MILESTONE_TITLE).ConfigureAwait(false); + await _releaseProvider.Received(1).CreateReleaseAsync(OWNER, REPOSITORY, Arg.Is(o => o.Body == RELEASE_NOTES && o.Name == MILESTONE_TITLE && o.TagName == MILESTONE_TITLE)).ConfigureAwait(false); @@ -397,7 +399,7 @@ await _vcsProvider.Received(1).CreateReleaseAsync(OWNER, REPOSITORY, Arg.Is(_notFoundException)); await Should.ThrowAsync(() => _vcsService.CreateReleaseFromMilestoneAsync(OWNER, REPOSITORY, MILESTONE_TITLE, MILESTONE_TITLE, null, null, false, _releaseNotesEmptyTemplateFilePath)).ConfigureAwait(false); @@ -409,7 +411,7 @@ public async Task Should_Throw_Exception_On_Creating_Release_With_Empty_Template [Ignore("This may be handled by the TemplateLoader instead")] public async Task Should_Throw_Exception_On_Creating_Release_With_Invalid_Template_File_Path() { - _vcsProvider.GetReleaseAsync(OWNER, REPOSITORY, TAG_NAME) + _releaseProvider.GetReleaseAsync(OWNER, REPOSITORY, TAG_NAME) .Returns(Task.FromException(_notFoundException)); var fileName = "InvalidReleaseNotesTemplate.txt"; @@ -434,18 +436,18 @@ public async Task Should_Update_Published_Release_On_Creating_Release_From_Miles _releaseNotesBuilder.BuildReleaseNotesAsync(OWNER, REPOSITORY, MILESTONE_TITLE, ReleaseTemplates.DEFAULT_NAME) .Returns(Task.FromResult(RELEASE_NOTES)); - _vcsProvider.GetReleaseAsync(OWNER, REPOSITORY, MILESTONE_TITLE) + _releaseProvider.GetReleaseAsync(OWNER, REPOSITORY, MILESTONE_TITLE) .Returns(Task.FromResult(release)); - _vcsProvider.UpdateReleaseAsync(OWNER, REPOSITORY, release) + _releaseProvider.UpdateReleaseAsync(OWNER, REPOSITORY, release) .Returns(Task.FromResult(new Release())); var result = await _vcsService.CreateReleaseFromMilestoneAsync(OWNER, REPOSITORY, MILESTONE_TITLE, MILESTONE_TITLE, null, null, false, null).ConfigureAwait(false); result.ShouldBeSameAs(release); await _releaseNotesBuilder.Received(1).BuildReleaseNotesAsync(OWNER, REPOSITORY, MILESTONE_TITLE, ReleaseTemplates.DEFAULT_NAME).ConfigureAwait(false); - await _vcsProvider.Received(1).GetReleaseAsync(OWNER, REPOSITORY, MILESTONE_TITLE).ConfigureAwait(false); - await _vcsProvider.Received(1).UpdateReleaseAsync(OWNER, REPOSITORY, release).ConfigureAwait(false); + await _releaseProvider.Received(1).GetReleaseAsync(OWNER, REPOSITORY, MILESTONE_TITLE).ConfigureAwait(false); + await _releaseProvider.Received(1).UpdateReleaseAsync(OWNER, REPOSITORY, release).ConfigureAwait(false); _logger.Received(1).Warning(Arg.Any(), MILESTONE_TITLE); _logger.Received(1).Verbose(Arg.Any(), MILESTONE_TITLE, OWNER, REPOSITORY); @@ -462,14 +464,14 @@ public async Task Should_Throw_Exception_While_Updating_Published_Release_On_Cre _releaseNotesBuilder.BuildReleaseNotesAsync(OWNER, REPOSITORY, MILESTONE_TITLE, ReleaseTemplates.DEFAULT_NAME) .Returns(Task.FromResult(RELEASE_NOTES)); - _vcsProvider.GetReleaseAsync(OWNER, REPOSITORY, MILESTONE_TITLE) + _releaseProvider.GetReleaseAsync(OWNER, REPOSITORY, MILESTONE_TITLE) .Returns(Task.FromResult(release)); var ex = await Should.ThrowAsync(() => _vcsService.CreateReleaseFromMilestoneAsync(OWNER, REPOSITORY, MILESTONE_TITLE, MILESTONE_TITLE, null, null, false, null)).ConfigureAwait(false); ex.Message.ShouldBe($"Release with tag '{MILESTONE_TITLE}' not in draft state, so not updating"); await _releaseNotesBuilder.Received(1).BuildReleaseNotesAsync(OWNER, REPOSITORY, MILESTONE_TITLE, ReleaseTemplates.DEFAULT_NAME).ConfigureAwait(false); - await _vcsProvider.Received(1).GetReleaseAsync(OWNER, REPOSITORY, MILESTONE_TITLE).ConfigureAwait(false); + await _releaseProvider.Received(1).GetReleaseAsync(OWNER, REPOSITORY, MILESTONE_TITLE).ConfigureAwait(false); } [Test] @@ -477,17 +479,17 @@ public async Task Should_Create_Release_From_InputFile() { var release = new Release(); - _vcsProvider.GetReleaseAsync(OWNER, REPOSITORY, MILESTONE_TITLE) + _releaseProvider.GetReleaseAsync(OWNER, REPOSITORY, MILESTONE_TITLE) .Returns(Task.FromResult(null)); - _vcsProvider.CreateReleaseAsync(OWNER, REPOSITORY, Arg.Any()) + _releaseProvider.CreateReleaseAsync(OWNER, REPOSITORY, Arg.Any()) .Returns(Task.FromResult(release)); var result = await _vcsService.CreateReleaseFromInputFileAsync(OWNER, REPOSITORY, MILESTONE_TITLE, _releaseNotesFilePath, null, null, false).ConfigureAwait(false); result.ShouldBeSameAs(release); - await _vcsProvider.Received(1).GetReleaseAsync(OWNER, REPOSITORY, MILESTONE_TITLE).ConfigureAwait(false); - await _vcsProvider.Received(1).CreateReleaseAsync(OWNER, REPOSITORY, Arg.Is(o => + await _releaseProvider.Received(1).GetReleaseAsync(OWNER, REPOSITORY, MILESTONE_TITLE).ConfigureAwait(false); + await _releaseProvider.Received(1).CreateReleaseAsync(OWNER, REPOSITORY, Arg.Is(o => o.Body == RELEASE_NOTES && o.Name == MILESTONE_TITLE && o.TagName == MILESTONE_TITLE)).ConfigureAwait(false); @@ -505,17 +507,17 @@ public async Task Should_Update_Published_Release_On_Creating_Release_From_Input _configuration.Create.AllowUpdateToPublishedRelease = updatePublishedRelease; - _vcsProvider.GetReleaseAsync(OWNER, REPOSITORY, MILESTONE_TITLE) + _releaseProvider.GetReleaseAsync(OWNER, REPOSITORY, MILESTONE_TITLE) .Returns(Task.FromResult(release)); - _vcsProvider.UpdateReleaseAsync(OWNER, REPOSITORY, release) + _releaseProvider.UpdateReleaseAsync(OWNER, REPOSITORY, release) .Returns(Task.FromResult(new Release())); var result = await _vcsService.CreateReleaseFromInputFileAsync(OWNER, REPOSITORY, MILESTONE_TITLE, _releaseNotesFilePath, null, null, false).ConfigureAwait(false); result.ShouldBeSameAs(release); - await _vcsProvider.Received(1).GetReleaseAsync(OWNER, REPOSITORY, MILESTONE_TITLE).ConfigureAwait(false); - await _vcsProvider.Received(1).UpdateReleaseAsync(OWNER, REPOSITORY, release).ConfigureAwait(false); + await _releaseProvider.Received(1).GetReleaseAsync(OWNER, REPOSITORY, MILESTONE_TITLE).ConfigureAwait(false); + await _releaseProvider.Received(1).UpdateReleaseAsync(OWNER, REPOSITORY, release).ConfigureAwait(false); _logger.Received(1).Warning(Arg.Any(), MILESTONE_TITLE); _logger.Received(1).Verbose(Arg.Any(), MILESTONE_TITLE, OWNER, REPOSITORY); @@ -529,13 +531,13 @@ public async Task Should_Throw_Exception_While_Updating_Published_Release_On_Cre _configuration.Create.AllowUpdateToPublishedRelease = false; - _vcsProvider.GetReleaseAsync(OWNER, REPOSITORY, MILESTONE_TITLE) + _releaseProvider.GetReleaseAsync(OWNER, REPOSITORY, MILESTONE_TITLE) .Returns(Task.FromResult(release)); var ex = await Should.ThrowAsync(() => _vcsService.CreateReleaseFromInputFileAsync(OWNER, REPOSITORY, MILESTONE_TITLE, _releaseNotesFilePath, null, null, false)).ConfigureAwait(false); ex.Message.ShouldBe($"Release with tag '{MILESTONE_TITLE}' not in draft state, so not updating"); - await _vcsProvider.Received(1).GetReleaseAsync(OWNER, REPOSITORY, MILESTONE_TITLE).ConfigureAwait(false); + await _releaseProvider.Received(1).GetReleaseAsync(OWNER, REPOSITORY, MILESTONE_TITLE).ConfigureAwait(false); } [Test] @@ -558,16 +560,16 @@ public async Task Should_Delete_Draft_Release() Draft = true, }; - _vcsProvider.GetReleaseAsync(OWNER, REPOSITORY, TAG_NAME) + _releaseProvider.GetReleaseAsync(OWNER, REPOSITORY, TAG_NAME) .Returns(Task.FromResult(release)); - _vcsProvider.DeleteReleaseAsync(OWNER, REPOSITORY, release) + _releaseProvider.DeleteReleaseAsync(OWNER, REPOSITORY, release) .Returns(Task.CompletedTask); await _vcsService.DiscardReleaseAsync(OWNER, REPOSITORY, TAG_NAME).ConfigureAwait(false); - await _vcsProvider.Received(1).GetReleaseAsync(OWNER, REPOSITORY, TAG_NAME).ConfigureAwait(false); - await _vcsProvider.Received(1).DeleteReleaseAsync(OWNER, REPOSITORY, release).ConfigureAwait(false); + await _releaseProvider.Received(1).GetReleaseAsync(OWNER, REPOSITORY, TAG_NAME).ConfigureAwait(false); + await _releaseProvider.Received(1).DeleteReleaseAsync(OWNER, REPOSITORY, release).ConfigureAwait(false); } [Test] @@ -575,26 +577,26 @@ public async Task Should_Not_Delete_Published_Release() { var release = new Release { Id = 1 }; - _vcsProvider.GetReleaseAsync(OWNER, REPOSITORY, TAG_NAME) + _releaseProvider.GetReleaseAsync(OWNER, REPOSITORY, TAG_NAME) .Returns(Task.FromResult(release)); await _vcsService.DiscardReleaseAsync(OWNER, REPOSITORY, TAG_NAME).ConfigureAwait(false); - await _vcsProvider.Received(1).GetReleaseAsync(OWNER, REPOSITORY, TAG_NAME).ConfigureAwait(false); - await _vcsProvider.DidNotReceive().DeleteReleaseAsync(OWNER, REPOSITORY, release).ConfigureAwait(false); + await _releaseProvider.Received(1).GetReleaseAsync(OWNER, REPOSITORY, TAG_NAME).ConfigureAwait(false); + await _releaseProvider.DidNotReceive().DeleteReleaseAsync(OWNER, REPOSITORY, release).ConfigureAwait(false); _logger.Received(1).Warning(Arg.Any(), TAG_NAME); } [Test] public async Task Should_Log_An_Warning_On_Deleting_Release_For_Non_Existing_Tag() { - _vcsProvider.GetReleaseAsync(OWNER, REPOSITORY, TAG_NAME) + _releaseProvider.GetReleaseAsync(OWNER, REPOSITORY, TAG_NAME) .Returns(Task.FromException(_notFoundException)); await _vcsService.DiscardReleaseAsync(OWNER, REPOSITORY, TAG_NAME).ConfigureAwait(false); - await _vcsProvider.Received().GetReleaseAsync(OWNER, REPOSITORY, TAG_NAME).ConfigureAwait(false); - await _vcsProvider.DidNotReceiveWithAnyArgs().DeleteReleaseAsync(OWNER, REPOSITORY, default).ConfigureAwait(false); + await _releaseProvider.Received().GetReleaseAsync(OWNER, REPOSITORY, TAG_NAME).ConfigureAwait(false); + await _releaseProvider.DidNotReceiveWithAnyArgs().DeleteReleaseAsync(OWNER, REPOSITORY, default).ConfigureAwait(false); _logger.Received(1).Warning(UNABLE_TO_FOUND_RELEASE_MESSAGE, TAG_NAME, OWNER, REPOSITORY); } @@ -603,16 +605,16 @@ public async Task Should_Publish_Release() { var release = new Release { Id = 1 }; - _vcsProvider.GetReleaseAsync(OWNER, REPOSITORY, TAG_NAME) + _releaseProvider.GetReleaseAsync(OWNER, REPOSITORY, TAG_NAME) .Returns(Task.FromResult(release)); - _vcsProvider.PublishReleaseAsync(OWNER, REPOSITORY, TAG_NAME, release) + _releaseProvider.PublishReleaseAsync(OWNER, REPOSITORY, TAG_NAME, release) .Returns(Task.CompletedTask); await _vcsService.PublishReleaseAsync(OWNER, REPOSITORY, TAG_NAME).ConfigureAwait(false); - await _vcsProvider.Received(1).GetReleaseAsync(OWNER, REPOSITORY, TAG_NAME).ConfigureAwait(false); - await _vcsProvider.Received(1).PublishReleaseAsync(OWNER, REPOSITORY, TAG_NAME, release).ConfigureAwait(false); + await _releaseProvider.Received(1).GetReleaseAsync(OWNER, REPOSITORY, TAG_NAME).ConfigureAwait(false); + await _releaseProvider.Received(1).PublishReleaseAsync(OWNER, REPOSITORY, TAG_NAME, release).ConfigureAwait(false); _logger.Received(1).Verbose(Arg.Any(), TAG_NAME, OWNER, REPOSITORY); _logger.Received(1).Debug(Arg.Any(), Arg.Any()); } @@ -620,13 +622,13 @@ public async Task Should_Publish_Release() [Test] public async Task Should_Log_An_Warning_On_Publishing_Release_For_Non_Existing_Tag() { - _vcsProvider.GetReleaseAsync(OWNER, REPOSITORY, TAG_NAME) + _releaseProvider.GetReleaseAsync(OWNER, REPOSITORY, TAG_NAME) .Returns(Task.FromException(_notFoundException)); await _vcsService.PublishReleaseAsync(OWNER, REPOSITORY, TAG_NAME).ConfigureAwait(false); - await _vcsProvider.Received().GetReleaseAsync(OWNER, REPOSITORY, TAG_NAME).ConfigureAwait(false); - await _vcsProvider.DidNotReceiveWithAnyArgs().PublishReleaseAsync(OWNER, REPOSITORY, TAG_NAME, default).ConfigureAwait(false); + await _releaseProvider.Received().GetReleaseAsync(OWNER, REPOSITORY, TAG_NAME).ConfigureAwait(false); + await _releaseProvider.DidNotReceiveWithAnyArgs().PublishReleaseAsync(OWNER, REPOSITORY, TAG_NAME, default).ConfigureAwait(false); _logger.Received(1).Warning(Arg.Any(), TAG_NAME, OWNER, REPOSITORY); } @@ -636,7 +638,7 @@ public async Task Should_Get_Release_Notes() var releases = Enumerable.Empty(); var releaseNotes = "Release Notes"; - _vcsProvider.GetReleasesAsync(OWNER, REPOSITORY, SKIP_PRERELEASES) + _releaseProvider.GetReleasesAsync(OWNER, REPOSITORY, SKIP_PRERELEASES) .Returns(Task.FromResult(releases)); _releaseNotesExporter.ExportReleaseNotes(Arg.Any>()) @@ -645,8 +647,8 @@ public async Task Should_Get_Release_Notes() var result = await _vcsService.ExportReleasesAsync(OWNER, REPOSITORY, null, SKIP_PRERELEASES).ConfigureAwait(false); result.ShouldBeSameAs(releaseNotes); - await _vcsProvider.DidNotReceive().GetReleaseAsync(Arg.Any(), Arg.Any(), Arg.Any()).ConfigureAwait(false); - await _vcsProvider.Received(1).GetReleasesAsync(OWNER, REPOSITORY, SKIP_PRERELEASES).ConfigureAwait(false); + await _releaseProvider.DidNotReceive().GetReleaseAsync(Arg.Any(), Arg.Any(), Arg.Any()).ConfigureAwait(false); + await _releaseProvider.Received(1).GetReleasesAsync(OWNER, REPOSITORY, SKIP_PRERELEASES).ConfigureAwait(false); _logger.Received(1).Verbose(Arg.Any(), OWNER, REPOSITORY); _releaseNotesExporter.Received(1).ExportReleaseNotes(Arg.Any>()); } @@ -656,7 +658,7 @@ public async Task Should_Get_Release_Notes_For_Tag() { var release = new Release(); - _vcsProvider.GetReleaseAsync(OWNER, REPOSITORY, TAG_NAME) + _releaseProvider.GetReleaseAsync(OWNER, REPOSITORY, TAG_NAME) .Returns(Task.FromResult(release)); _releaseNotesExporter.ExportReleaseNotes(Arg.Any>()) @@ -665,8 +667,8 @@ public async Task Should_Get_Release_Notes_For_Tag() var result = await _vcsService.ExportReleasesAsync(OWNER, REPOSITORY, TAG_NAME, SKIP_PRERELEASES).ConfigureAwait(false); result.ShouldBeSameAs(RELEASE_NOTES); - await _vcsProvider.Received(1).GetReleaseAsync(OWNER, REPOSITORY, TAG_NAME).ConfigureAwait(false); - await _vcsProvider.DidNotReceive().GetReleasesAsync(Arg.Any(), Arg.Any(), Arg.Any()).ConfigureAwait(false); + await _releaseProvider.Received(1).GetReleaseAsync(OWNER, REPOSITORY, TAG_NAME).ConfigureAwait(false); + await _releaseProvider.DidNotReceive().GetReleasesAsync(Arg.Any(), Arg.Any(), Arg.Any()).ConfigureAwait(false); _logger.Received(1).Verbose(Arg.Any(), OWNER, REPOSITORY, TAG_NAME); _releaseNotesExporter.Received(1).ExportReleaseNotes(Arg.Any>()); } @@ -674,7 +676,7 @@ public async Task Should_Get_Release_Notes_For_Tag() [Test] public async Task Should_Get_Default_Release_Notes_For_Non_Existent_Tag() { - _vcsProvider.GetReleaseAsync(OWNER, REPOSITORY, TAG_NAME) + _releaseProvider.GetReleaseAsync(OWNER, REPOSITORY, TAG_NAME) .Returns(Task.FromException(_notFoundException)); _releaseNotesExporter.ExportReleaseNotes(Arg.Any>()) @@ -683,7 +685,7 @@ public async Task Should_Get_Default_Release_Notes_For_Non_Existent_Tag() var result = await _vcsService.ExportReleasesAsync(OWNER, REPOSITORY, TAG_NAME, SKIP_PRERELEASES).ConfigureAwait(false); result.ShouldBeSameAs(RELEASE_NOTES); - await _vcsProvider.Received(1).GetReleaseAsync(OWNER, REPOSITORY, TAG_NAME).ConfigureAwait(false); + await _releaseProvider.Received(1).GetReleaseAsync(OWNER, REPOSITORY, TAG_NAME).ConfigureAwait(false); _logger.Received(1).Warning(UNABLE_TO_FOUND_RELEASE_MESSAGE, TAG_NAME, OWNER, REPOSITORY); _releaseNotesExporter.Received(1).ExportReleaseNotes(Arg.Any>()); } diff --git a/src/GitReleaseManager.Core/GitReleaseManager.Core.csproj b/src/GitReleaseManager.Core/GitReleaseManager.Core.csproj index bca953b3..6caf0ced 100644 --- a/src/GitReleaseManager.Core/GitReleaseManager.Core.csproj +++ b/src/GitReleaseManager.Core/GitReleaseManager.Core.csproj @@ -4,6 +4,8 @@ 8.0 + + net6.0;net7.0 GitReleaseManager.Core Create release notes in markdown given a milestone diff --git a/src/GitReleaseManager.Core/Helpers/FileSystem.cs b/src/GitReleaseManager.Core/Helpers/FileSystem.cs index c6f53827..996fc5c5 100644 --- a/src/GitReleaseManager.Core/Helpers/FileSystem.cs +++ b/src/GitReleaseManager.Core/Helpers/FileSystem.cs @@ -19,6 +19,21 @@ public void Copy(string @source, string destination, bool overwrite) File.Copy(@source, destination, overwrite); } + public void CreateDirectory(string path) + { + // It is safe to call CreateDirectory, if the directory + // already exists these are no-op. + + if (string.IsNullOrEmpty(path)) + { + Directory.CreateDirectory(Environment.CurrentDirectory); + } + else + { + Directory.CreateDirectory(path); + } + } + public void Move(string @source, string destination) { File.Move(@source, destination); @@ -59,9 +74,19 @@ public IEnumerable DirectoryGetFiles(string directory, string searchPatt return Directory.GetFiles(directory, searchPattern, searchOption); } + public Stream OpenRead(string path) + { + return File.Open(path, FileMode.Open, FileAccess.Read, FileShare.Read); + } + public Stream OpenWrite(string path) { - return File.OpenWrite(path); + return OpenWrite(path, overwrite: false); + } + + public Stream OpenWrite(string path, bool overwrite) + { + return File.Open(path, overwrite ? FileMode.Create : FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read); } } } \ No newline at end of file diff --git a/src/GitReleaseManager.Core/Helpers/IFileSystem.cs b/src/GitReleaseManager.Core/Helpers/IFileSystem.cs index eb85b7fc..12fdb938 100644 --- a/src/GitReleaseManager.Core/Helpers/IFileSystem.cs +++ b/src/GitReleaseManager.Core/Helpers/IFileSystem.cs @@ -7,6 +7,8 @@ public interface IFileSystem { void Copy(string source, string destination, bool overwrite); + void CreateDirectory(string path); + void Move(string source, string destination); bool Exists(string file); @@ -21,6 +23,9 @@ public interface IFileSystem IEnumerable DirectoryGetFiles(string directory, string searchPattern, SearchOption searchOption); + Stream OpenRead(string path); + Stream OpenWrite(string path); + Stream OpenWrite(string path, bool overwrite); } } \ No newline at end of file diff --git a/src/GitReleaseManager.Core/Options/CreateSubOptions.cs b/src/GitReleaseManager.Core/Options/CreateSubOptions.cs index 34d0d7c5..85721e9d 100644 --- a/src/GitReleaseManager.Core/Options/CreateSubOptions.cs +++ b/src/GitReleaseManager.Core/Options/CreateSubOptions.cs @@ -29,5 +29,8 @@ public class CreateSubOptions : BaseVcsOptions [Option("allowEmpty", Required = false, HelpText = "Allow the creation of an empty set of release notes. In this mode, milestone and input file path will be ignored.")] public bool AllowEmpty { get; set; } + + [Option("output", Required = false, HelpText = "The path to a local file location where the release notes will be created, instead of creating in remotely on the specified provider.")] + public string OutputPath { get; set; } } } \ No newline at end of file diff --git a/src/GitReleaseManager.Core/Provider/GitHubProvider.cs b/src/GitReleaseManager.Core/Provider/GitHubProvider.cs index 68d19c4a..f14be17d 100644 --- a/src/GitReleaseManager.Core/Provider/GitHubProvider.cs +++ b/src/GitReleaseManager.Core/Provider/GitHubProvider.cs @@ -35,6 +35,8 @@ public GitHubProvider(IGitHubClient gitHubClient, IMapper mapper) _mapper = mapper; } + public string Name => "GitHub"; + public Task DeleteAssetAsync(string owner, string repository, ReleaseAsset asset) { return ExecuteAsync(async () => diff --git a/src/GitReleaseManager.Core/Provider/GitLabProvider.cs b/src/GitReleaseManager.Core/Provider/GitLabProvider.cs index c2371c72..0349bf70 100644 --- a/src/GitReleaseManager.Core/Provider/GitLabProvider.cs +++ b/src/GitReleaseManager.Core/Provider/GitLabProvider.cs @@ -43,6 +43,8 @@ public GitLabProvider(IGitLabClient gitLabClient, IMapper mapper, ILogger logger _logger = logger; } + public string Name => "GitLab"; + public Task DeleteAssetAsync(string owner, string repository, ReleaseAsset asset) { // TODO: This is a discussion here: diff --git a/src/GitReleaseManager.Core/Provider/IAssetsProvider.cs b/src/GitReleaseManager.Core/Provider/IAssetsProvider.cs new file mode 100644 index 00000000..25bacc39 --- /dev/null +++ b/src/GitReleaseManager.Core/Provider/IAssetsProvider.cs @@ -0,0 +1,14 @@ +namespace GitReleaseManager.Core.Provider +{ + using System.Threading.Tasks; + using GitReleaseManager.Core.Model; + + public interface IAssetsProvider + { + bool SupportsAssets { get; } + + Task DeleteAssetAsync(string owner, string repository, ReleaseAsset asset); + + Task UploadAssetAsync(Release release, ReleaseAssetUpload releaseAssetUpload); + } +} diff --git a/src/GitReleaseManager.Core/Provider/ICommitsProvider.cs b/src/GitReleaseManager.Core/Provider/ICommitsProvider.cs new file mode 100644 index 00000000..42a2cd57 --- /dev/null +++ b/src/GitReleaseManager.Core/Provider/ICommitsProvider.cs @@ -0,0 +1,13 @@ +namespace GitReleaseManager.Core.Provider +{ + using System.Threading.Tasks; + + public interface ICommitsProvider + { + bool SupportsCommits { get; } + + Task GetCommitsCountAsync(string owner, string repository, string baseCommit, string headCommit); + + string GetCommitsUrl(string owner, string repository, string head, string baseCommit = null); + } +} \ No newline at end of file diff --git a/src/GitReleaseManager.Core/Provider/IIssuesProvider.cs b/src/GitReleaseManager.Core/Provider/IIssuesProvider.cs new file mode 100644 index 00000000..9d91cccd --- /dev/null +++ b/src/GitReleaseManager.Core/Provider/IIssuesProvider.cs @@ -0,0 +1,20 @@ +namespace GitReleaseManager.Core.Provider +{ + using System.Collections.Generic; + using System.Threading.Tasks; + using GitReleaseManager.Core.Model; + + public interface IIssuesProvider + { + bool SupportIssues { get; } + bool SupportIssueComments { get; } + + Task CreateIssueCommentAsync(string owner, string repository, Issue issue, string comment); + + Task> GetIssueCommentsAsync(string owner, string repository, Issue issue); + + Task> GetIssuesAsync(string owner, string repository, Milestone milstone, ItemStateFilter itemStateFilter = ItemStateFilter.All); + + string GetIssueType(Issue issue); + } +} \ No newline at end of file diff --git a/src/GitReleaseManager.Core/Provider/ILabelsProvider.cs b/src/GitReleaseManager.Core/Provider/ILabelsProvider.cs new file mode 100644 index 00000000..b7c00a59 --- /dev/null +++ b/src/GitReleaseManager.Core/Provider/ILabelsProvider.cs @@ -0,0 +1,17 @@ +namespace GitReleaseManager.Core.Provider +{ + using System.Collections.Generic; + using System.Threading.Tasks; + using GitReleaseManager.Core.Model; + + public interface ILabelsProvider + { + bool SupportsLabels { get; } + + Task CreateLabelAsync(string owner, string repository, Label label); + + Task DeleteLabelAsync(string owner, string repository, Label label); + + Task> GetLabelsAsync(string owner, string repository); + } +} \ No newline at end of file diff --git a/src/GitReleaseManager.Core/Provider/IMilestonesProvider.cs b/src/GitReleaseManager.Core/Provider/IMilestonesProvider.cs new file mode 100644 index 00000000..de569249 --- /dev/null +++ b/src/GitReleaseManager.Core/Provider/IMilestonesProvider.cs @@ -0,0 +1,19 @@ +namespace GitReleaseManager.Core.Provider +{ + using System.Collections.Generic; + using System.Threading.Tasks; + using GitReleaseManager.Core.Model; + + public interface IMilestonesProvider + { + bool SupportMilestones { get; } + + Task GetMilestoneAsync(string owner, string repository, string milestoneTitle, ItemStateFilter itemStateFilter = ItemStateFilter.All); + + Task> GetMilestonesAsync(string owner, string repository, ItemStateFilter itemStateFilter = ItemStateFilter.All); + + Task SetMilestoneStateAsync(string owner, string repository, Milestone milestone, ItemState itemState); + + string GetMilestoneQueryString(); + } +} diff --git a/src/GitReleaseManager.Core/Provider/IRateLimitProvider.cs b/src/GitReleaseManager.Core/Provider/IRateLimitProvider.cs new file mode 100644 index 00000000..2900ec14 --- /dev/null +++ b/src/GitReleaseManager.Core/Provider/IRateLimitProvider.cs @@ -0,0 +1,9 @@ +namespace GitReleaseManager.Core.Provider +{ + using GitReleaseManager.Core.Model; + + public interface IRateLimitProvider + { + RateLimit GetRateLimit(); + } +} diff --git a/src/GitReleaseManager.Core/Provider/IReleasesProvider.cs b/src/GitReleaseManager.Core/Provider/IReleasesProvider.cs new file mode 100644 index 00000000..e8ca3171 --- /dev/null +++ b/src/GitReleaseManager.Core/Provider/IReleasesProvider.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using GitReleaseManager.Core.Model; + +namespace GitReleaseManager.Core.Provider +{ + public interface IReleasesProvider + { + bool SupportReleases { get; } + + Task CreateReleaseAsync(string owner, string repository, Release release); + + Task DeleteReleaseAsync(string owner, string repository, Release release); + + Task GetReleaseAsync(string owner, string repository, string tagName); + + Task> GetReleasesAsync(string owner, string repository, bool skipPrereleases); + + Task PublishReleaseAsync(string owner, string repository, string tagName, Release release); + + Task UpdateReleaseAsync(string owner, string repository, Release release); + } +} \ No newline at end of file diff --git a/src/GitReleaseManager.Core/Provider/IVcsProvider.cs b/src/GitReleaseManager.Core/Provider/IVcsProvider.cs index a7bf82a8..7984d4a2 100644 --- a/src/GitReleaseManager.Core/Provider/IVcsProvider.cs +++ b/src/GitReleaseManager.Core/Provider/IVcsProvider.cs @@ -4,50 +4,7 @@ namespace GitReleaseManager.Core.Provider { - public interface IVcsProvider + public interface IVcsProvider : IAssetsProvider, ICommitsProvider, IIssuesProvider, IMilestonesProvider, IRateLimitProvider, IReleasesProvider { - Task DeleteAssetAsync(string owner, string repository, ReleaseAsset asset); - - Task UploadAssetAsync(Release release, ReleaseAssetUpload releaseAssetUpload); - - Task GetCommitsCountAsync(string owner, string repository, string @base, string head); - - string GetCommitsUrl(string owner, string repository, string head, string @base = null); - - Task CreateIssueCommentAsync(string owner, string repository, Issue issue, string comment); - - Task> GetIssuesAsync(string owner, string repository, Milestone milstone, ItemStateFilter itemStateFilter = ItemStateFilter.All); - - Task> GetIssueCommentsAsync(string owner, string repository, Issue issue); - - Task CreateLabelAsync(string owner, string repository, Label label); - - Task DeleteLabelAsync(string owner, string repository, Label label); - - Task> GetLabelsAsync(string owner, string repository); - - Task GetMilestoneAsync(string owner, string repository, string milestoneTitle, ItemStateFilter itemStateFilter = ItemStateFilter.All); - - Task> GetMilestonesAsync(string owner, string repository, ItemStateFilter itemStateFilter = ItemStateFilter.All); - - Task SetMilestoneStateAsync(string owner, string repository, Milestone milestone, ItemState itemState); - - Task CreateReleaseAsync(string owner, string repository, Release release); - - Task DeleteReleaseAsync(string owner, string repository, Release release); - - Task GetReleaseAsync(string owner, string repository, string tagName); - - Task> GetReleasesAsync(string owner, string repository, bool skipPrereleases); - - Task PublishReleaseAsync(string owner, string repository, string tagName, Release release); - - Task UpdateReleaseAsync(string owner, string repository, Release release); - - RateLimit GetRateLimit(); - - string GetMilestoneQueryString(); - - string GetIssueType(Issue issue); } } \ No newline at end of file diff --git a/src/GitReleaseManager.Core/Provider/LocalProvider.cs b/src/GitReleaseManager.Core/Provider/LocalProvider.cs new file mode 100644 index 00000000..7c6ff8db --- /dev/null +++ b/src/GitReleaseManager.Core/Provider/LocalProvider.cs @@ -0,0 +1,149 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using GitReleaseManager.Core.Helpers; +using GitReleaseManager.Core.Model; + +namespace GitReleaseManager.Core.Provider +{ + public partial class LocalProvider : IReleasesProvider + { + private const string NAME_AND_TAG_REGEX = @"^#+\s*(?[^\(\r\n]+)(?:\s*\((?[^\)\r\n]+))?"; + private readonly IFileSystem _fileSystem; + private readonly string _outputPath; + + public LocalProvider(IFileSystem fileSystem, string outputPath) + { + _fileSystem = fileSystem; + _outputPath = outputPath; + } + + public bool SupportReleases => true; + + public async Task CreateReleaseAsync(string owner, string repository, Release release) + { + ArgumentNullException.ThrowIfNull(release); + + string directory = Path.GetDirectoryName(_outputPath) ?? Environment.CurrentDirectory; + _fileSystem.CreateDirectory(directory); + +#pragma warning disable CA2007 // Consider calling ConfigureAwait on the awaited task + await using (var stream = _fileSystem.OpenWrite(_outputPath, overwrite: true)) + await using (StreamWriter writer = new StreamWriter(stream, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true))) +#pragma warning restore CA2007 // Consider calling ConfigureAwait on the awaited task + { + await writer.WriteAsync("# ").ConfigureAwait(false); + await writer.WriteAsync(release.Name ?? release.TagName).ConfigureAwait(false); + + if (release.Name != null && release.Name != release.TagName) + { + await writer.WriteAsync(" (").ConfigureAwait(false); + await writer.WriteAsync(release.TagName).ConfigureAwait(false); + await writer.WriteLineAsync(")").ConfigureAwait(false); + } + else + { + await writer.WriteLineAsync().ConfigureAwait(false); + } + + await writer.WriteLineAsync().ConfigureAwait(false); + + await writer.WriteLineAsync(release.Body).ConfigureAwait(false); + + await writer.FlushAsync().ConfigureAwait(false); + } + + release.CreatedAt = DateTime.UtcNow; + release.HtmlUrl = _outputPath; + + return release; + } + + public Task DeleteReleaseAsync(string owner, string repository, Release release) + { + _fileSystem.Delete(_outputPath); + + return Task.CompletedTask; + } + + public async Task GetReleaseAsync(string owner, string repository, string tagName) + { + Release release = new Release(); + await using (var stream = _fileSystem.OpenRead(_outputPath)) + using (StreamReader reader = new StreamReader(stream, Encoding.UTF8)) + { + var line = await reader.ReadLineAsync(); + var titleMatch = NameAndTagNameRegex().Match(line); + + if (titleMatch.Success) + { + release.Name = titleMatch.Groups["NAME"].Value; + + if (titleMatch.Groups["TAG_NAME"].Success) + { + release.TagName = titleMatch.Groups["TAG_NAME"].Value; + } + } + + line = await reader.ReadLineAsync(); + + while (string.IsNullOrEmpty(line)) + { + line = await reader.ReadLineAsync(); + } + + var body = new StringBuilder(line).AppendLine(); + var block = await reader.ReadToEndAsync(); + + if (block.Length > 0) + { + body.Append(block); + } + + if (release.Name.Length == 0 && body.Length == 0) + { + return null; + } + else + { + release.Body = body.ToString(); + release.Draft = true; + release.HtmlUrl = _outputPath; + release.Prerelease = Regex.IsMatch(@"^\d\.+-[a-z]", release.Name, RegexOptions.IgnoreCase | RegexOptions.Compiled); + } + } + + return release; + } + + public Task> GetReleasesAsync(string owner, string repository, bool skipPrereleases) + { + return Task.FromResult>(null); + } + + public Task PublishReleaseAsync(string owner, string repository, string tagName, Release release) + { + // No-op + return Task.CompletedTask; + } + + public async Task UpdateReleaseAsync(string owner, string repository, Release release) + { + await DeleteReleaseAsync(owner, repository, release).ConfigureAwait(false); + await CreateReleaseAsync(owner, repository, release).ConfigureAwait(false); + } + +#if NET7_0_OR_GREATER && USE_GENERATED_REGEX + [GeneratedRegex(NAME_AND_TAG_REGEX, RegexOptions.IgnoreCase)] + private static partial Regex NameAndTagNameRegex(); +#else + private static Regex NameAndTagNameRegex() + { + return new Regex(NAME_AND_TAG_REGEX, RegexOptions.IgnoreCase | RegexOptions.Compiled); + } +#endif + } +} diff --git a/src/GitReleaseManager.Core/Provider/NullReleasesProvider.cs b/src/GitReleaseManager.Core/Provider/NullReleasesProvider.cs new file mode 100644 index 00000000..bd353ebf --- /dev/null +++ b/src/GitReleaseManager.Core/Provider/NullReleasesProvider.cs @@ -0,0 +1,167 @@ +namespace GitReleaseManager.Core.Provider +{ + using System.Collections.Generic; + using System.Linq; + using System.Threading.Tasks; + using GitReleaseManager.Core.Model; + using Serilog; + + public class NullReleasesProvider : IVcsProvider + { + private readonly string _vcsProvider; + private readonly ILogger _logger; + + public NullReleasesProvider(string vcsProvider, ILogger logger) + { + _vcsProvider = vcsProvider; + _logger = logger; + } + + public bool SupportsAssets => false; + + public bool SupportsCommits => false; + + public bool SupportIssues => false; + + public bool SupportIssueComments => false; + + public bool SupportMilestones => false; + + public bool SupportReleases => false; + + public Task CreateIssueCommentAsync(string owner, string repository, Issue issue, string comment) + { + _logger.Warning("The provider '{Provider}' do not support creating issue comments!", _vcsProvider); + return Task.CompletedTask; + } + + public Task CreateLabelAsync(string owner, string repository, Label label) + { + _logger.Warning("The provider '{Provider}' do not support creating labels!", _vcsProvider); + return Task.CompletedTask; + } + + public Task CreateReleaseAsync(string owner, string repository, Release release) + { + _logger.Warning("The provider '{Provider}' do not support creating releases!", _vcsProvider); + return Task.FromResult(null); + } + + public Task DeleteAssetAsync(string owner, string repository, ReleaseAsset asset) + { + _logger.Warning("The provider '{Provider}' do not support deleting assets!", _vcsProvider); + return Task.CompletedTask; + } + + public Task DeleteLabelAsync(string owner, string repository, Label label) + { + _logger.Warning("The provider '{Provider}' do not support deleting labels!", _vcsProvider); + return Task.CompletedTask; + } + + public Task DeleteReleaseAsync(string owner, string repository, Release release) + { + _logger.Warning("The provider '{Provider}' do not support releases labels!", _vcsProvider); + return Task.CompletedTask; + } + + public Task GetCommitsCountAsync(string owner, string repository, string @base, string head) + { + _logger.Warning("The provider '{Provider}' do not support acquiring commits!", _vcsProvider); + return Task.FromResult(0); + } + + public string GetCommitsUrl(string owner, string repository, string head, string @base = null) + { + _logger.Warning("The provider '{Provider}' do not support acquiring commits!", _vcsProvider); + return null; + } + + public Task> GetIssueCommentsAsync(string owner, string repository, Issue issue) + { + _logger.Warning("The provider '{Provider}' do not support acquiring issue comments!", _vcsProvider); + return Task.FromResult(Enumerable.Empty()); + } + + public Task> GetIssuesAsync(string owner, string repository, Milestone milstone, ItemStateFilter itemStateFilter = ItemStateFilter.All) + { + _logger.Warning("The provider '{Provider}' do not support acquiring issues!", _vcsProvider); + return Task.FromResult(Enumerable.Empty()); + } + + public string GetIssueType(Issue issue) + { + _logger.Warning("The provider '{Provider}' do not support acquiring issues!", _vcsProvider); + return null; + } + + public Task> GetLabelsAsync(string owner, string repository) + { + _logger.Warning("The provider '{Provider}' do not support acquiring labels!", _vcsProvider); + return Task.FromResult(Enumerable.Empty