From 6a7a78a59da311950b60e3cba82e90fe58c08807 Mon Sep 17 00:00:00 2001 From: Gary Ewan Park Date: Wed, 30 Aug 2023 10:51:35 +0100 Subject: [PATCH] (#146) Provide initial support for GitLab Need to put some words here... --- src/GitReleaseManager.Cli/Program.cs | 27 +- .../Provider/GitHubProviderTests.cs | 31 +- .../VcsServiceTests.cs | 25 +- .../Commands/AddAssetsCommand.cs | 8 + .../Commands/ExportCommand.cs | 10 +- .../Commands/LabelCommand.cs | 8 + .../Extensions/MilestoneExtensions.cs | 26 +- .../GitReleaseManager.Core.csproj | 1 + .../MappingProfiles/GitHubProfile.cs | 2 + .../MappingProfiles/GitLabProfile.cs | 35 ++ src/GitReleaseManager.Core/Model/Milestone.cs | 4 +- .../Model/VcsProvider.cs | 8 + .../Options/BaseVcsSubOptions.cs | 4 + .../Provider/GitHubProvider.cs | 13 +- .../Provider/GitLabProvider.cs | 337 ++++++++++++++++++ .../Provider/IVcsProvider.cs | 6 +- .../ReleaseNotes/ReleaseNotesBuilder.cs | 5 +- .../Templates/default/release-info.sbn | 4 +- .../Templates/empty/create/footer.sbn | 0 .../Templates/empty/index.sbn | 1 + src/GitReleaseManager.Core/VcsService.cs | 29 +- .../GitHubProviderIntegrationTests.cs | 4 +- .../ReleaseNotesBuilderTests.cs | 9 +- .../GitReleaseManager.Tool.csproj | 1 + 24 files changed, 545 insertions(+), 53 deletions(-) create mode 100644 src/GitReleaseManager.Core/MappingProfiles/GitLabProfile.cs create mode 100644 src/GitReleaseManager.Core/Model/VcsProvider.cs create mode 100644 src/GitReleaseManager.Core/Provider/GitLabProvider.cs create mode 100644 src/GitReleaseManager.Core/Templates/empty/create/footer.sbn create mode 100644 src/GitReleaseManager.Core/Templates/empty/index.sbn diff --git a/src/GitReleaseManager.Cli/Program.cs b/src/GitReleaseManager.Cli/Program.cs index fdc36e1a..f89fc72d 100644 --- a/src/GitReleaseManager.Cli/Program.cs +++ b/src/GitReleaseManager.Cli/Program.cs @@ -8,11 +8,13 @@ using GitReleaseManager.Core.Commands; using GitReleaseManager.Core.Configuration; using GitReleaseManager.Core.Helpers; +using GitReleaseManager.Core.Model; using GitReleaseManager.Core.Options; using GitReleaseManager.Core.Provider; using GitReleaseManager.Core.ReleaseNotes; using GitReleaseManager.Core.Templates; using Microsoft.Extensions.DependencyInjection; +using NGitLab; using Octokit; using Serilog; @@ -96,7 +98,6 @@ private static void RegisterServices(BaseSubOptions options) .AddSingleton(fileSystem) .AddSingleton() .AddSingleton() - .AddSingleton() .AddSingleton(); if (options is BaseVcsOptions vcsOptions) @@ -106,9 +107,7 @@ private static void RegisterServices(BaseSubOptions options) throw new Exception("The token option is not defined"); } - var gitHubClient = new GitHubClient(new ProductHeaderValue("GitReleaseManager")) { Credentials = new Credentials(vcsOptions.Token) }; - serviceCollection = serviceCollection - .AddSingleton(gitHubClient); + RegisterVcsProvider(vcsOptions, serviceCollection); } serviceCollection = serviceCollection @@ -197,5 +196,25 @@ private static Task ExecuteCommand(TOptions options) private static void LogOptions(BaseSubOptions options) => Log.Debug("{@Options}", options); + + private static void RegisterVcsProvider(BaseVcsOptions vcsOptions, IServiceCollection serviceCollection) + { + Log.Information("Using {Provider} as VCS Provider", vcsOptions.Provider); + if (vcsOptions.Provider == VcsProvider.GitLab) + { + var gitlabClient = new GitLabClient("https://gitlab.com", vcsOptions.Token); + serviceCollection + .AddSingleton() + .AddSingleton(gitlabClient); + } + else + { + // default to Github + var gitHubClient = new GitHubClient(new ProductHeaderValue("GitReleaseManager")) { Credentials = new Credentials(vcsOptions.Token) }; + serviceCollection + .AddSingleton(gitHubClient) + .AddSingleton(); + } + } } } \ No newline at end of file diff --git a/src/GitReleaseManager.Core.Tests/Provider/GitHubProviderTests.cs b/src/GitReleaseManager.Core.Tests/Provider/GitHubProviderTests.cs index 501300cf..c345b685 100644 --- a/src/GitReleaseManager.Core.Tests/Provider/GitHubProviderTests.cs +++ b/src/GitReleaseManager.Core.Tests/Provider/GitHubProviderTests.cs @@ -44,6 +44,7 @@ public class GitHubProviderTests private const bool SKIP_PRERELEASES = false; private readonly Release _release = new Release(); + private readonly Milestone _milestone = new Milestone { PublicNumber = MILESTONE_NUMBER }; private readonly ReleaseAssetUpload _releaseAssetUpload = new ReleaseAssetUpload(); private readonly Octokit.NewLabel _newLabel = new Octokit.NewLabel(LABEL_NAME, "ffffff"); private readonly Octokit.NewRelease _newRelease = new Octokit.NewRelease(TAG_NAME); @@ -270,7 +271,7 @@ public async Task Should_Get_Issues_For_Milestone(ItemStateFilter itemStateFilte _mapper.Map>(Arg.Any()) .Returns(issues); - var result = await _gitHubProvider.GetIssuesAsync(OWNER, REPOSITORY, MILESTONE_NUMBER, itemStateFilter).ConfigureAwait(false); + var result = await _gitHubProvider.GetIssuesAsync(OWNER, REPOSITORY, _milestone, itemStateFilter).ConfigureAwait(false); result.ShouldBeSameAs(issues); await _gitHubClient.Issue.Received(1).GetAllForRepository( @@ -288,7 +289,7 @@ public async Task Should_Throw_An_Exception_On_Getting_Issues_For_Non_Existent_M _gitHubClient.Issue.GetAllForRepository(OWNER, REPOSITORY, Arg.Any(), Arg.Any()) .Returns(Task.FromException>(_exception)); - var ex = await Should.ThrowAsync(() => _gitHubProvider.GetIssuesAsync(OWNER, REPOSITORY, 1)).ConfigureAwait(false); + var ex = await Should.ThrowAsync(() => _gitHubProvider.GetIssuesAsync(OWNER, REPOSITORY, _milestone)).ConfigureAwait(false); ex.Message.ShouldBe(_exception.Message); ex.InnerException.ShouldBe(_exception); } @@ -581,25 +582,29 @@ public async Task Should_Throw_An_Exception_On_Creating_Release() [Test] public async Task Should_Delete_Release() { - var id = 1; + var releaseId = 1; + var release = new Release(); + release.Id = releaseId; - _gitHubClient.Repository.Release.Delete(OWNER, REPOSITORY, id) + _gitHubClient.Repository.Release.Delete(OWNER, REPOSITORY, releaseId) .Returns(Task.CompletedTask); - await _gitHubProvider.DeleteReleaseAsync(OWNER, REPOSITORY, id).ConfigureAwait(false); + await _gitHubProvider.DeleteReleaseAsync(OWNER, REPOSITORY, release).ConfigureAwait(false); - await _gitHubClient.Repository.Release.Received(1).Delete(OWNER, REPOSITORY, id).ConfigureAwait(false); + await _gitHubClient.Repository.Release.Received(1).Delete(OWNER, REPOSITORY, releaseId).ConfigureAwait(false); } [Test] public async Task Should_Throw_An_Exception_On_Deleting_Release_For_Non_Existent_Id() { - var id = 1; + var releaseId = 1; + var release = new Release(); + release.Id = releaseId; - _gitHubClient.Repository.Release.Delete(OWNER, REPOSITORY, id) + _gitHubClient.Repository.Release.Delete(OWNER, REPOSITORY, releaseId) .Returns(Task.FromException(_notFoundException)); - var ex = await Should.ThrowAsync(() => _gitHubProvider.DeleteReleaseAsync(OWNER, REPOSITORY, id)).ConfigureAwait(false); + var ex = await Should.ThrowAsync(() => _gitHubProvider.DeleteReleaseAsync(OWNER, REPOSITORY, release)).ConfigureAwait(false); ex.Message.ShouldBe(_notFoundException.Message); ex.InnerException.ShouldBeSameAs(_notFoundException); } @@ -607,12 +612,14 @@ public async Task Should_Throw_An_Exception_On_Deleting_Release_For_Non_Existent [Test] public async Task Should_Throw_An_Exception_On_Deleting_Release() { - var id = 1; + var releaseId = 1; + var release = new Release(); + release.Id = releaseId; - _gitHubClient.Repository.Release.Delete(OWNER, REPOSITORY, id) + _gitHubClient.Repository.Release.Delete(OWNER, REPOSITORY, releaseId) .Returns(Task.FromException(_exception)); - var ex = await Should.ThrowAsync(() => _gitHubProvider.DeleteReleaseAsync(OWNER, REPOSITORY, id)).ConfigureAwait(false); + var ex = await Should.ThrowAsync(() => _gitHubProvider.DeleteReleaseAsync(OWNER, REPOSITORY, release)).ConfigureAwait(false); ex.Message.ShouldBe(_exception.Message); ex.InnerException.ShouldBeSameAs(_exception); } diff --git a/src/GitReleaseManager.Core.Tests/VcsServiceTests.cs b/src/GitReleaseManager.Core.Tests/VcsServiceTests.cs index 8b3ebe96..6d5d84c3 100644 --- a/src/GitReleaseManager.Core.Tests/VcsServiceTests.cs +++ b/src/GitReleaseManager.Core.Tests/VcsServiceTests.cs @@ -26,7 +26,8 @@ public class VcsServiceTests { private const string OWNER = "owner"; private const string REPOSITORY = "repository"; - private const int MILESTONE_NUMBER = 1; + private const int PUBLIC_MILESTONE_NUMBER = 1; + private const int INTERNAL_MILESTONE_NUMBER = 123; private const string MILESTONE_TITLE = "0.1.0"; private const string TAG_NAME = "0.1.0"; private const string RELEASE_NOTES = "Release Notes"; @@ -235,18 +236,18 @@ public async Task Should_Log_An_Warning_When_Labels_Not_Configured() [Test] public async Task Should_Close_Milestone() { - var milestone = new Milestone { Number = MILESTONE_NUMBER }; + var milestone = new Milestone { PublicNumber = PUBLIC_MILESTONE_NUMBER, InternalNumber = INTERNAL_MILESTONE_NUMBER }; _vcsProvider.GetMilestoneAsync(OWNER, REPOSITORY, MILESTONE_TITLE, ItemStateFilter.Open) .Returns(Task.FromResult(milestone)); - _vcsProvider.SetMilestoneStateAsync(OWNER, REPOSITORY, milestone.Number, ItemState.Closed) + _vcsProvider.SetMilestoneStateAsync(OWNER, REPOSITORY, milestone.InternalNumber, ItemState.Closed) .Returns(Task.CompletedTask); await _vcsService.CloseMilestoneAsync(OWNER, REPOSITORY, MILESTONE_TITLE).ConfigureAwait(false); await _vcsProvider.Received(1).GetMilestoneAsync(OWNER, REPOSITORY, MILESTONE_TITLE, ItemStateFilter.Open).ConfigureAwait(false); - await _vcsProvider.Received(1).SetMilestoneStateAsync(OWNER, REPOSITORY, milestone.Number, ItemState.Closed).ConfigureAwait(false); + await _vcsProvider.Received(1).SetMilestoneStateAsync(OWNER, REPOSITORY, milestone.InternalNumber, ItemState.Closed).ConfigureAwait(false); } [Test] @@ -258,25 +259,25 @@ public async Task Should_Log_An_Warning_On_Closing_When_Milestone_Cannot_Be_Foun await _vcsService.CloseMilestoneAsync(OWNER, REPOSITORY, MILESTONE_TITLE).ConfigureAwait(false); await _vcsProvider.Received(1).GetMilestoneAsync(OWNER, REPOSITORY, MILESTONE_TITLE, ItemStateFilter.Open).ConfigureAwait(false); - await _vcsProvider.DidNotReceive().SetMilestoneStateAsync(OWNER, REPOSITORY, MILESTONE_NUMBER, ItemState.Closed).ConfigureAwait(false); + await _vcsProvider.DidNotReceive().SetMilestoneStateAsync(OWNER, REPOSITORY, PUBLIC_MILESTONE_NUMBER, ItemState.Closed).ConfigureAwait(false); _logger.Received(1).Warning(UNABLE_TO_FOUND_MILESTONE_MESSAGE, "open", MILESTONE_TITLE, OWNER, REPOSITORY); } [Test] public async Task Should_Open_Milestone() { - var milestone = new Milestone { Number = MILESTONE_NUMBER }; + var milestone = new Milestone { PublicNumber = PUBLIC_MILESTONE_NUMBER, InternalNumber = INTERNAL_MILESTONE_NUMBER }; _vcsProvider.GetMilestoneAsync(OWNER, REPOSITORY, MILESTONE_TITLE, ItemStateFilter.Closed) .Returns(Task.FromResult(milestone)); - _vcsProvider.SetMilestoneStateAsync(OWNER, REPOSITORY, milestone.Number, ItemState.Open) + _vcsProvider.SetMilestoneStateAsync(OWNER, REPOSITORY, milestone.InternalNumber, ItemState.Open) .Returns(Task.CompletedTask); await _vcsService.OpenMilestoneAsync(OWNER, REPOSITORY, MILESTONE_TITLE).ConfigureAwait(false); await _vcsProvider.Received(1).GetMilestoneAsync(OWNER, REPOSITORY, MILESTONE_TITLE, ItemStateFilter.Closed).ConfigureAwait(false); - await _vcsProvider.Received(1).SetMilestoneStateAsync(OWNER, REPOSITORY, milestone.Number, ItemState.Open).ConfigureAwait(false); + await _vcsProvider.Received(1).SetMilestoneStateAsync(OWNER, REPOSITORY, milestone.InternalNumber, ItemState.Open).ConfigureAwait(false); _logger.Received(2).Verbose(Arg.Any(), MILESTONE_TITLE, OWNER, REPOSITORY); } @@ -289,7 +290,7 @@ public async Task Should_Log_An_Warning_On_Opening_When_Milestone_Cannot_Be_Foun await _vcsService.OpenMilestoneAsync(OWNER, REPOSITORY, MILESTONE_TITLE).ConfigureAwait(false); await _vcsProvider.Received(1).GetMilestoneAsync(OWNER, REPOSITORY, MILESTONE_TITLE, ItemStateFilter.Closed).ConfigureAwait(false); - await _vcsProvider.DidNotReceive().SetMilestoneStateAsync(OWNER, REPOSITORY, MILESTONE_NUMBER, ItemState.Open).ConfigureAwait(false); + await _vcsProvider.DidNotReceive().SetMilestoneStateAsync(OWNER, REPOSITORY, PUBLIC_MILESTONE_NUMBER, ItemState.Open).ConfigureAwait(false); _logger.Received(1).Warning(UNABLE_TO_FOUND_MILESTONE_MESSAGE, "closed", MILESTONE_TITLE, OWNER, REPOSITORY); } @@ -556,13 +557,13 @@ public async Task Should_Delete_Draft_Release() _vcsProvider.GetReleaseAsync(OWNER, REPOSITORY, TAG_NAME) .Returns(Task.FromResult(release)); - _vcsProvider.DeleteReleaseAsync(OWNER, REPOSITORY, release.Id) + _vcsProvider.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.Id).ConfigureAwait(false); + await _vcsProvider.Received(1).DeleteReleaseAsync(OWNER, REPOSITORY, release).ConfigureAwait(false); } [Test] @@ -576,7 +577,7 @@ public async Task Should_Not_Delete_Published_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.Id).ConfigureAwait(false); + await _vcsProvider.DidNotReceive().DeleteReleaseAsync(OWNER, REPOSITORY, release).ConfigureAwait(false); _logger.Received(1).Warning(Arg.Any(), TAG_NAME); } diff --git a/src/GitReleaseManager.Core/Commands/AddAssetsCommand.cs b/src/GitReleaseManager.Core/Commands/AddAssetsCommand.cs index 3073bc33..8765f18e 100644 --- a/src/GitReleaseManager.Core/Commands/AddAssetsCommand.cs +++ b/src/GitReleaseManager.Core/Commands/AddAssetsCommand.cs @@ -17,6 +17,14 @@ public AddAssetsCommand(IVcsService vcsService, ILogger logger) public async Task ExecuteAsync(AddAssetSubOptions options) { + var vcsOptions = options as BaseVcsOptions; + + if (vcsOptions?.Provider == Model.VcsProvider.GitLab) + { + _logger.Error("The addasset command is currently not supported when targetting GitLab."); + return 1; + } + _logger.Information("Uploading assets"); await _vcsService.AddAssetsAsync(options.RepositoryOwner, options.RepositoryName, options.TagName, options.AssetPaths).ConfigureAwait(false); diff --git a/src/GitReleaseManager.Core/Commands/ExportCommand.cs b/src/GitReleaseManager.Core/Commands/ExportCommand.cs index b61ec0e6..011ae3cd 100644 --- a/src/GitReleaseManager.Core/Commands/ExportCommand.cs +++ b/src/GitReleaseManager.Core/Commands/ExportCommand.cs @@ -18,7 +18,15 @@ public ExportCommand(IVcsService vcsService, ILogger logger) public async Task ExecuteAsync(ExportSubOptions options) { - _logger.Information("Exporting release {TagName}", options.TagName); + if (string.IsNullOrEmpty(options.TagName)) + { + _logger.Information("Exporting all releases"); + } + else + { + _logger.Information("Exporting release {TagName}", options.TagName); + } + var releasesContent = await _vcsService.ExportReleasesAsync(options.RepositoryOwner, options.RepositoryName, options.TagName, options.SkipPrereleases).ConfigureAwait(false); using (var sw = new StreamWriter(File.Open(options.FileOutputPath, FileMode.OpenOrCreate))) diff --git a/src/GitReleaseManager.Core/Commands/LabelCommand.cs b/src/GitReleaseManager.Core/Commands/LabelCommand.cs index da12d955..1a766b90 100644 --- a/src/GitReleaseManager.Core/Commands/LabelCommand.cs +++ b/src/GitReleaseManager.Core/Commands/LabelCommand.cs @@ -17,6 +17,14 @@ public LabelCommand(IVcsService vcsService, ILogger logger) public async Task ExecuteAsync(LabelSubOptions options) { + var vcsOptions = options as BaseVcsOptions; + + if (vcsOptions?.Provider == Model.VcsProvider.GitLab) + { + _logger.Error("The label command is currently not supported when targetting GitLab."); + return 1; + } + _logger.Information("Creating standard labels"); await _vcsService.CreateLabelsAsync(options.RepositoryOwner, options.RepositoryName).ConfigureAwait(false); diff --git a/src/GitReleaseManager.Core/Extensions/MilestoneExtensions.cs b/src/GitReleaseManager.Core/Extensions/MilestoneExtensions.cs index 41578201..19e29e6c 100644 --- a/src/GitReleaseManager.Core/Extensions/MilestoneExtensions.cs +++ b/src/GitReleaseManager.Core/Extensions/MilestoneExtensions.cs @@ -1,5 +1,4 @@ using System; -using Octokit; using Serilog; namespace GitReleaseManager.Core.Extensions @@ -8,7 +7,30 @@ public static class MilestoneExtensions { public static readonly ILogger _logger = Log.ForContext(typeof(MilestoneExtensions)); - public static Version Version(this Milestone ver) + public static Version Version(this Octokit.Milestone ver) + { + if (ver is null) + { + throw new ArgumentNullException(nameof(ver)); + } + + var nameWithoutPrerelease = ver.Title.Split('-')[0]; + if (nameWithoutPrerelease.StartsWith("v", StringComparison.OrdinalIgnoreCase)) + { + _logger.Debug("Removing version prefix from {Name}", ver.Title); + nameWithoutPrerelease = nameWithoutPrerelease.Remove(0, 1); + } + + if (!System.Version.TryParse(nameWithoutPrerelease, out Version parsedVersion)) + { + _logger.Warning("No valid version was found on {Title}", ver.Title); + return new Version(0, 0); + } + + return parsedVersion; + } + + public static Version Version(this NGitLab.Models.Milestone ver) { if (ver is null) { diff --git a/src/GitReleaseManager.Core/GitReleaseManager.Core.csproj b/src/GitReleaseManager.Core/GitReleaseManager.Core.csproj index f9737c58..75b9a6c0 100644 --- a/src/GitReleaseManager.Core/GitReleaseManager.Core.csproj +++ b/src/GitReleaseManager.Core/GitReleaseManager.Core.csproj @@ -23,6 +23,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all + diff --git a/src/GitReleaseManager.Core/MappingProfiles/GitHubProfile.cs b/src/GitReleaseManager.Core/MappingProfiles/GitHubProfile.cs index 87a73462..2dd7b300 100644 --- a/src/GitReleaseManager.Core/MappingProfiles/GitHubProfile.cs +++ b/src/GitReleaseManager.Core/MappingProfiles/GitHubProfile.cs @@ -20,6 +20,8 @@ public GitHubProfile() CreateMap().ReverseMap(); CreateMap(); CreateMap() + .ForMember(dest => dest.PublicNumber, act => act.MapFrom(src => src.Number)) + .ForMember(dest => dest.InternalNumber, act => act.MapFrom(src => src.Number)) .AfterMap((src, dest) => dest.Version = src.Version()); } } diff --git a/src/GitReleaseManager.Core/MappingProfiles/GitLabProfile.cs b/src/GitReleaseManager.Core/MappingProfiles/GitLabProfile.cs new file mode 100644 index 00000000..99f4fbd6 --- /dev/null +++ b/src/GitReleaseManager.Core/MappingProfiles/GitLabProfile.cs @@ -0,0 +1,35 @@ +namespace GitReleaseManager.Core.MappingProfiles +{ + using System; + using AutoMapper; + using GitReleaseManager.Core.Extensions; + + public class GitLabProfile : Profile + { + public GitLabProfile() + { + CreateMap() + .ForMember(dest => dest.PublicNumber, act => act.MapFrom(src => src.Iid)) + .ForMember(dest => dest.InternalNumber, act => act.MapFrom(src => src.Id)) + .AfterMap((src, dest) => dest.Version = src.Version()); + CreateMap() + .ForMember(dest => dest.Draft, act => act.MapFrom(src => src.ReleasedAt > DateTime.UtcNow)) + .ForMember(dest => dest.Body, act => act.MapFrom(src => src.Description)) + .ForMember(dest => dest.Assets, act => act.MapFrom(src => src.Assets.Links)) + .ReverseMap(); + CreateMap().ReverseMap(); + CreateMap() + .ForMember(dest => dest.Number, act => act.MapFrom(src => src.IssueId)) + .ForMember(dest => dest.HtmlUrl, act => act.MapFrom(src => src.WebUrl)) + .ReverseMap(); + CreateMap().ForMember(dest => dest.Name, act => act.MapFrom(src => src)); + CreateMap() + .ForMember(dest => dest.Description, act => act.MapFrom(src => src.Body)) + .ForMember(dest => dest.Ref, act => act.MapFrom(src => src.TargetCommitish)) + .ForMember(dest => dest.Milestones, act => act.MapFrom(src => new string[] { src.TagName })) + .ForMember(dest => dest.ReleasedAt, act => act.MapFrom(src => src.Draft ? DateTime.UtcNow.AddYears(1) : DateTime.UtcNow)) + .ForMember(dest => dest.Assets, act => act.Ignore()) + .ReverseMap(); + } + } +} diff --git a/src/GitReleaseManager.Core/Model/Milestone.cs b/src/GitReleaseManager.Core/Model/Milestone.cs index c719186b..13e1098a 100644 --- a/src/GitReleaseManager.Core/Model/Milestone.cs +++ b/src/GitReleaseManager.Core/Model/Milestone.cs @@ -8,7 +8,9 @@ public sealed class Milestone public string Description { get; set; } - public int Number { get; set; } + public int PublicNumber { get; set; } + + public int InternalNumber { get; set; } public string HtmlUrl { get; set; } diff --git a/src/GitReleaseManager.Core/Model/VcsProvider.cs b/src/GitReleaseManager.Core/Model/VcsProvider.cs new file mode 100644 index 00000000..565b218e --- /dev/null +++ b/src/GitReleaseManager.Core/Model/VcsProvider.cs @@ -0,0 +1,8 @@ +namespace GitReleaseManager.Core.Model +{ + public enum VcsProvider + { + GitHub = 0, + GitLab = 1, + } +} diff --git a/src/GitReleaseManager.Core/Options/BaseVcsSubOptions.cs b/src/GitReleaseManager.Core/Options/BaseVcsSubOptions.cs index 35b994bd..90faca2a 100644 --- a/src/GitReleaseManager.Core/Options/BaseVcsSubOptions.cs +++ b/src/GitReleaseManager.Core/Options/BaseVcsSubOptions.cs @@ -1,5 +1,6 @@ using CommandLine; using Destructurama.Attributed; +using GitReleaseManager.Core.Model; namespace GitReleaseManager.Core.Options { @@ -14,5 +15,8 @@ public abstract class BaseVcsOptions : BaseSubOptions [Option('r', "repository", HelpText = "The name of the repository.", Required = true)] public string RepositoryName { get; set; } + + [Option("provider", HelpText = "Version Control System provider", Default = VcsProvider.GitHub)] + public VcsProvider Provider { 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 d0960073..8e7e8cee 100644 --- a/src/GitReleaseManager.Core/Provider/GitHubProvider.cs +++ b/src/GitReleaseManager.Core/Provider/GitHubProvider.cs @@ -92,13 +92,13 @@ public Task CreateIssueCommentAsync(string owner, string repository, int issueNu }); } - public Task> GetIssuesAsync(string owner, string repository, int milestoneNumber, ItemStateFilter itemStateFilter = ItemStateFilter.All) + public Task> GetIssuesAsync(string owner, string repository, Milestone milestone, ItemStateFilter itemStateFilter = ItemStateFilter.All) { return ExecuteAsync(async () => { var openIssueRequest = new RepositoryIssueRequest { - Milestone = milestoneNumber.ToString(CultureInfo.InvariantCulture), + Milestone = milestone.PublicNumber.ToString(CultureInfo.InvariantCulture), State = (Octokit.ItemStateFilter)itemStateFilter, }; @@ -242,11 +242,11 @@ public Task CreateReleaseAsync(string owner, string repository, Release }); } - public Task DeleteReleaseAsync(string owner, string repository, int id) + public Task DeleteReleaseAsync(string owner, string repository, Release release) { return ExecuteAsync(async () => { - await _gitHubClient.Repository.Release.Delete(owner, repository, id).ConfigureAwait(false); + await _gitHubClient.Repository.Release.Delete(owner, repository, release.Id).ConfigureAwait(false); }); } @@ -345,6 +345,11 @@ public RateLimit GetRateLimit() } } + public string GetMiletoneUrlQueryString() + { + return "closed=1"; + } + private async Task ExecuteAsync(Func action) { try diff --git a/src/GitReleaseManager.Core/Provider/GitLabProvider.cs b/src/GitReleaseManager.Core/Provider/GitLabProvider.cs new file mode 100644 index 00000000..d0c398ee --- /dev/null +++ b/src/GitReleaseManager.Core/Provider/GitLabProvider.cs @@ -0,0 +1,337 @@ +namespace GitReleaseManager.Core.Provider +{ + using System; + using System.Collections.Generic; + using System.Globalization; + using System.Linq; + using System.Threading.Tasks; + using AutoMapper; + using NGitLab; + using NGitLab.Models; + using Serilog; + using ApiException = GitReleaseManager.Core.Exceptions.ApiException; + using Issue = GitReleaseManager.Core.Model.Issue; + using IssueComment = GitReleaseManager.Core.Model.IssueComment; + using ItemState = GitReleaseManager.Core.Model.ItemState; + using ItemStateFilter = GitReleaseManager.Core.Model.ItemStateFilter; + using Label = GitReleaseManager.Core.Model.Label; + using Milestone = GitReleaseManager.Core.Model.Milestone; + using NotFoundException = GitReleaseManager.Core.Exceptions.NotFoundException; + using RateLimit = GitReleaseManager.Core.Model.RateLimit; + using Release = GitReleaseManager.Core.Model.Release; + using ReleaseAssetUpload = GitReleaseManager.Core.Model.ReleaseAssetUpload; + + public class GitLabProvider : IVcsProvider + { + private const string NOT_FOUND_MESSGAE = "NotFound"; + + private readonly IGitLabClient _gitLabClient; + private readonly IMapper _mapper; + private readonly ILogger _logger; + + public GitLabProvider(IGitLabClient gitLabClient, IMapper mapper, ILogger logger) + { + _gitLabClient = gitLabClient; + _mapper = mapper; + _logger = logger; + } + + public Task DeleteAssetAsync(string owner, string repository, int id) + { + throw new NotImplementedException(); + } + + public Task UploadAssetAsync(Release release, ReleaseAssetUpload releaseAssetUpload) + { + return ExecuteAsync(async () => + { + _logger.Warning("Uploading of assets is not currently supported when targetting GitLab."); + }); + } + + public Task GetCommitsCountAsync(string owner, string repository, string @base, string head) + { + return ExecuteAsync(async () => + { + // TODO: neither of these clients seem to have the information that we need + return 0; + }); + } + + public string GetCommitsUrl(string owner, string repository, string head, string @base = null) + { + Ensure.IsNotNullOrWhiteSpace(owner, nameof(owner)); + Ensure.IsNotNullOrWhiteSpace(repository, nameof(repository)); + Ensure.IsNotNullOrWhiteSpace(head, nameof(head)); + + return string.IsNullOrWhiteSpace(@base) + ? string.Format(CultureInfo.InvariantCulture, "https://gitlab.com/{0}/{1}/-/commits/{2}", owner, repository, head) + : string.Format(CultureInfo.InvariantCulture, "https://gitlab.com/{0}/{1}/-/compare/{2}...{3}", owner, repository, @base, head); + } + + public Task CreateIssueCommentAsync(string owner, string repository, int issueNumber, string comment) + { + throw new NotImplementedException(); + } + + public Task> GetIssuesAsync(string owner, string repository, Milestone milestone, ItemStateFilter itemStateFilter = ItemStateFilter.All) + { + return ExecuteAsync(async () => + { + var issuesClient = _gitLabClient.Issues; + + var query = new IssueQuery(); + query.Milestone = milestone.Title; + + if (itemStateFilter == ItemStateFilter.Open) + { + query.State = IssueState.opened; + } + else if (itemStateFilter == ItemStateFilter.Closed) + { + query.State = IssueState.closed; + } + + var issues = issuesClient.GetAsync(GetGitLabProjectId(owner, repository), query); + return _mapper.Map>(issues); + }); + } + + public Task> GetIssueCommentsAsync(string owner, string repository, int issueNumber) + { + throw new NotImplementedException(); + } + + public Task CreateLabelAsync(string owner, string repository, Label label) + { + throw new NotImplementedException(); + } + + public Task DeleteLabelAsync(string owner, string repository, string labelName) + { + throw new NotImplementedException(); + } + + public Task> GetLabelsAsync(string owner, string repository) + { + throw new NotImplementedException(); + } + + public Task GetMilestoneAsync(string owner, string repository, string milestoneTitle, ItemStateFilter itemStateFilter = ItemStateFilter.All) + { + return ExecuteAsync(async () => + { + var milestones = await GetMilestonesAsync(owner, repository, itemStateFilter).ConfigureAwait(false); + var milestone = milestones.FirstOrDefault(m => m.Title == milestoneTitle); + + if (milestone is null) + { + throw new NotFoundException(NOT_FOUND_MESSGAE); + } + + return milestone; + }); + } + + public Task> GetMilestonesAsync(string owner, string repository, ItemStateFilter itemStateFilter = ItemStateFilter.All) + { + return ExecuteAsync(async () => + { + var query = new MilestoneQuery(); + + if (itemStateFilter == ItemStateFilter.Open) + { + query.State = MilestoneState.active; + } + else if (itemStateFilter == ItemStateFilter.Closed) + { + query.State = MilestoneState.closed; + } + + var mileStoneClient = _gitLabClient.GetMilestone(GetGitLabProjectId(owner, repository)); + + var milestones = mileStoneClient.Get(query); + var mappedMilestones = _mapper.Map>(milestones); + + foreach (var mappedMilestone in mappedMilestones) + { + mappedMilestone.HtmlUrl = string.Format("https://gitlab.com/{0}/{1}/-/milestones/{2}#tab-issues", owner, repository, mappedMilestone.PublicNumber); + } + + return mappedMilestones; + }); + } + + public Task SetMilestoneStateAsync(string owner, string repository, int milestoneNumber, ItemState itemState) + { + return ExecuteAsync(async () => + { + var mileStoneClient = _gitLabClient.GetMilestone(GetGitLabProjectId(owner, repository)); + + if (itemState == ItemState.Open) + { + mileStoneClient.Activate(milestoneNumber); + } + else if (itemState == ItemState.Closed) + { + mileStoneClient.Close(milestoneNumber); + } + }); + } + + public Task CreateReleaseAsync(string owner, string repository, Release release) + { + return ExecuteAsync(async () => + { + var releaseClient = _gitLabClient.GetReleases(GetGitLabProjectId(owner, repository)); + + var newRelease = _mapper.Map(release); + var nGitLabRelease = releaseClient.Create(newRelease); + var mappedRelease = _mapper.Map(nGitLabRelease); + + if (mappedRelease != null) + { + mappedRelease.HtmlUrl = string.Format("https://gitlab.com/{0}/{1}/-/releases/{2}", owner, repository, release.TagName); + } + + return mappedRelease; + }); + } + + public Task DeleteReleaseAsync(string owner, string repository, Release release) + { + return ExecuteAsync(async () => + { + var releaseClient = _gitLabClient.GetReleases(GetGitLabProjectId(owner, repository)); + + releaseClient.Delete(release.TagName); + }); + } + + public Task GetReleaseAsync(string owner, string repository, string tagName) + { + return ExecuteAsync(async () => + { + var releaseClient = _gitLabClient.GetReleases(GetGitLabProjectId(owner, repository)); + + var releases = releaseClient.GetAsync(); + + var release = releases.FirstOrDefault(r => r.TagName == tagName); + var mappedRelease = _mapper.Map(release); + + if (mappedRelease != null) + { + mappedRelease.HtmlUrl = string.Format("https://gitlab.com/{0}/{1}/-/releases/{2}", owner, repository, tagName); + } + + return mappedRelease; + }); + } + + public Task> GetReleasesAsync(string owner, string repository, bool skipPrereleases) + { + return ExecuteAsync(async () => + { + var releaseClient = _gitLabClient.GetReleases(GetGitLabProjectId(owner, repository)); + + return _mapper.Map>(releaseClient.GetAsync()); + }); + } + + public Task PublishReleaseAsync(string owner, string repository, string tagName, int releaseId) + { + return ExecuteAsync(async () => + { + var releaseClient = _gitLabClient.GetReleases(GetGitLabProjectId(owner, repository)); + + var update = new ReleaseUpdate + { + ReleasedAt = DateTime.UtcNow, + TagName = tagName, + }; + + releaseClient.Update(update); + }); + } + + public Task UpdateReleaseAsync(string owner, string repository, Release release) + { + return ExecuteAsync(async () => + { + var releaseClient = _gitLabClient.GetReleases(GetGitLabProjectId(owner, repository)); + + var update = new ReleaseUpdate + { + + Description = release.Body, + ReleasedAt = release.Draft ? DateTime.UtcNow.AddYears(1) : DateTime.UtcNow, + Name = release.Name, + TagName = release.TagName, + Milestones = new string[] { release.TagName }, + }; + + releaseClient.Update(update); + }); + } + + public RateLimit GetRateLimit() + { + // TODO: There doesn't currently seem to be a way to get the remaining + // rate limit for GitLab using the library we are using, so for now, + // let's just hard code it. + return new RateLimit { Limit = 600, Remaining = 100 }; + } + + public string GetMiletoneUrlQueryString() + { + return "sort=due_date_desc&state=closed"; + } + + private int GetGitLabProjectId(string owner, string repository) + { + var projectName = string.Format("{0}/{1}", owner, repository); + var project = _gitLabClient.Projects[projectName]; + return project.Id; + } + + private async Task ExecuteAsync(Func action) + { + try + { + await action().ConfigureAwait(false); + } + ////catch (Octokit.ForbiddenException ex) + ////{ + //// throw new ForbiddenException(ex.Message, ex); + ////} + ////catch (Octokit.NotFoundException ex) + ////{ + //// throw new NotFoundException(ex.Message, ex); + ////} + catch (Exception ex) when (!(ex is NotFoundException)) + { + throw new ApiException(ex.Message, ex); + } + } + + private async Task ExecuteAsync(Func> action) + { + try + { + return await action().ConfigureAwait(false); + } + ////catch (Octokit.ForbiddenException ex) + ////{ + //// throw new ForbiddenException(ex.Message, ex); + ////} + ////catch (Octokit.NotFoundException ex) + ////{ + //// throw new NotFoundException(ex.Message, ex); + ////} + catch (Exception ex) when (!(ex is NotFoundException)) + { + throw new ApiException(ex.Message, ex); + } + } + } +} diff --git a/src/GitReleaseManager.Core/Provider/IVcsProvider.cs b/src/GitReleaseManager.Core/Provider/IVcsProvider.cs index 32616f38..7647a2cc 100644 --- a/src/GitReleaseManager.Core/Provider/IVcsProvider.cs +++ b/src/GitReleaseManager.Core/Provider/IVcsProvider.cs @@ -16,7 +16,7 @@ public interface IVcsProvider Task CreateIssueCommentAsync(string owner, string repository, int issueNumber, string comment); - Task> GetIssuesAsync(string owner, string repository, int milestoneNumber, ItemStateFilter itemStateFilter = ItemStateFilter.All); + Task> GetIssuesAsync(string owner, string repository, Milestone milestone, ItemStateFilter itemStateFilter = ItemStateFilter.All); Task> GetIssueCommentsAsync(string owner, string repository, int issueNumber); @@ -34,7 +34,7 @@ public interface IVcsProvider Task CreateReleaseAsync(string owner, string repository, Release release); - Task DeleteReleaseAsync(string owner, string repository, int id); + Task DeleteReleaseAsync(string owner, string repository, Release release); Task GetReleaseAsync(string owner, string repository, string tagName); @@ -45,5 +45,7 @@ public interface IVcsProvider Task UpdateReleaseAsync(string owner, string repository, Release release); RateLimit GetRateLimit(); + + string GetMiletoneUrlQueryString(); } } \ No newline at end of file diff --git a/src/GitReleaseManager.Core/ReleaseNotes/ReleaseNotesBuilder.cs b/src/GitReleaseManager.Core/ReleaseNotes/ReleaseNotesBuilder.cs index 3f8c7d50..68461fc7 100644 --- a/src/GitReleaseManager.Core/ReleaseNotes/ReleaseNotesBuilder.cs +++ b/src/GitReleaseManager.Core/ReleaseNotes/ReleaseNotesBuilder.cs @@ -68,6 +68,8 @@ public async Task BuildReleaseNotesAsync(string user, string repository, var issuesDict = GetIssuesDict(issues); + var milestoneUrlQueryString = _vcsProvider.GetMiletoneUrlQueryString(); + var templateModel = new { Issues = new @@ -84,6 +86,7 @@ public async Task BuildReleaseNotesAsync(string user, string repository, { Target = _targetMilestone, Previous = previousMilestone, + UrlQueryString = milestoneUrlQueryString, }, IssueLabels = issuesDict.Keys.ToList(), }; @@ -181,7 +184,7 @@ private async Task LoadMilestonesAsync() private async Task> GetIssuesAsync(Milestone milestone) { - var allIssues = await _vcsProvider.GetIssuesAsync(_user, _repository, milestone.Number, ItemStateFilter.Closed).ConfigureAwait(false); + var allIssues = await _vcsProvider.GetIssuesAsync(_user, _repository, milestone, ItemStateFilter.Closed).ConfigureAwait(false); var result = CheckIssuesForValidLabels(allIssues); diff --git a/src/GitReleaseManager.Core/Templates/default/release-info.sbn b/src/GitReleaseManager.Core/Templates/default/release-info.sbn index 6a1084a5..1fbd615e 100644 --- a/src/GitReleaseManager.Core/Templates/default/release-info.sbn +++ b/src/GitReleaseManager.Core/Templates/default/release-info.sbn @@ -1,9 +1,9 @@ {{ if issues.count > 0 if commits.count > 0 -}}As part of this release we had [{{ commits.count }} {{ commits.count | string.pluralize "commit" "commits" }}]({{ commits.html_url }}) which resulted in [{{ issues.count }} {{ issues.count | string.pluralize "issue" "issues" }}]({{ milestone.target.html_url }}?closed=1) being closed. +}}As part of this release we had [{{ commits.count }} {{ commits.count | string.pluralize "commit" "commits" }}]({{ commits.html_url }}) which resulted in [{{ issues.count }} {{ issues.count | string.pluralize "issue" "issues" }}]({{ milestone.target.html_url }}?{{ milestone.url_query_string }}) being closed. {{ else -}}As part of this release we had [{{ issues.count }} {{ issues.count | string.pluralize "issue" "issues" }}]({{ milestone.target.html_url }}?closed=1) closed. +}}As part of this release we had [{{ issues.count }} {{ issues.count | string.pluralize "issue" "issues" }}]({{ milestone.target.html_url }}?{{ milestone.url_query_string }}) closed. {{ end else if commits.count > 0 }}As part of this release we had [{{ commits.count }} {{ commits.count | string.pluralize "commit" "commits" }}]({{ commits.html_url }}). diff --git a/src/GitReleaseManager.Core/Templates/empty/create/footer.sbn b/src/GitReleaseManager.Core/Templates/empty/create/footer.sbn new file mode 100644 index 00000000..e69de29b diff --git a/src/GitReleaseManager.Core/Templates/empty/index.sbn b/src/GitReleaseManager.Core/Templates/empty/index.sbn new file mode 100644 index 00000000..b09ac493 --- /dev/null +++ b/src/GitReleaseManager.Core/Templates/empty/index.sbn @@ -0,0 +1 @@ +This is empty on purpose! \ No newline at end of file diff --git a/src/GitReleaseManager.Core/VcsService.cs b/src/GitReleaseManager.Core/VcsService.cs index f9499f44..da3ae662 100644 --- a/src/GitReleaseManager.Core/VcsService.cs +++ b/src/GitReleaseManager.Core/VcsService.cs @@ -114,7 +114,7 @@ public async Task DiscardReleaseAsync(string owner, string repository, string ta if (release.Draft) { - await _vcsProvider.DeleteReleaseAsync(owner, repository, release.Id).ConfigureAwait(false); + await _vcsProvider.DeleteReleaseAsync(owner, repository, release).ConfigureAwait(false); } else { @@ -151,7 +151,15 @@ private async Task AddAssetsAsync(string owner, string repository, string tagNam if (existingAsset != null) { _logger.Warning("Requested asset to be uploaded already exists on draft release, replacing with new file: {AssetPath}", asset); - await _vcsProvider.DeleteAssetAsync(owner, repository, existingAsset.Id).ConfigureAwait(false); + + if (_vcsProvider is GitLabProvider) + { + _logger.Error("Deleting of assets is not currently supported when targetting GitLab."); + } + else + { + await _vcsProvider.DeleteAssetAsync(owner, repository, existingAsset.Id).ConfigureAwait(false); + } } var upload = new ReleaseAssetUpload @@ -246,11 +254,18 @@ public async Task CloseMilestoneAsync(string owner, string repository, string mi var milestone = await _vcsProvider.GetMilestoneAsync(owner, repository, milestoneTitle, ItemStateFilter.Open).ConfigureAwait(false); _logger.Verbose("Closing milestone '{Title}' on '{Owner}/{Repository}'", milestoneTitle, owner, repository); - await _vcsProvider.SetMilestoneStateAsync(owner, repository, milestone.Number, ItemState.Closed).ConfigureAwait(false); + await _vcsProvider.SetMilestoneStateAsync(owner, repository, milestone.InternalNumber, ItemState.Closed).ConfigureAwait(false); if (_configuration.Close.IssueComments) { - await AddIssueCommentsAsync(owner, repository, milestone).ConfigureAwait(false); + if (_vcsProvider is GitLabProvider) + { + _logger.Error("Adding comments to issues when closing a milestone is not currently supported when targetting GitLab."); + } + else + { + await AddIssueCommentsAsync(owner, repository, milestone).ConfigureAwait(false); + } } } catch (NotFoundException) @@ -267,7 +282,7 @@ public async Task OpenMilestoneAsync(string owner, string repository, string mil var milestone = await _vcsProvider.GetMilestoneAsync(owner, repository, milestoneTitle, ItemStateFilter.Closed).ConfigureAwait(false); _logger.Verbose("Opening milestone '{Title}' on '{Owner}/{Repository}'", milestoneTitle, owner, repository); - await _vcsProvider.SetMilestoneStateAsync(owner, repository, milestone.Number, ItemState.Open).ConfigureAwait(false); + await _vcsProvider.SetMilestoneStateAsync(owner, repository, milestone.InternalNumber, ItemState.Open).ConfigureAwait(false); } catch (NotFoundException) { @@ -374,8 +389,8 @@ private async Task AddIssueCommentsAsync(string owner, string repository, Milest const string detectionComment = ""; var issueComment = detectionComment + "\n" + _configuration.Close.IssueCommentFormat.ReplaceTemplate(new { owner, repository, Milestone = milestone.Title }); - _logger.Verbose("Finding issues with milestone: '{Milestone}", milestone.Number); - var issues = await _vcsProvider.GetIssuesAsync(owner, repository, milestone.Number, ItemStateFilter.Closed).ConfigureAwait(false); + _logger.Verbose("Finding issues with milestone: '{Milestone}", milestone.PublicNumber); + var issues = await _vcsProvider.GetIssuesAsync(owner, repository, milestone, ItemStateFilter.Closed).ConfigureAwait(false); foreach (var issue in issues) { diff --git a/src/GitReleaseManager.IntegrationTests/GitHubProviderIntegrationTests.cs b/src/GitReleaseManager.IntegrationTests/GitHubProviderIntegrationTests.cs index 272eb943..710616c1 100644 --- a/src/GitReleaseManager.IntegrationTests/GitHubProviderIntegrationTests.cs +++ b/src/GitReleaseManager.IntegrationTests/GitHubProviderIntegrationTests.cs @@ -63,14 +63,14 @@ public async Task Should_Get_Milestones() var result = await _gitHubProvider.GetMilestonesAsync(OWNER, REPOSITORY).ConfigureAwait(false); result.Count().ShouldBeGreaterThan(0); - _milestone = result.OrderByDescending(m => m.Number).First(); + _milestone = result.OrderByDescending(m => m.PublicNumber).First(); } [Test] [Order(3)] public async Task Should_Get_Issues() { - var result = await _gitHubProvider.GetIssuesAsync(OWNER, REPOSITORY, _milestone.Number).ConfigureAwait(false); + var result = await _gitHubProvider.GetIssuesAsync(OWNER, REPOSITORY, _milestone).ConfigureAwait(false); result.Count().ShouldBeGreaterThan(0); _issue = result.First(); diff --git a/src/GitReleaseManager.Tests/ReleaseNotesBuilderTests.cs b/src/GitReleaseManager.Tests/ReleaseNotesBuilderTests.cs index 3d6007db..7d0fcf0f 100644 --- a/src/GitReleaseManager.Tests/ReleaseNotesBuilderTests.cs +++ b/src/GitReleaseManager.Tests/ReleaseNotesBuilderTests.cs @@ -206,7 +206,6 @@ private static void AcceptTest(int commits, Config config, Milestone milestone, { var owner = "TestUser"; var repository = "FakeRepository"; - var milestoneNumber = 1; milestone ??= CreateMilestone("1.2.3"); var vcsService = new VcsServiceMock(); @@ -231,12 +230,15 @@ private static void AcceptTest(int commits, Config config, Milestone milestone, vcsProvider.GetCommitsUrl(owner, repository, Arg.Any(), Arg.Any()) .Returns(o => new GitHubProvider(null, null).GetCommitsUrl((string)o[0], (string)o[1], (string)o[2], (string)o[3])); - vcsProvider.GetIssuesAsync(owner, repository, milestoneNumber, ItemStateFilter.Closed) + vcsProvider.GetIssuesAsync(owner, repository, milestone, ItemStateFilter.Closed) .Returns(Task.FromResult((IEnumerable)vcsService.Issues)); vcsProvider.GetMilestonesAsync(owner, repository, Arg.Any()) .Returns(Task.FromResult((IEnumerable)vcsService.Milestones)); + vcsProvider.GetMiletoneUrlQueryString() + .Returns("closed=1"); + var builder = new ReleaseNotesBuilder(vcsProvider, logger, fileSystem, configuration, new TemplateFactory(fileSystem, configuration, TemplateKind.Create)); var notes = builder.BuildReleaseNotesAsync(owner, repository, milestone.Title, ReleaseTemplates.DEFAULT_NAME).Result; @@ -249,7 +251,8 @@ private static Milestone CreateMilestone(string version, string description = nu { Title = version, Description = description, - Number = 1, + PublicNumber = 1, + InternalNumber = 123, HtmlUrl = "https://github.com/gep13/FakeRepository/issues?q=milestone%3A" + version, Version = new Version(version), }; diff --git a/src/GitReleaseManager.Tool/GitReleaseManager.Tool.csproj b/src/GitReleaseManager.Tool/GitReleaseManager.Tool.csproj index 99fad416..205bb31c 100644 --- a/src/GitReleaseManager.Tool/GitReleaseManager.Tool.csproj +++ b/src/GitReleaseManager.Tool/GitReleaseManager.Tool.csproj @@ -38,6 +38,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all +