diff --git a/docs/input/docs/configuration/default-configuration.md b/docs/input/docs/configuration/default-configuration.md index 79b90b78..9dbe9a86 100644 --- a/docs/input/docs/configuration/default-configuration.md +++ b/docs/input/docs/configuration/default-configuration.md @@ -148,6 +148,14 @@ control the look and feel of the generated release notes. no issues are found to be associated with a milestone. The contents of the empty release can be controlled via the associated Scriban template. **NOTE:** This configuration option was added in version 0.20.0 of GitReleaseManager. +- **sort-issues-by** + - A string value which indicates the name of the field used to sort the issues. The + possible values are: Title, Id. + **NOTE:** This configuration option was added in version 0.21.0 of GitReleaseManager. +- **sort-issues-direction** + - A string value which indicates how the issues are sorted. The posible values are: + Ascending, Descending. + **NOTE:** This configuration option was added in version 0.21.0 of GitReleaseManager. See the [example create configuration section](create-configuration) to see an example of how a footer can be configured. diff --git a/src/GitReleaseManager.Core/Configuration/Config.cs b/src/GitReleaseManager.Core/Configuration/Config.cs index 51da2a99..e25aa88f 100644 --- a/src/GitReleaseManager.Core/Configuration/Config.cs +++ b/src/GitReleaseManager.Core/Configuration/Config.cs @@ -29,6 +29,8 @@ public Config() AllowUpdateToPublishedRelease = false, AllowMilestonesWithoutIssues = false, IncludeContributors = false, + SortIssuesBy = Model.SortIssuesBy.Id, + SortIssuesDirection = Model.SortDirection.Ascending, }; Export = new ExportConfig diff --git a/src/GitReleaseManager.Core/Configuration/CreateConfig.cs b/src/GitReleaseManager.Core/Configuration/CreateConfig.cs index 06b27389..65d355f0 100644 --- a/src/GitReleaseManager.Core/Configuration/CreateConfig.cs +++ b/src/GitReleaseManager.Core/Configuration/CreateConfig.cs @@ -1,5 +1,6 @@ using System.ComponentModel; using GitReleaseManager.Core.Attributes; +using GitReleaseManager.Core.Model; using YamlDotNet.Serialization; namespace GitReleaseManager.Core.Configuration @@ -40,5 +41,11 @@ public class CreateConfig [YamlMember(Alias = "include-contributors")] public bool IncludeContributors { get; set; } + + [YamlMember(Alias = "sort-issues-by")] + public SortIssuesBy SortIssuesBy { get; set; } + + [YamlMember(Alias = "sort-issues-direction")] + public SortDirection SortIssuesDirection { get; set; } } } \ No newline at end of file diff --git a/src/GitReleaseManager.Core/Model/SortDirection.cs b/src/GitReleaseManager.Core/Model/SortDirection.cs new file mode 100644 index 00000000..5a498eb0 --- /dev/null +++ b/src/GitReleaseManager.Core/Model/SortDirection.cs @@ -0,0 +1,11 @@ +namespace GitReleaseManager.Core.Model +{ + public enum SortDirection + { + /// Ascending. + Ascending = 0, + + /// Descending. + Descending = 1, + } +} \ No newline at end of file diff --git a/src/GitReleaseManager.Core/Model/SortIssuesBy.cs b/src/GitReleaseManager.Core/Model/SortIssuesBy.cs new file mode 100644 index 00000000..02e4b1aa --- /dev/null +++ b/src/GitReleaseManager.Core/Model/SortIssuesBy.cs @@ -0,0 +1,15 @@ +namespace GitReleaseManager.Core.Model +{ + public enum SortIssuesBy + { + /// + /// Sort by title of the issue + /// + Title, + + /// + /// Sort by the Id of the issue + /// + Id, + } +} \ No newline at end of file diff --git a/src/GitReleaseManager.Core/ReleaseNotes/ReleaseNotesBuilder.cs b/src/GitReleaseManager.Core/ReleaseNotes/ReleaseNotesBuilder.cs index b8fbb815..20a7cf76 100644 --- a/src/GitReleaseManager.Core/ReleaseNotes/ReleaseNotesBuilder.cs +++ b/src/GitReleaseManager.Core/ReleaseNotes/ReleaseNotesBuilder.cs @@ -147,13 +147,34 @@ private Dictionary> GetIssuesDict(List issues) var issueLabels = _configuration.IssueLabelsInclude; var excludedIssueLabels = _configuration.IssueLabelsExclude; + Func getSortPropertyName = (sortBy) => + { + return sortBy switch + { + SortIssuesBy.Title => "Title", + SortIssuesBy.Id => "PublicNumber", + _ => throw new NotSupportedException($"'{sortBy}' is an unknown sort property."), + }; + }; + + Func keySelector = (i) => typeof(Issue).GetProperty(getSortPropertyName(_configuration.Create.SortIssuesBy)).GetValue(i, null); + var issuesByLabel = issues .Where(o => !o.Labels.Any(l => excludedIssueLabels.Any(eil => string.Equals(eil, l.Name, StringComparison.OrdinalIgnoreCase)))) .SelectMany(o => o.Labels, (issue, label) => new { Label = label.Name, Issue = issue }) .Where(o => issueLabels.Any(il => string.Equals(il, o.Label, StringComparison.OrdinalIgnoreCase))) .GroupBy(o => o.Label, o => o.Issue) - .OrderBy(o => o.Key) - .ToDictionary(o => GetValidLabel(o.Key, o.Count()), o => o.OrderBy(issue => issue.PublicNumber).ToList()); + .OrderBy(o => o.Key) // Sort the labels alphabetically + .ToDictionary(o => GetValidLabel(o.Key, o.Count()), o => + { + // Sort the issues within each group based on configuration + return _configuration.Create.SortIssuesDirection switch + { + SortDirection.Ascending => o.OrderBy(i => keySelector(i)).ToList(), + SortDirection.Descending => o.OrderByDescending(i => keySelector(i)).ToList(), + _ => throw new NotSupportedException($"'{_configuration.Create.SortIssuesDirection}' is an unknown sort direction."), + }; + }); return issuesByLabel; } diff --git a/src/GitReleaseManager.IntegrationTests/ReleaseNotesBuilderIntegrationTests.cs b/src/GitReleaseManager.IntegrationTests/ReleaseNotesBuilderIntegrationTests.cs index 2415e5c6..04c85cb3 100644 --- a/src/GitReleaseManager.IntegrationTests/ReleaseNotesBuilderIntegrationTests.cs +++ b/src/GitReleaseManager.IntegrationTests/ReleaseNotesBuilderIntegrationTests.cs @@ -77,6 +77,10 @@ public async Task SingleMilestone() // Indicate that we want to include the 'Contributors' section in the release notes configuration.Create.IncludeContributors = true; + // Configure sorting + configuration.Create.SortIssuesBy = Core.Model.SortIssuesBy.Title; + configuration.Create.SortIssuesDirection = Core.Model.SortDirection.Descending; + var vcsProvider = new GitHubProvider(_gitHubClient, _mapper, _graphQlClient); var releaseNotesBuilder = new ReleaseNotesBuilder(vcsProvider, _logger, fileSystem, configuration, new TemplateFactory(fileSystem, configuration, TemplateKind.Create)); var result = await releaseNotesBuilder.BuildReleaseNotesAsync("GitTools", "GitReleaseManager", "0.12.0", string.Empty).ConfigureAwait(false); // 0.12.0 contains a mix of issues and PRs diff --git a/src/GitReleaseManager.Tests/ConfigurationTests.cs b/src/GitReleaseManager.Tests/ConfigurationTests.cs index 4ebfc358..e21499a3 100644 --- a/src/GitReleaseManager.Tests/ConfigurationTests.cs +++ b/src/GitReleaseManager.Tests/ConfigurationTests.cs @@ -2,6 +2,7 @@ using System.IO; using System.Text; using GitReleaseManager.Core.Configuration; +using GitReleaseManager.Core.Model; using NUnit.Framework; namespace GitReleaseManager.Tests @@ -127,5 +128,45 @@ public void Should_WriteSample_Multiline_String_Values() Environment.NewLine); Assert.That(text, Contains.Substring(expectedText)); } + + [Test] + [TestCase(SortIssuesBy.Id, "Id")] + [TestCase(SortIssuesBy.Title, "Title")] + public void Should_Write_SortIssuesBy_Values(SortIssuesBy sortBy, string expected) + { + // Given + var config = new Config(); + config.Create.SortIssuesBy = sortBy; + + // When + var builder = new StringBuilder(); + using (var writer = new StringWriter(builder)) + { + ConfigSerializer.Write(config, writer); + } + + // Then + Assert.That(builder.ToString(), Contains.Substring($"sort-issues-by: {expected}")); + } + + [Test] + [TestCase(SortDirection.Ascending, "Ascending")] + [TestCase(SortDirection.Descending, "Descending")] + public void Should_Write_SortIssuesDirection_Values(SortDirection sortDirection, string expected) + { + // Given + var config = new Config(); + config.Create.SortIssuesDirection = sortDirection; + + // When + var builder = new StringBuilder(); + using (var writer = new StringWriter(builder)) + { + ConfigSerializer.Write(config, writer); + } + + // Then + Assert.That(builder.ToString(), Contains.Substring($"sort-issues-direction: {expected}")); + } } } \ No newline at end of file