From 21eb7f67fda7bc03594b9b3cbe56c605c4d3269b Mon Sep 17 00:00:00 2001 From: AdmiringWorm Date: Wed, 16 Sep 2020 05:26:21 +0200 Subject: [PATCH 01/12] (GH-217) Create text template for generating templates --- .gitignore | 1 + recipe.cake | 13 ++ .../GitReleaseManager.Core.csproj | 26 ++++ .../Templates/ReleaseTemplates.tt | 135 ++++++++++++++++++ .../Templates/default/create/footer.sbn | 4 + .../Templates/default/index.sbn | 5 + .../Templates/default/issue-details.sbn | 5 + .../Templates/default/issue-note.sbn | 1 + .../Templates/default/issues.sbn | 3 + .../Templates/default/milestone.sbn | 1 + .../Templates/default/release-info.sbn | 11 ++ 11 files changed, 205 insertions(+) create mode 100644 src/GitReleaseManager.Core/Templates/ReleaseTemplates.tt create mode 100644 src/GitReleaseManager.Core/Templates/default/create/footer.sbn create mode 100644 src/GitReleaseManager.Core/Templates/default/index.sbn create mode 100644 src/GitReleaseManager.Core/Templates/default/issue-details.sbn create mode 100644 src/GitReleaseManager.Core/Templates/default/issue-note.sbn create mode 100644 src/GitReleaseManager.Core/Templates/default/issues.sbn create mode 100644 src/GitReleaseManager.Core/Templates/default/milestone.sbn create mode 100644 src/GitReleaseManager.Core/Templates/default/release-info.sbn diff --git a/.gitignore b/.gitignore index 604391e2..896e6f6c 100644 --- a/.gitignore +++ b/.gitignore @@ -327,3 +327,4 @@ config.wyam.packages.xml # Integration Tests tests/integration/tools tests/integration/output +*.g.cs diff --git a/recipe.cake b/recipe.cake index 3996632a..72bfd8ad 100644 --- a/recipe.cake +++ b/recipe.cake @@ -1,4 +1,5 @@ #load nuget:?package=Cake.Recipe&version=2.1.0 +#tool dotnet:?package=dotnet-t4&version=2.0.5 Environment.SetVariableNames(githubTokenVariable: "GITTOOLS_GITHUB_TOKEN"); @@ -43,6 +44,18 @@ BuildParameters.Tasks.DotNetCoreBuildTask.Does((context) => BuildParameters.Tasks.CreateReleaseNotesTask .IsDependentOn(BuildParameters.Tasks.DotNetCoreBuildTask); // We need to be sure that the executable exist, and have been registered before using it +Task("Transform-TextTemplates") + .IsDependeeOf(BuildParameters.Tasks.DotNetCoreBuildTask.Task.Name) + .Does(() => +{ + var templates = GetFiles("src/**/*.tt"); + + foreach (var template in templates) + { + TransformTemplate(template); + } +}); + ((CakeTask)BuildParameters.Tasks.ExportReleaseNotesTask.Task).ErrorHandler = null; ((CakeTask)BuildParameters.Tasks.PublishGitHubReleaseTask.Task).ErrorHandler = null; BuildParameters.Tasks.PublishPreReleasePackagesTask.IsDependentOn(BuildParameters.Tasks.PublishGitHubReleaseTask); diff --git a/src/GitReleaseManager.Core/GitReleaseManager.Core.csproj b/src/GitReleaseManager.Core/GitReleaseManager.Core.csproj index 8dcce6d1..07b052bb 100644 --- a/src/GitReleaseManager.Core/GitReleaseManager.Core.csproj +++ b/src/GitReleaseManager.Core/GitReleaseManager.Core.csproj @@ -1,4 +1,7 @@ + + + 8.0 netstandard2.0;net461 @@ -6,7 +9,13 @@ Create release notes in markdown given a milestone false $(NoWarn);CA1707; + true + + + + + @@ -20,4 +29,21 @@ + + + True + True + ReleaseTemplates.tt + + + + + TextTemplatingFileGenerator + ReleaseTemplates.g.cs + + + + + + \ No newline at end of file diff --git a/src/GitReleaseManager.Core/Templates/ReleaseTemplates.tt b/src/GitReleaseManager.Core/Templates/ReleaseTemplates.tt new file mode 100644 index 00000000..80040c12 --- /dev/null +++ b/src/GitReleaseManager.Core/Templates/ReleaseTemplates.tt @@ -0,0 +1,135 @@ +<#@ template debug="false" hostspecific="true" language="C#" #> +<#@ assembly name="System.Core" #> +<#@ import namespace="System.IO" #> +<#@ import namespace="System.Linq" #> +<#@ import namespace="System.Text" #> +<#@ import namespace="System.Collections.Generic" #> +<#@ output extension=".g.cs" #> +<# + var currentPath = this.Host.ResolvePath("."); + var directories = Directory.EnumerateDirectories(currentPath); + var resourceKeys = new Dictionary>(); + var encoding = new UTF8Encoding(false); + var allKindNames = new string[] { "create" }; +#> +/* + * Do NOT Edit this file, it is automatically generated during + * build time. + */ + +namespace GitReleaseManager.Core.Templates +{ + using System; + using System.Collections.Generic; + using System.IO; + + public static class ReleaseTemplates + { + public const string RESOURCE_PREFIX = "resource://"; + + #region Templates +<# + foreach (var directory in directories) + { + string resourceName = Path.GetFileName(directory).ToUpperInvariant() + "_"; +#> + public const string <#= resourceName #>NAME = "<#= resourceName.TrimEnd('_').ToLowerInvariant() #>"; +<# + foreach (var file in Directory.EnumerateFiles(directory, "*.sbn")) + { + var fileName = Path.GetFileNameWithoutExtension(file); + var content = File.ReadAllText(file, encoding); + var fileNameKey = fileName.Replace(".", "__").Replace("-", "__").ToUpperInvariant(); + resourceKeys.Add(resourceName + fileNameKey, new List()); +#> + public static readonly string <#= resourceName + fileNameKey #> = "<#= content.Replace("\\", "\\\\").Replace("\"", "\\\"").Replace("\r", "\\r").Replace("\n", "\\n") #>"; +<# + foreach (var key in allKindNames) + { + var path = Path.Combine(directory, key, Path.GetFileName(file)); + if (!File.Exists(path)) + { + resourceKeys[resourceName + fileNameKey].Add(resourceName + key.ToUpperInvariant() + "_" + fileNameKey); + } + } + } + foreach (var resourceDirectory in Directory.EnumerateDirectories(directory)) + { + string configResourceName = Path.GetFileName(resourceDirectory).ToUpperInvariant() + "_"; + foreach (var file in Directory.EnumerateFiles(resourceDirectory, "*.sbn")) + { + var filename = Path.GetFileNameWithoutExtension(file); + var content = File.ReadAllText(file, encoding); + var fileNameKey = filename.Replace(".", "__").Replace("-", "__").ToUpperInvariant(); + resourceKeys.Add(resourceName + configResourceName + fileNameKey, new List{ resourceName + configResourceName + fileNameKey }); +#> + public static readonly string <#= resourceName + configResourceName + fileNameKey #> = "<#= content.Replace("\\", "\\\\").Replace("\"", "\\\"").Replace("\r", "\\r").Replace("\n", "\\n") #>"; +<# + } + } + } +#> + #endregion Templates + + #region Loading + + public static string LoadTemplate(string resourcePath) + { + if (resourcePath is null) + { + throw new ArgumentNullException(nameof(resourcePath)); + } + + if (resourcePath.StartsWith(RESOURCE_PREFIX)) + { + resourcePath = resourcePath.Substring(RESOURCE_PREFIX.Length); + } + + switch (resourcePath.Replace("/", "_").Replace("-", "__").ToUpperInvariant()) + { +<# + foreach (var item in resourceKeys) + { + foreach (var value in item.Value) + { +#> + case "<#= value #>": +<# + } +#> + return <#= item.Key #>; + +<# + } +#> + default: + throw new ArgumentOutOfRangeException(nameof(resourcePath), $"Embedded template '{resourcePath}' was not found"); + } + } + + #endregion Loading + + #region Exporting + + public static IReadOnlyDictionary GetExportTemplates() + { + var result = new Dictionary + { +<# + foreach (var key in resourceKeys.Keys) + { + var relativePath = key.Replace("__", "-").Split('_').Select(k => k.ToLowerInvariant()); + var text = "\"" + string.Join("\", \"", relativePath) + ".sbn\""; +#> + { Path.Combine(<#= text #>), <#= key #> }, +<# + } +#> + }; + + return result; + } + + #endregion + } +} \ No newline at end of file diff --git a/src/GitReleaseManager.Core/Templates/default/create/footer.sbn b/src/GitReleaseManager.Core/Templates/default/create/footer.sbn new file mode 100644 index 00000000..59dddbb9 --- /dev/null +++ b/src/GitReleaseManager.Core/Templates/default/create/footer.sbn @@ -0,0 +1,4 @@ +{{~ if include_footer ~}} +### {{ footer_heading }} +{{ footer_content }} +{{~ end ~}} diff --git a/src/GitReleaseManager.Core/Templates/default/index.sbn b/src/GitReleaseManager.Core/Templates/default/index.sbn new file mode 100644 index 00000000..f0ab9e02 --- /dev/null +++ b/src/GitReleaseManager.Core/Templates/default/index.sbn @@ -0,0 +1,5 @@ +{{ include 'release-info' ~}} +{{ include 'milestone' ~}} + +{{ include 'issues' ~}} +{{ include 'footer' ~}} diff --git a/src/GitReleaseManager.Core/Templates/default/issue-details.sbn b/src/GitReleaseManager.Core/Templates/default/issue-details.sbn new file mode 100644 index 00000000..2fc7ae22 --- /dev/null +++ b/src/GitReleaseManager.Core/Templates/default/issue-details.sbn @@ -0,0 +1,5 @@ +__{{ issue_label }}__ + +{{ for issue in issues[issue_label] ~}} + {{~ include 'issue-note' ~}} +{{~ end }} diff --git a/src/GitReleaseManager.Core/Templates/default/issue-note.sbn b/src/GitReleaseManager.Core/Templates/default/issue-note.sbn new file mode 100644 index 00000000..ddfdb2c5 --- /dev/null +++ b/src/GitReleaseManager.Core/Templates/default/issue-note.sbn @@ -0,0 +1 @@ +- [__#{{ issue.number }}__]({{ issue.html_url }}) {{ issue.title }} diff --git a/src/GitReleaseManager.Core/Templates/default/issues.sbn b/src/GitReleaseManager.Core/Templates/default/issues.sbn new file mode 100644 index 00000000..59597f1f --- /dev/null +++ b/src/GitReleaseManager.Core/Templates/default/issues.sbn @@ -0,0 +1,3 @@ +{{ for issue_label in issue_labels ~}} + {{~ include 'issue-details' ~}} +{{~ end ~}} diff --git a/src/GitReleaseManager.Core/Templates/default/milestone.sbn b/src/GitReleaseManager.Core/Templates/default/milestone.sbn new file mode 100644 index 00000000..c16e85c6 --- /dev/null +++ b/src/GitReleaseManager.Core/Templates/default/milestone.sbn @@ -0,0 +1 @@ +{{ milestone_description }} diff --git a/src/GitReleaseManager.Core/Templates/default/release-info.sbn b/src/GitReleaseManager.Core/Templates/default/release-info.sbn new file mode 100644 index 00000000..fc2da166 --- /dev/null +++ b/src/GitReleaseManager.Core/Templates/default/release-info.sbn @@ -0,0 +1,11 @@ +{{ + if issues_count > 0 + if commits_count > 0 + "As part of this release we had [" + commits_text + "](" + commits_link + ") which resulted in [" + issues_text + "](" + milestone_html_url + "?closed=1) being closed." + else + "As part of this release we had [" + issues_text + "](" + milestone_html_url + "?closed=1) closed." + end + else if commits_count > 0 + "As part of this release we had [" + commits_text + "](" + commits_link + ")." + end +}} From 75c2df994849abff656fdf9859820be571849737 Mon Sep 17 00:00:00 2001 From: AdmiringWorm Date: Wed, 16 Sep 2020 05:29:09 +0200 Subject: [PATCH 02/12] (GH-217) Move template logic to its own class(es) --- src/GitReleaseManager.Cli/Program.cs | 7 + .../Templates/TemplateLoaderTests.cs | 374 ++++++++++++++++++ .../VcsServiceTests.cs | 19 +- .../Configuration/Config.cs | 15 + .../Configuration/ConfigSerializer.cs | 2 +- .../ReleaseNotes/IReleaseNotesBuilder.cs | 2 +- .../ReleaseNotes/ReleaseNotesBuilder.cs | 16 +- .../ReleaseNotes/ReleaseNotesTemplate.cs | 32 -- .../Templates/TemplateFactory.cs | 65 +++ .../Templates/TemplateKind.cs | 13 + .../Templates/TemplateLoader.cs | 207 ++++++++++ src/GitReleaseManager.Core/VcsService.cs | 24 +- .../ReleaseNotesBuilderIntegrationTests.cs | 9 +- .../ReleaseNotesBuilderTests.cs | 5 +- 14 files changed, 717 insertions(+), 73 deletions(-) create mode 100644 src/GitReleaseManager.Core.Tests/Templates/TemplateLoaderTests.cs delete mode 100644 src/GitReleaseManager.Core/ReleaseNotes/ReleaseNotesTemplate.cs create mode 100644 src/GitReleaseManager.Core/Templates/TemplateFactory.cs create mode 100644 src/GitReleaseManager.Core/Templates/TemplateKind.cs create mode 100644 src/GitReleaseManager.Core/Templates/TemplateLoader.cs diff --git a/src/GitReleaseManager.Cli/Program.cs b/src/GitReleaseManager.Cli/Program.cs index 2130425d..a02105be 100644 --- a/src/GitReleaseManager.Cli/Program.cs +++ b/src/GitReleaseManager.Cli/Program.cs @@ -19,6 +19,7 @@ namespace GitReleaseManager.Cli using GitReleaseManager.Core.Options; using GitReleaseManager.Core.Provider; using GitReleaseManager.Core.ReleaseNotes; + using GitReleaseManager.Core.Templates; using Microsoft.Extensions.DependencyInjection; using Octokit; using Serilog; @@ -116,6 +117,12 @@ private static void RegisterServices(BaseSubOptions options) .AddSingleton(gitHubClient); } + if (options is CreateSubOptions) + { + serviceCollection = serviceCollection + .AddTransient((services) => new TemplateFactory(services.GetRequiredService(), services.GetRequiredService(), TemplateKind.Create)); + } + _serviceProvider = serviceCollection.BuildServiceProvider(); } diff --git a/src/GitReleaseManager.Core.Tests/Templates/TemplateLoaderTests.cs b/src/GitReleaseManager.Core.Tests/Templates/TemplateLoaderTests.cs new file mode 100644 index 00000000..8deab9e2 --- /dev/null +++ b/src/GitReleaseManager.Core.Tests/Templates/TemplateLoaderTests.cs @@ -0,0 +1,374 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) 2015 - Present - GitTools Contributors +// +// ----------------------------------------------------------------------- + +namespace GitReleaseManager.Core.Tests.Templates +{ + using System; + using System.Collections; + using System.IO; + using GitReleaseManager.Core.Configuration; + using GitReleaseManager.Core.Helpers; + using GitReleaseManager.Core.Templates; + using NSubstitute; + using NUnit.Framework; + using Scriban; + using Shouldly; + + [TestFixture] + public class TemplateLoaderTests + { + public static IEnumerable PossibleScribanTestPaths + { + get + { + var basePath = Path.Combine(Environment.CurrentDirectory, ".templates"); + yield return new TestCaseData(Path.Combine(basePath, "default", "create", "index.sbn")); + yield return new TestCaseData(Path.Combine(basePath, "default", "create", "index.scriban")); + yield return new TestCaseData(Path.Combine(basePath, "default", "create", "default.sbn")); + yield return new TestCaseData(Path.Combine(basePath, "default", "create", "default.scriban")); + yield return new TestCaseData(Path.Combine(basePath, "create", "default", "index.sbn")); + yield return new TestCaseData(Path.Combine(basePath, "create", "default", "index.scriban")); + yield return new TestCaseData(Path.Combine(basePath, "create", "default", "default.sbn")); + yield return new TestCaseData(Path.Combine(basePath, "create", "default", "default.scriban")); + yield return new TestCaseData(Path.Combine(basePath, "default", "index.sbn")); + yield return new TestCaseData(Path.Combine(basePath, "default", "index.scriban")); + yield return new TestCaseData(Path.Combine(basePath, "default", "default.sbn")); + yield return new TestCaseData(Path.Combine(basePath, "default", "default.scriban")); + yield return new TestCaseData(Path.Combine(basePath, "create", "index.sbn")); + yield return new TestCaseData(Path.Combine(basePath, "create", "index.scriban")); + yield return new TestCaseData(Path.Combine(basePath, "create", "default.sbn")); + yield return new TestCaseData(Path.Combine(basePath, "create", "default.scriban")); + yield return new TestCaseData(Path.Combine(basePath, "default.sbn")); + yield return new TestCaseData(Path.Combine(basePath, "default.scriban")); + } + } + + public static IEnumerable PossibleScribanRelativePaths + { + get + { + var basePath = Path.Combine(Environment.CurrentDirectory, ".templates"); + yield return new TestCaseData(Path.Combine(basePath, "default", "create", "test.sbn")); + yield return new TestCaseData(Path.Combine(basePath, "default", "create", "test.scriban")); + yield return new TestCaseData(Path.Combine(basePath, "create", "default", "test.sbn")); + yield return new TestCaseData(Path.Combine(basePath, "create", "default", "test.scriban")); + yield return new TestCaseData(Path.Combine(basePath, "create", "test.sbn")); + yield return new TestCaseData(Path.Combine(basePath, "create", "test.scriban")); + yield return new TestCaseData(Path.Combine(basePath, "default", "test.sbn")); + yield return new TestCaseData(Path.Combine(basePath, "default", "test.scriban")); + yield return new TestCaseData(Path.Combine(basePath, "test.sbn")); + yield return new TestCaseData(Path.Combine(basePath, "test.scriban")); + } + } + + public static IEnumerable ValidFilePathForResolvingResources + { + get + { + var basePath = Path.Combine(Environment.CurrentDirectory, ".templates"); + yield return new TestCaseData(Path.Combine(basePath, "default", "create", "index.sbn")); + yield return new TestCaseData(Path.Combine(basePath, "default", "create", "index.scriban")); + yield return new TestCaseData(Path.Combine(basePath, "default", "create", "default.sbn")); + yield return new TestCaseData(Path.Combine(basePath, "default", "create", "default.scriban")); + yield return new TestCaseData(Path.Combine(basePath, "create", "default", "index.sbn")); + yield return new TestCaseData(Path.Combine(basePath, "create", "default", "index.scriban")); + yield return new TestCaseData(Path.Combine(basePath, "create", "default", "default.sbn")); + yield return new TestCaseData(Path.Combine(basePath, "create", "default", "default.scriban")); + yield return new TestCaseData(Path.Combine(basePath, "default", "index.sbn")); + yield return new TestCaseData(Path.Combine(basePath, "default", "index.scriban")); + yield return new TestCaseData(Path.Combine(basePath, "default", "default.sbn")); + yield return new TestCaseData(Path.Combine(basePath, "default", "default.scriban")); + } + } + + public static IEnumerable InvalidFilePathsForResolvigResources + { + get + { + var basePath = Path.Combine(Environment.CurrentDirectory, ".templates"); + yield return new TestCaseData(Path.Combine(basePath, "create", "index.sbn")); + yield return new TestCaseData(Path.Combine(basePath, "create", "index.scriban")); + yield return new TestCaseData(Path.Combine(basePath, "create", "default.sbn")); + yield return new TestCaseData(Path.Combine(basePath, "create", "default.scriban")); + yield return new TestCaseData(Path.Combine(basePath, "default.sbn")); + yield return new TestCaseData(Path.Combine(basePath, "default.scriban")); + } + } + + public static IEnumerable ResourceTemplateLoading + { + get + { + yield return new TestCaseData("default/create/footer", ReleaseTemplates.DEFAULT_CREATE_FOOTER); + yield return new TestCaseData("default/create/index", ReleaseTemplates.DEFAULT_INDEX); + yield return new TestCaseData("default/create/issue-details", ReleaseTemplates.DEFAULT_ISSUE__DETAILS); + yield return new TestCaseData("default/create/issue-note", ReleaseTemplates.DEFAULT_ISSUE__NOTE); + yield return new TestCaseData("default/create/issues", ReleaseTemplates.DEFAULT_ISSUES); + yield return new TestCaseData("default/create/milestone", ReleaseTemplates.DEFAULT_MILESTONE); + yield return new TestCaseData("default/create/release-info", ReleaseTemplates.DEFAULT_RELEASE__INFO); + } + } + + [Test] + public void Should_GetDefaultResourcePath() + { + var fileSystem = Substitute.For(); + fileSystem.Exists(Arg.Any()).Returns(false); + var config = new Config(); + var loader = new TemplateLoader(config, fileSystem, TemplateKind.Create); + + var result = loader.GetPath(null, default, "default"); + + result.ShouldBe(ReleaseTemplates.RESOURCE_PREFIX + "default/create/index"); + } + + [Test] + public void Should_GetRelativeResourcePath() + { + var fileSystem = Substitute.For(); + fileSystem.Exists(Arg.Any()).Returns(false); + var config = new Config(); + var loader = new TemplateLoader(config, fileSystem, TemplateKind.Create); + var templateContext = new TemplateContext(); + templateContext.PushSourceFile(ReleaseTemplates.RESOURCE_PREFIX + "default/create/index"); + + var result = loader.GetPath(templateContext, default, "release-info"); + + result.ShouldBe(ReleaseTemplates.RESOURCE_PREFIX + "default/create/release-info"); + } + + [TestCase("index.scriban")] + [TestCase("index.sbn")] + public void Should_GetFullIndexPathWhenFileExists(string expectedFileName) + { + var expectedFile = Path.Combine(Environment.CurrentDirectory, ".templates", "default", "create", expectedFileName); + var fileSystem = Substitute.For(); + fileSystem.Exists(Arg.Any()).Returns(false); + fileSystem.Exists(expectedFile).Returns(true); + + var loader = new TemplateLoader(new Config(), fileSystem, TemplateKind.Create); + var result = loader.GetPath(null, default, "default"); + + result.ShouldBe(expectedFile); + } + + [TestCase("test.scriban")] + [TestCase("grm.sbn")] + public void Should_GetRelativeFileWhenExists(string expectedFileName) + { + var expectedFile = Path.Combine(Environment.CurrentDirectory, ".templates", "default", "create", expectedFileName); + var fileSystem = Substitute.For(); + fileSystem.Exists(Arg.Any()).Returns(false); + fileSystem.Exists(expectedFile).Returns(true); + var context = new TemplateContext(); + context.PushSourceFile(Path.Combine(Environment.CurrentDirectory, ".templates", "default", "create", "index.sbn")); + + var loader = new TemplateLoader(new Config(), fileSystem, TemplateKind.Create); + var result = loader.GetPath(context, default, Path.GetFileNameWithoutExtension(expectedFileName)); + + result.ShouldBe(expectedFile); + } + + [TestCase("txt")] + [TestCase("md")] + [TestCase("html")] + public void Should_GetFullIndexPathWhenExtensionIsUsedAndFileExists(string extension) + { + var expectedFile = Path.Combine(Environment.CurrentDirectory, ".templates", "default", "create", "index." + extension); + var fileSystem = Substitute.For(); + fileSystem.Exists(Arg.Any()).Returns(false); + fileSystem.Exists(expectedFile).Returns(true); + + var loader = new TemplateLoader(new Config(), fileSystem, TemplateKind.Create); + var result = loader.GetPath(null, default, "default." + extension); + + result.ShouldBe(expectedFile); + } + + [TestCase("txt")] + [TestCase("md")] + [TestCase("html")] + public void Should_GetRelativeWhenExtensionIsUsedAndFileWhenExists(string extension) + { + var expectedFile = Path.Combine(Environment.CurrentDirectory, ".templates", "default", "create", "test-file." + extension); + var fileSystem = Substitute.For(); + fileSystem.Exists(Arg.Any()).Returns(false); + fileSystem.Exists(expectedFile).Returns(true); + var context = new TemplateContext(); + context.PushSourceFile(Path.Combine(Environment.CurrentDirectory, ".templates", "default", "create", "index.sbn")); + + var loader = new TemplateLoader(new Config(), fileSystem, TemplateKind.Create); + var result = loader.GetPath(context, default, "test-file." + extension); + + result.ShouldBe(expectedFile); + } + + [Test] + public void Should_GetFilePathWhenPreviousSourceWasResourcefile() + { + var expectedFile = Path.Combine(Environment.CurrentDirectory, ".templates", "default", "create", "releases.sbn"); + var fileSystem = Substitute.For(); + fileSystem.Exists(Arg.Any()).Returns(false); + fileSystem.Exists(expectedFile).Returns(true); + var context = new TemplateContext(); + context.PushSourceFile(ReleaseTemplates.RESOURCE_PREFIX + "default/create/full"); + + var loader = new TemplateLoader(new Config(), fileSystem, TemplateKind.Create); + var result = loader.GetPath(context, default, "releases"); + + result.ShouldBe(expectedFile); + } + + [TestCaseSource(nameof(ValidFilePathForResolvingResources))] + public void Should_GetResourcePathWhenPreviousSourceWasFile(string sourcePath) + { + var expected = ReleaseTemplates.RESOURCE_PREFIX + "default/create/note"; + var fileSystem = Substitute.For(); + fileSystem.Exists(Arg.Any()).Returns(false); + var context = new TemplateContext(); + context.PushSourceFile(sourcePath); + + var loader = new TemplateLoader(new Config(), fileSystem, TemplateKind.Create); + var result = loader.GetPath(context, default, "note"); + + result.ShouldBe(expected); + } + + [TestCaseSource(nameof(InvalidFilePathsForResolvigResources))] + public void Should_ThrowFileNotFoundExceptionOnInvalidFilePathsForResourceFallback(string sourcePath) + { + var fileSystem = Substitute.For(); + fileSystem.Exists(Arg.Any()).Returns(false); + var context = new TemplateContext(); + context.PushSourceFile(sourcePath); + + var loader = new TemplateLoader(new Config(), fileSystem, TemplateKind.Create); + + Should.Throw(() => loader.GetPath(context, default, "test")); + } + + [Test] + public void Should_GetExistingFileExtensionAsPreviousFile() + { + var expectedFile = Path.Combine(Environment.CurrentDirectory, ".templates", "xml", "create", "release.xml"); + var fileSystem = Substitute.For(); + fileSystem.Exists(Arg.Any()).Returns(false); + fileSystem.Exists(expectedFile).Returns(true); + var context = new TemplateContext(); + context.PushSourceFile(Path.Combine(Environment.CurrentDirectory, ".templates", "xml", "create", "index.xml")); + + var loader = new TemplateLoader(new Config(), fileSystem, TemplateKind.Create); + var result = loader.GetPath(context, default, "release"); + + result.ShouldBe(expectedFile); + } + + [TestCaseSource(nameof(PossibleScribanTestPaths))] + public void Should_GetExpectedSourcePaths(string expectedFile) + { + var fileSystem = Substitute.For(); + fileSystem.Exists(Arg.Any()).Returns(false); + fileSystem.Exists(expectedFile).Returns(true); + + var loader = new TemplateLoader(new Config(), fileSystem, TemplateKind.Create); + var result = loader.GetPath(null, default, "default"); + + result.ShouldBe(expectedFile); + } + + [TestCaseSource(nameof(PossibleScribanRelativePaths))] + public void Should_GetExpectedRelativePathsWhenPreviousIsResource(string expectedFile) + { + var fileSystem = Substitute.For(); + fileSystem.Exists(Arg.Any()).Returns(false); + fileSystem.Exists(expectedFile).Returns(true); + var context = new TemplateContext(); + context.PushSourceFile(ReleaseTemplates.RESOURCE_PREFIX + "default/create/index"); + + var loader = new TemplateLoader(new Config(), fileSystem, TemplateKind.Create); + var result = loader.GetPath(context, default, "test"); + + result.ShouldBe(expectedFile); + } + + [Test] + public void Should_GetValidPathFromAbsolutePath() + { + var expectedFile = Path.Combine(Environment.CurrentDirectory, "test-file.md"); + var fileSystem = Substitute.For(); + fileSystem.Exists(Arg.Any()).Returns(false); + fileSystem.Exists(expectedFile).Returns(true); + + var loader = new TemplateLoader(new Config(), fileSystem, TemplateKind.Create); + var result = loader.GetPath(null, default, expectedFile); + + result.ShouldBe(expectedFile); + } + + [TestCase("sbn")] + [TestCase("scriban")] + public void Should_GetValidPathFromAbsolutePathWithoutExtension(string extension) + { + var expectedFile = Path.Combine(Environment.CurrentDirectory, "test-file." + extension); + var fileSystem = Substitute.For(); + fileSystem.Exists(Arg.Any()).Returns(false); + fileSystem.Exists(expectedFile).Returns(true); + + var loader = new TemplateLoader(new Config(), fileSystem, TemplateKind.Create); + var result = loader.GetPath(null, default, Path.Combine(Environment.CurrentDirectory, "test-file")); + + result.ShouldBe(expectedFile); + } + + [TestCaseSource(nameof(ResourceTemplateLoading))] + public void Should_LoadReleaseTemplateFromResource(string key, string expected) + { + var fileSystem = Substitute.For(); + + var loader = new TemplateLoader(new Config(), fileSystem, TemplateKind.Create); + var result = loader.Load(null, default, ReleaseTemplates.RESOURCE_PREFIX + key); + + result.ShouldBe(expected); + } + + [Test] + public void Should_ThrowExceptionOnInvalidResource() + { + var fileSystem = Substitute.For(); + + var loader = new TemplateLoader(new Config(), fileSystem, TemplateKind.Create); + + Should.Throw(() => loader.Load(null, default, ReleaseTemplates.RESOURCE_PREFIX + "invalid")); + } + + [TestCase(null)] + [TestCase("")] + [TestCase(" ")] + public void Should_ReturnEmptyTextWhenPathIsEmpty(string path) + { + var fileSystem = Substitute.For(); + + var loader = new TemplateLoader(new Config(), fileSystem, TemplateKind.Create); + var result = loader.Load(null, default, path); + + result.ShouldBeEmpty(); + } + + [Test] + public void Should_LoadAllTextFromFilePath() + { + var expected = "I WAS Loaded!!!"; + var testPath = Path.Combine(Environment.CurrentDirectory, "test.sbn"); + var fileSystem = Substitute.For(); + fileSystem.ReadAllText(testPath).Returns(expected); + + var loader = new TemplateLoader(new Config(), fileSystem, TemplateKind.Create); + var result = loader.Load(null, default, testPath); + + result.ShouldBe(expected); + } + } +} diff --git a/src/GitReleaseManager.Core.Tests/VcsServiceTests.cs b/src/GitReleaseManager.Core.Tests/VcsServiceTests.cs index dfed7651..82a8b723 100644 --- a/src/GitReleaseManager.Core.Tests/VcsServiceTests.cs +++ b/src/GitReleaseManager.Core.Tests/VcsServiceTests.cs @@ -16,6 +16,7 @@ namespace GitReleaseManager.Core.Tests using GitReleaseManager.Core.Model; using GitReleaseManager.Core.Provider; using GitReleaseManager.Core.ReleaseNotes; +using GitReleaseManager.Core.Templates; using NSubstitute; using NUnit.Framework; using Serilog; @@ -35,13 +36,13 @@ public class VcsServiceTests private const string _milestoneTitle = "0.1.0"; private const string _tagName = "0.1.0"; private const string _releaseNotes = "Release Notes"; - private const string _releaseNotesTemplate = "Release Notes Template"; private const string _assetContent = "Asset Content"; private const string _unableToFoundMilestoneMessage = "Unable to find a {State} milestone with title '{Title}' on '{Owner}/{Repository}'"; private const string _unableToFoundReleaseMessage = "Unable to find a release with tag '{TagName}' on '{Owner}/{Repository}'"; - private readonly string _tempPath = Path.GetTempPath(); + private static readonly string _tempPath = Path.GetTempPath(); + private static readonly string _releaseNotesTemplate = Path.Combine(_tempPath, "ReleaseNotesTemplate.txt"); private readonly List _assets = new List(); private readonly List _files = new List(); private readonly NotFoundException _notFoundException = new NotFoundException("NotFound"); @@ -302,7 +303,7 @@ public async Task Should_Create_Release_From_Milestone() { var release = new Release(); - _releaseNotesBuilder.BuildReleaseNotes(_owner, _repository, _milestoneTitle, ReleaseNotesTemplate.Default) + _releaseNotesBuilder.BuildReleaseNotes(_owner, _repository, _milestoneTitle, ReleaseTemplates.DEFAULT_NAME) .Returns(Task.FromResult(_releaseNotes)); _vcsProvider.GetReleaseAsync(_owner, _repository, _milestoneTitle) @@ -314,7 +315,7 @@ public async Task Should_Create_Release_From_Milestone() var result = await _vcsService.CreateReleaseFromMilestoneAsync(_owner, _repository, _milestoneTitle, _milestoneTitle, null, null, false, null).ConfigureAwait(false); result.ShouldBeSameAs(release); - await _releaseNotesBuilder.Received(1).BuildReleaseNotes(_owner, _repository, _milestoneTitle, ReleaseNotesTemplate.Default).ConfigureAwait(false); + await _releaseNotesBuilder.Received(1).BuildReleaseNotes(_owner, _repository, _milestoneTitle, ReleaseTemplates.DEFAULT_NAME).ConfigureAwait(false); await _vcsProvider.Received(1).GetReleaseAsync(_owner, _repository, _milestoneTitle).ConfigureAwait(false); await _vcsProvider.Received(1).CreateReleaseAsync(_owner, _repository, Arg.Is(o => o.Body == _releaseNotes && @@ -354,6 +355,7 @@ await _vcsProvider.Received(1).CreateReleaseAsync(_owner, _repository, Arg.Is(() => _vcsService.CreateReleaseFromMilestoneAsync(_owner, _repository, _milestoneTitle, _milestoneTitle, null, null, false, null)).ConfigureAwait(false); ex.Message.ShouldBe($"Release with tag '{_milestoneTitle}' not in draft state, so not updating"); - await _releaseNotesBuilder.Received(1).BuildReleaseNotes(_owner, _repository, _milestoneTitle, ReleaseNotesTemplate.Default).ConfigureAwait(false); + await _releaseNotesBuilder.Received(1).BuildReleaseNotes(_owner, _repository, _milestoneTitle, ReleaseTemplates.DEFAULT_NAME).ConfigureAwait(false); await _vcsProvider.Received(1).GetReleaseAsync(_owner, _repository, _milestoneTitle).ConfigureAwait(false); } diff --git a/src/GitReleaseManager.Core/Configuration/Config.cs b/src/GitReleaseManager.Core/Configuration/Config.cs index e8333c10..46b16bcf 100644 --- a/src/GitReleaseManager.Core/Configuration/Config.cs +++ b/src/GitReleaseManager.Core/Configuration/Config.cs @@ -6,8 +6,10 @@ namespace GitReleaseManager.Core.Configuration { + using System; using System.Collections.Generic; using System.ComponentModel; + using System.IO; using YamlDotNet.Serialization; public class Config @@ -136,8 +138,21 @@ public Config() }; LabelAliases = new List(); + + TemplatesDirectory = ".templates"; } + [Description("The directory where templates are located")] + [YamlMember(Alias = "templates-dir")] + public string TemplatesDirectory + { + get => TemplatesDirectoryInfo.FullName.Replace(Environment.CurrentDirectory, string.Empty).TrimStart('\\', '/'); + set => TemplatesDirectoryInfo = new DirectoryInfo(Path.Combine(Environment.CurrentDirectory, value)); + } + + [YamlIgnore] + public DirectoryInfo TemplatesDirectoryInfo { get; private set; } + [Description("Configuration values used when creating new releases")] [YamlMember(Alias = "create")] public CreateConfig Create { get; private set; } diff --git a/src/GitReleaseManager.Core/Configuration/ConfigSerializer.cs b/src/GitReleaseManager.Core/Configuration/ConfigSerializer.cs index b660c0a3..b53a6072 100644 --- a/src/GitReleaseManager.Core/Configuration/ConfigSerializer.cs +++ b/src/GitReleaseManager.Core/Configuration/ConfigSerializer.cs @@ -87,7 +87,7 @@ private static void SetConfigurationSamples(object config, Type configType, Text var sampleAttribute = property.GetCustomAttribute(); var propertyType = property.PropertyType; - if (propertyType.IsClass && propertyType != typeof(string)) + if (propertyType.IsClass && propertyType != typeof(string) && propertyType != typeof(DirectoryInfo)) { var subConfig = property.GetValue(config); SetConfigurationSamples(subConfig, propertyType, writer); diff --git a/src/GitReleaseManager.Core/ReleaseNotes/IReleaseNotesBuilder.cs b/src/GitReleaseManager.Core/ReleaseNotes/IReleaseNotesBuilder.cs index 8bac46de..30918d9d 100644 --- a/src/GitReleaseManager.Core/ReleaseNotes/IReleaseNotesBuilder.cs +++ b/src/GitReleaseManager.Core/ReleaseNotes/IReleaseNotesBuilder.cs @@ -10,6 +10,6 @@ namespace GitReleaseManager.Core.ReleaseNotes public interface IReleaseNotesBuilder { - Task BuildReleaseNotes(string user, string repository, string milestoneTitle, string templateText); + Task BuildReleaseNotes(string user, string repository, string milestoneTitle, string template); } } \ No newline at end of file diff --git a/src/GitReleaseManager.Core/ReleaseNotes/ReleaseNotesBuilder.cs b/src/GitReleaseManager.Core/ReleaseNotes/ReleaseNotesBuilder.cs index 8ed31e65..ebeb2104 100644 --- a/src/GitReleaseManager.Core/ReleaseNotes/ReleaseNotesBuilder.cs +++ b/src/GitReleaseManager.Core/ReleaseNotes/ReleaseNotesBuilder.cs @@ -13,30 +13,37 @@ namespace GitReleaseManager.Core.ReleaseNotes using System.Threading.Tasks; using GitReleaseManager.Core.Configuration; using GitReleaseManager.Core.Extensions; + using GitReleaseManager.Core.Helpers; using GitReleaseManager.Core.Model; using GitReleaseManager.Core.Provider; + using GitReleaseManager.Core.Templates; using Scriban; + using Scriban.Runtime; using Serilog; public class ReleaseNotesBuilder : IReleaseNotesBuilder { private readonly IVcsProvider _vcsProvider; private readonly ILogger _logger; + private readonly IFileSystem _fileSystem; private readonly Config _configuration; + private readonly TemplateFactory _templateFactory; private string _user; private string _repository; private string _milestoneTitle; private IEnumerable _milestones; private Milestone _targetMilestone; - public ReleaseNotesBuilder(IVcsProvider vcsProvider, ILogger logger, Config configuration) + public ReleaseNotesBuilder(IVcsProvider vcsProvider, ILogger logger, IFileSystem fileSystem, Config configuration, TemplateFactory templateFactory) { _vcsProvider = vcsProvider; _logger = logger; + _fileSystem = fileSystem; _configuration = configuration; + _templateFactory = templateFactory; } - public async Task BuildReleaseNotes(string user, string repository, string milestoneTitle, string templateText) + public async Task BuildReleaseNotes(string user, string repository, string milestoneTitle, string template) { _user = user; _repository = repository; @@ -83,7 +90,7 @@ public async Task BuildReleaseNotes(string user, string repository, stri var issuesDict = GetIssuesDict(issues); - var templateContext = new + var templateModel = new { IssuesCount = issues.Count, CommitsCount = numberOfCommits, @@ -99,8 +106,7 @@ public async Task BuildReleaseNotes(string user, string repository, stri FooterContent = footerContent, }; - var template = Template.Parse(templateText); - var releaseNotes = template.Render(templateContext); + var releaseNotes = await _templateFactory.RenderTemplateAsync(template, templateModel).ConfigureAwait(false); _logger.Verbose("Finished building release notes"); diff --git a/src/GitReleaseManager.Core/ReleaseNotes/ReleaseNotesTemplate.cs b/src/GitReleaseManager.Core/ReleaseNotes/ReleaseNotesTemplate.cs deleted file mode 100644 index 2d7f659c..00000000 --- a/src/GitReleaseManager.Core/ReleaseNotes/ReleaseNotesTemplate.cs +++ /dev/null @@ -1,32 +0,0 @@ -namespace GitReleaseManager.Core.ReleaseNotes -{ - public static class ReleaseNotesTemplate - { - public const string Default = @"{{ -if issues_count > 0 - if commits_count > 0 - ""As part of this release we had ["" + commits_text + ""]("" + commits_link + "") which resulted in ["" + issues_text + ""]("" + milestone_html_url + ""?closed=1) being closed."" - else - ""As part of this release we had ["" + issues_text + ""]("" + milestone_html_url + ""?closed=1) closed."" - end - -else if commits_count > 0 - ""As part of this release we had ["" + commits_text + ""]("" + commits_link + "")."" -end -}} -{{ milestone_description }} - -{{~ for issue_label in issue_labels ~}} -__{{ issue_label }}__ - -{{~ for issue in issues[issue_label] ~}} -- [__#{{ issue.number }}__]({{ issue.html_url }}) {{ issue.title }} -{{~ end ~}} - -{{~ end ~}} -{{~ if include_footer ~}} -### {{ footer_heading }} -{{ footer_content }} -{{~ end ~}}"; - } -} \ No newline at end of file diff --git a/src/GitReleaseManager.Core/Templates/TemplateFactory.cs b/src/GitReleaseManager.Core/Templates/TemplateFactory.cs new file mode 100644 index 00000000..9b5507ae --- /dev/null +++ b/src/GitReleaseManager.Core/Templates/TemplateFactory.cs @@ -0,0 +1,65 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) 2015 - Present - GitTools Contributors +// +// ----------------------------------------------------------------------- + +namespace GitReleaseManager.Core.Templates +{ + using System.Threading.Tasks; + using GitReleaseManager.Core.Configuration; + using GitReleaseManager.Core.Helpers; + using Scriban; + using Scriban.Runtime; + + public class TemplateFactory + { + private readonly IFileSystem fileSystem; + private readonly Config config; + private readonly TemplateKind templateKind; + + public TemplateFactory(IFileSystem fileSystem, Config config, TemplateKind templateKind) + { + this.fileSystem = fileSystem; + this.config = config; + this.templateKind = templateKind; + } + + public string RenderTemplate(string templateName, object model) + { + var loader = new TemplateLoader(config, fileSystem, templateKind); + var sourcePath = loader.GetPath(null, default, templateName); + var templateContent = loader.Load(null, default, sourcePath); + + var template = Template.Parse(templateContent); + + var context = CreateTemplateContext(model, loader, sourcePath); + return template.Render(context); + } + + public async Task RenderTemplateAsync(string templateName, object model) + { + var loader = new TemplateLoader(config, fileSystem, templateKind); + var sourcePath = loader.GetPath(null, default, templateName); + var templateContent = await loader.LoadAsync(null, default, sourcePath).ConfigureAwait(false); + + var template = Template.Parse(templateContent); + + var context = CreateTemplateContext(model, loader, sourcePath); + return await template.RenderAsync(context).ConfigureAwait(false); + } + + private static TemplateContext CreateTemplateContext(object model, TemplateLoader loader, string sourcePath) + { + var sc = new ScriptObject(); + sc.Import(model); + var context = new TemplateContext + { + TemplateLoader = loader, + }; + context.PushSourceFile(sourcePath); + context.PushGlobal(sc); + return context; + } + } +} \ No newline at end of file diff --git a/src/GitReleaseManager.Core/Templates/TemplateKind.cs b/src/GitReleaseManager.Core/Templates/TemplateKind.cs new file mode 100644 index 00000000..238c5549 --- /dev/null +++ b/src/GitReleaseManager.Core/Templates/TemplateKind.cs @@ -0,0 +1,13 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) 2015 - Present - GitTools Contributors +// +// ----------------------------------------------------------------------- + +namespace GitReleaseManager.Core.Templates +{ + public enum TemplateKind + { + Create, + } +} \ No newline at end of file diff --git a/src/GitReleaseManager.Core/Templates/TemplateLoader.cs b/src/GitReleaseManager.Core/Templates/TemplateLoader.cs new file mode 100644 index 00000000..bfa1a4a9 --- /dev/null +++ b/src/GitReleaseManager.Core/Templates/TemplateLoader.cs @@ -0,0 +1,207 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) 2015 - Present - GitTools Contributors +// +// ----------------------------------------------------------------------- + +namespace GitReleaseManager.Core.Templates +{ + using System; + using System.Collections; + using System.Collections.Generic; + using System.Diagnostics; + using System.IO; + using System.Linq; + using System.Runtime.CompilerServices; + using System.Threading.Tasks; + using AutoMapper.Configuration.Annotations; + using GitReleaseManager.Core.Configuration; + using GitReleaseManager.Core.Helpers; + using Scriban; + using Scriban.Parsing; + using Scriban.Runtime; + + public class TemplateLoader : ITemplateLoader + { + private static readonly string[] _templateExtensions = new[] { ".sbn", ".scriban" }; + private readonly Config _config; + private readonly IFileSystem _fileSystem; + private readonly TemplateKind _templateKind; + + public TemplateLoader(Config config, IFileSystem fileSystem, TemplateKind templateKind) + { + _config = config; + _fileSystem = fileSystem; + _templateKind = templateKind; + } + + public string GetPath(TemplateContext context, SourceSpan callerSpan, string templateName) + { + IEnumerable possiblePaths; + if (Path.IsPathRooted(templateName)) + { + if (!Path.HasExtension(templateName)) + { + possiblePaths = _templateExtensions.Select(e => templateName + e); + } + else + { + possiblePaths = new[] { templateName }; + } + } + else + { + possiblePaths = GetFilePaths(context, _config, templateName, _templateKind); + } + + foreach (var path in possiblePaths) + { + if (_fileSystem.Exists(path)) + { + return path; + } + } + + return GetResourcePath(context, templateName, _templateKind); + } + + public string Load(TemplateContext context, SourceSpan callerSpan, string templatePath) + { + if (string.IsNullOrEmpty(templatePath)) + { + return string.Empty; + } + + if (templatePath.StartsWith(ReleaseTemplates.RESOURCE_PREFIX, StringComparison.InvariantCultureIgnoreCase)) + { + return ReleaseTemplates.LoadTemplate(templatePath); + } + + return _fileSystem.ReadAllText(templatePath); + } + + public async ValueTask LoadAsync(TemplateContext context, SourceSpan callerSpan, string templatePath) + { + var result = Load(context, callerSpan, templatePath); + + return await Task.FromResult(result).ConfigureAwait(false); + } + + private static IEnumerable GetFilePaths(TemplateContext context, Config config, string templateName, TemplateKind templateKind) + { + var extension = Path.GetExtension(templateName); + var fileName = Path.GetFileNameWithoutExtension(templateName); + + // If additional template paths is added, this need to be updated +#pragma warning disable CA1308 // Normalize strings to uppercase + string configName = templateKind.ToString().ToLowerInvariant(); +#pragma warning restore CA1308 // Normalize strings to uppercase + var possibleExtensions = new List(_templateExtensions); + + IEnumerable testPaths; + + if (!string.IsNullOrEmpty(extension)) + { + possibleExtensions.Clear(); + possibleExtensions.Add(extension); + } + else if (context != null) + { + extension = Path.GetExtension(context.CurrentSourceFile); + if (!string.IsNullOrEmpty(extension)) + { + possibleExtensions.Insert(0, extension); + } + } + + if (context is null) + { + testPaths = GetTestPaths(config.TemplatesDirectoryInfo?.FullName ?? string.Empty, configName, fileName); + } + else if (context.CurrentSourceFile.StartsWith(ReleaseTemplates.RESOURCE_PREFIX, StringComparison.InvariantCultureIgnoreCase)) + { + var currentSourceSub = context.CurrentSourceFile.Substring(ReleaseTemplates.RESOURCE_PREFIX.Length); + + // This should always be 3 items. config, template and previous resource name + var splits = currentSourceSub.Split('/'); + Debug.Assert(splits.Length == 3, "Resource current source is not a 3-part length"); + testPaths = GetTestPaths(config.TemplatesDirectoryInfo?.FullName, splits[1], splits[0], fileName); + } + else + { + string directory = directory = Path.GetDirectoryName(context.CurrentSourceFile); + + testPaths = new[] + { + Path.Combine(directory, fileName), + Path.Combine(directory, Path.GetDirectoryName(templateName), fileName), + }; + } + + return testPaths.Distinct().SelectMany(t => possibleExtensions.Select(p => t + p)); + } + + private static IEnumerable GetTestPaths(string baseDirectory, string configType, string templateName, string fileName = null) + { + if (fileName is null) + { + yield return Path.Combine(baseDirectory, templateName, configType, "index"); + yield return Path.Combine(baseDirectory, templateName, configType, templateName); + yield return Path.Combine(baseDirectory, configType, templateName, "index"); + yield return Path.Combine(baseDirectory, configType, templateName, templateName); + } + else + { + yield return Path.Combine(baseDirectory, templateName, configType, fileName); + } + + yield return Path.Combine(baseDirectory, configType, templateName, fileName ?? templateName); + + if (fileName is null) + { + yield return Path.Combine(baseDirectory, templateName, "index"); + } + + yield return Path.Combine(baseDirectory, templateName, fileName ?? templateName); + + if (fileName is null) + { + yield return Path.Combine(baseDirectory, configType, "index"); + } + yield return Path.Combine(baseDirectory, configType, fileName ?? templateName); + yield return Path.Combine(baseDirectory, fileName ?? templateName); + } + + private static string GetResourcePath(TemplateContext context, string templateName, TemplateKind templateKind) + { + var fileName = Path.GetFileNameWithoutExtension(templateName); +#pragma warning disable CA1308 // Normalize strings to uppercase + string configName = templateKind.ToString().ToLowerInvariant(); +#pragma warning restore CA1308 // Normalize strings to uppercase + + if (context is null) + { + return ReleaseTemplates.RESOURCE_PREFIX + string.Join("/", new[] { fileName, configName, "index" }); + } + else + { + var directory = Path.GetDirectoryName(context.CurrentSourceFile); + var directoryName = Path.GetFileNameWithoutExtension(directory); + if (Enum.TryParse(directoryName, true, out _)) + { + directory = Path.GetDirectoryName(directory); + directoryName = Path.GetFileNameWithoutExtension(directory); + } + + if (string.IsNullOrWhiteSpace(directoryName)) + { + throw new FileNotFoundException($"No file named '{fileName}' was found!"); + } + + return ReleaseTemplates.RESOURCE_PREFIX + string.Join( + "/", + new[] { directoryName, configName, fileName }); + } + } + } +} \ No newline at end of file diff --git a/src/GitReleaseManager.Core/VcsService.cs b/src/GitReleaseManager.Core/VcsService.cs index 23e49f5b..b5633b16 100644 --- a/src/GitReleaseManager.Core/VcsService.cs +++ b/src/GitReleaseManager.Core/VcsService.cs @@ -20,6 +20,7 @@ namespace GitReleaseManager.Core using GitReleaseManager.Core.Model; using GitReleaseManager.Core.Provider; using GitReleaseManager.Core.ReleaseNotes; + using GitReleaseManager.Core.Templates; using Serilog; public class VcsService : IVcsService @@ -44,31 +45,14 @@ public VcsService(IVcsProvider vcsProvider, ILogger logger, IReleaseNotesBuilder public async Task CreateReleaseFromMilestoneAsync(string owner, string repository, string milestone, string releaseName, string targetCommitish, IList assets, bool prerelease, string templateFilePath) { - var templateText = ReleaseNotesTemplate.Default; + var templatePath = ReleaseTemplates.DEFAULT_NAME; if (!string.IsNullOrWhiteSpace(templateFilePath)) { - if (File.Exists(templateFilePath)) - { - var templateFileContent = File.ReadAllText(templateFilePath); - - if (string.IsNullOrWhiteSpace(templateFileContent)) - { - throw new ArgumentException("The release notes template cannot be empty."); - } - - templateText = templateFileContent; - } - else - { - var fileName = Path.GetFileName(templateFilePath); - var message = $"The release notes template file '{templateFilePath}' could not be found."; - - throw new FileNotFoundException(message, fileName); - } + templatePath = templateFilePath; } - var releaseNotes = await _releaseNotesBuilder.BuildReleaseNotes(owner, repository, milestone, templateText).ConfigureAwait(false); + var releaseNotes = await _releaseNotesBuilder.BuildReleaseNotes(owner, repository, milestone, templatePath).ConfigureAwait(false); var release = await CreateRelease(owner, repository, releaseName, milestone, releaseNotes, prerelease, targetCommitish, assets).ConfigureAwait(false); return release; diff --git a/src/GitReleaseManager.IntegrationTests/ReleaseNotesBuilderIntegrationTests.cs b/src/GitReleaseManager.IntegrationTests/ReleaseNotesBuilderIntegrationTests.cs index 247d1932..823f9240 100644 --- a/src/GitReleaseManager.IntegrationTests/ReleaseNotesBuilderIntegrationTests.cs +++ b/src/GitReleaseManager.IntegrationTests/ReleaseNotesBuilderIntegrationTests.cs @@ -15,6 +15,7 @@ namespace GitReleaseManager.IntegrationTests using GitReleaseManager.Core.Helpers; using GitReleaseManager.Core.Provider; using GitReleaseManager.Core.ReleaseNotes; + using GitReleaseManager.Core.Templates; using NUnit.Framework; using Octokit; using Serilog; @@ -62,8 +63,8 @@ public async Task SingleMilestone() var configuration = ConfigurationProvider.Provide(currentDirectory, fileSystem); var vcsProvider = new GitHubProvider(_gitHubClient, _mapper); - var releaseNotesBuilder = new ReleaseNotesBuilder(vcsProvider, _logger, configuration); - var result = await releaseNotesBuilder.BuildReleaseNotes("Chocolatey", "ChocolateyGUI", "0.12.4", ReleaseNotesTemplate.Default).ConfigureAwait(false); + var releaseNotesBuilder = new ReleaseNotesBuilder(vcsProvider, _logger, fileSystem, configuration, new TemplateFactory(fileSystem, configuration, TemplateKind.Create)); + var result = await releaseNotesBuilder.BuildReleaseNotes("Chocolatey", "ChocolateyGUI", "0.12.4", ReleaseTemplates.DEFAULT_NAME).ConfigureAwait(false); Debug.WriteLine(result); ClipBoardHelper.SetClipboard(result); } @@ -84,8 +85,8 @@ public async Task SingleMilestone3() var configuration = ConfigurationProvider.Provide(currentDirectory, fileSystem); var vcsProvider = new GitHubProvider(_gitHubClient, _mapper); - var releaseNotesBuilder = new ReleaseNotesBuilder(vcsProvider, _logger, configuration); - var result = await releaseNotesBuilder.BuildReleaseNotes("Chocolatey", "ChocolateyGUI", "0.13.0", ReleaseNotesTemplate.Default).ConfigureAwait(false); + var releaseNotesBuilder = new ReleaseNotesBuilder(vcsProvider, _logger, fileSystem, configuration, new TemplateFactory(fileSystem, configuration, TemplateKind.Create)); + var result = await releaseNotesBuilder.BuildReleaseNotes("Chocolatey", "ChocolateyGUI", "0.13.0", ReleaseTemplates.DEFAULT_NAME).ConfigureAwait(false); Debug.WriteLine(result); ClipBoardHelper.SetClipboard(result); } diff --git a/src/GitReleaseManager.Tests/ReleaseNotesBuilderTests.cs b/src/GitReleaseManager.Tests/ReleaseNotesBuilderTests.cs index ece0c875..f3cf5df5 100644 --- a/src/GitReleaseManager.Tests/ReleaseNotesBuilderTests.cs +++ b/src/GitReleaseManager.Tests/ReleaseNotesBuilderTests.cs @@ -16,6 +16,7 @@ namespace GitReleaseManager.Tests using GitReleaseManager.Core.Model; using GitReleaseManager.Core.Provider; using GitReleaseManager.Core.ReleaseNotes; + using GitReleaseManager.Core.Templates; using NSubstitute; using NUnit.Framework; using Serilog; @@ -183,8 +184,8 @@ private static void AcceptTest(int commits, Config config, params Issue[] issues vcsProvider.GetMilestonesAsync(owner, repository, Arg.Any()) .Returns(Task.FromResult((IEnumerable)vcsService.Milestones)); - var builder = new ReleaseNotesBuilder(vcsProvider, logger, configuration); - var notes = builder.BuildReleaseNotes(owner, repository, milestoneTitle, ReleaseNotesTemplate.Default).Result; + var builder = new ReleaseNotesBuilder(vcsProvider, logger, fileSystem, configuration, new TemplateFactory(fileSystem, configuration, TemplateKind.Create)); + var notes = builder.BuildReleaseNotes(owner, repository, milestoneTitle, ReleaseTemplates.DEFAULT_NAME).Result; Approvals.Verify(notes); } From 5ff5cb815ab79feeb1da68a15c4f924e099d9dac Mon Sep 17 00:00:00 2001 From: AdmiringWorm Date: Wed, 16 Sep 2020 05:33:13 +0200 Subject: [PATCH 03/12] (GH-217) Rename TemplateFilePath option to Template The help text for the option have also been updated to mention updated template provider. --- .../Commands/CreateCommandTests.cs | 4 ++-- src/GitReleaseManager.Core/Commands/CreateCommand.cs | 2 +- src/GitReleaseManager.Core/Options/CreateSubOptions.cs | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/GitReleaseManager.Core.Tests/Commands/CreateCommandTests.cs b/src/GitReleaseManager.Core.Tests/Commands/CreateCommandTests.cs index 8029b764..fff2bcd3 100644 --- a/src/GitReleaseManager.Core.Tests/Commands/CreateCommandTests.cs +++ b/src/GitReleaseManager.Core.Tests/Commands/CreateCommandTests.cs @@ -50,13 +50,13 @@ public async Task Should_Create_Release_From_Milestone(string name, int logVerbo var releaseName = options.Name ?? options.Milestone; - _vcsService.CreateReleaseFromMilestoneAsync(options.RepositoryOwner, options.RepositoryName, options.Milestone, releaseName, options.TargetCommitish, options.AssetPaths, options.Prerelease, options.TemplateFilePath) + _vcsService.CreateReleaseFromMilestoneAsync(options.RepositoryOwner, options.RepositoryName, options.Milestone, releaseName, options.TargetCommitish, options.AssetPaths, options.Prerelease, options.Template) .Returns(_release); var result = await _command.Execute(options).ConfigureAwait(false); result.ShouldBe(0); - await _vcsService.Received(1).CreateReleaseFromMilestoneAsync(options.RepositoryOwner, options.RepositoryName, options.Milestone, releaseName, options.TargetCommitish, options.AssetPaths, options.Prerelease, options.TemplateFilePath).ConfigureAwait(false); + await _vcsService.Received(1).CreateReleaseFromMilestoneAsync(options.RepositoryOwner, options.RepositoryName, options.Milestone, releaseName, options.TargetCommitish, options.AssetPaths, options.Prerelease, options.Template).ConfigureAwait(false); _logger.Received(1).Information(Arg.Any()); _logger.Received(logVerboseCount).Verbose(Arg.Any(), options.Milestone); _logger.Received(1).Information(Arg.Any(), _release.HtmlUrl); diff --git a/src/GitReleaseManager.Core/Commands/CreateCommand.cs b/src/GitReleaseManager.Core/Commands/CreateCommand.cs index d9c535f8..f04a0cea 100644 --- a/src/GitReleaseManager.Core/Commands/CreateCommand.cs +++ b/src/GitReleaseManager.Core/Commands/CreateCommand.cs @@ -39,7 +39,7 @@ public async Task Execute(CreateSubOptions options) releaseName = options.Milestone; } - release = await _vcsService.CreateReleaseFromMilestoneAsync(options.RepositoryOwner, options.RepositoryName, options.Milestone, releaseName, options.TargetCommitish, options.AssetPaths, options.Prerelease, options.TemplateFilePath).ConfigureAwait(false); + release = await _vcsService.CreateReleaseFromMilestoneAsync(options.RepositoryOwner, options.RepositoryName, options.Milestone, releaseName, options.TargetCommitish, options.AssetPaths, options.Prerelease, options.Template).ConfigureAwait(false); } else { diff --git a/src/GitReleaseManager.Core/Options/CreateSubOptions.cs b/src/GitReleaseManager.Core/Options/CreateSubOptions.cs index 7d67018e..03881f20 100644 --- a/src/GitReleaseManager.Core/Options/CreateSubOptions.cs +++ b/src/GitReleaseManager.Core/Options/CreateSubOptions.cs @@ -27,8 +27,8 @@ public class CreateSubOptions : BaseVcsOptions [Option('i', "inputFilePath", HelpText = "The path to the file to be used as the content of the release notes.", Required = false)] public string InputFilePath { get; set; } - [Option('t', "template", HelpText = "The path to the file to be used as the template for the release notes.", Required = false)] - public string TemplateFilePath { get; set; } + [Option('t', "template", HelpText = "The name of the template file to use. Can also be a relative or absolute path (relative paths are resolved from yaml template-dir configuration). Defaults to 'default'")] + public string Template { get; set; } [Option('e', "pre", Required = false, HelpText = "Creates the release as a pre-release.")] public bool Prerelease { get; set; } From 9a383a261d7ce0195f6b796d849eb716e1af8e4e Mon Sep 17 00:00:00 2001 From: AdmiringWorm Date: Wed, 16 Sep 2020 06:04:21 +0200 Subject: [PATCH 04/12] (maint) Update FileSystem class to resolve relative paths --- src/GitReleaseManager.Cli/Program.cs | 2 +- .../Commands/InitCommandTests.cs | 2 +- .../Templates/TemplateLoaderTests.cs | 18 +++++ .../Configuration/Config.cs | 9 +-- .../Helpers/FileSystem.cs | 19 +++++ .../Helpers/IFileSystem.cs | 2 + .../Templates/TemplateLoader.cs | 74 +++++++++---------- .../ReleaseNotesBuilderIntegrationTests.cs | 5 +- .../ReleaseNotesBuilderTests.cs | 2 +- .../ReleaseNotesExporterTests.cs | 2 +- 10 files changed, 83 insertions(+), 52 deletions(-) diff --git a/src/GitReleaseManager.Cli/Program.cs b/src/GitReleaseManager.Cli/Program.cs index a02105be..90fb22eb 100644 --- a/src/GitReleaseManager.Cli/Program.cs +++ b/src/GitReleaseManager.Cli/Program.cs @@ -79,7 +79,7 @@ private static async Task Main(string[] args) private static void RegisterServices(BaseSubOptions options) { - var fileSystem = new FileSystem(); + var fileSystem = new FileSystem(options); var logger = Log.ForContext(); var mapper = AutoMapperConfiguration.Configure(); var configuration = ConfigurationProvider.Provide(options.TargetDirectory ?? Environment.CurrentDirectory, fileSystem); diff --git a/src/GitReleaseManager.Core.Tests/Commands/InitCommandTests.cs b/src/GitReleaseManager.Core.Tests/Commands/InitCommandTests.cs index 58591f66..4a19ee28 100644 --- a/src/GitReleaseManager.Core.Tests/Commands/InitCommandTests.cs +++ b/src/GitReleaseManager.Core.Tests/Commands/InitCommandTests.cs @@ -27,7 +27,7 @@ public class InitCommandTests [SetUp] public void Setup() { - _fileSystem = new FileSystem(); + _fileSystem = new FileSystem(Substitute.For()); _logger = Substitute.For(); _command = new InitCommand(_fileSystem, _logger); _targetDirectory = Path.GetTempPath(); diff --git a/src/GitReleaseManager.Core.Tests/Templates/TemplateLoaderTests.cs b/src/GitReleaseManager.Core.Tests/Templates/TemplateLoaderTests.cs index 8deab9e2..3de245b1 100644 --- a/src/GitReleaseManager.Core.Tests/Templates/TemplateLoaderTests.cs +++ b/src/GitReleaseManager.Core.Tests/Templates/TemplateLoaderTests.cs @@ -116,6 +116,7 @@ public static IEnumerable ResourceTemplateLoading public void Should_GetDefaultResourcePath() { var fileSystem = Substitute.For(); + fileSystem.ResolvePath(Arg.Any()).Returns(s => Path.Combine(Environment.CurrentDirectory, s.Arg())); fileSystem.Exists(Arg.Any()).Returns(false); var config = new Config(); var loader = new TemplateLoader(config, fileSystem, TemplateKind.Create); @@ -129,6 +130,7 @@ public void Should_GetDefaultResourcePath() public void Should_GetRelativeResourcePath() { var fileSystem = Substitute.For(); + fileSystem.ResolvePath(Arg.Any()).Returns(s => Path.Combine(Environment.CurrentDirectory, s.Arg())); fileSystem.Exists(Arg.Any()).Returns(false); var config = new Config(); var loader = new TemplateLoader(config, fileSystem, TemplateKind.Create); @@ -146,6 +148,7 @@ public void Should_GetFullIndexPathWhenFileExists(string expectedFileName) { var expectedFile = Path.Combine(Environment.CurrentDirectory, ".templates", "default", "create", expectedFileName); var fileSystem = Substitute.For(); + fileSystem.ResolvePath(Arg.Any()).Returns(s => Path.Combine(Environment.CurrentDirectory, s.Arg())); fileSystem.Exists(Arg.Any()).Returns(false); fileSystem.Exists(expectedFile).Returns(true); @@ -161,6 +164,7 @@ public void Should_GetRelativeFileWhenExists(string expectedFileName) { var expectedFile = Path.Combine(Environment.CurrentDirectory, ".templates", "default", "create", expectedFileName); var fileSystem = Substitute.For(); + fileSystem.ResolvePath(Arg.Any()).Returns(s => Path.Combine(Environment.CurrentDirectory, s.Arg())); fileSystem.Exists(Arg.Any()).Returns(false); fileSystem.Exists(expectedFile).Returns(true); var context = new TemplateContext(); @@ -179,6 +183,7 @@ public void Should_GetFullIndexPathWhenExtensionIsUsedAndFileExists(string exten { var expectedFile = Path.Combine(Environment.CurrentDirectory, ".templates", "default", "create", "index." + extension); var fileSystem = Substitute.For(); + fileSystem.ResolvePath(Arg.Any()).Returns(s => Path.Combine(Environment.CurrentDirectory, s.Arg())); fileSystem.Exists(Arg.Any()).Returns(false); fileSystem.Exists(expectedFile).Returns(true); @@ -195,6 +200,7 @@ public void Should_GetRelativeWhenExtensionIsUsedAndFileWhenExists(string extens { var expectedFile = Path.Combine(Environment.CurrentDirectory, ".templates", "default", "create", "test-file." + extension); var fileSystem = Substitute.For(); + fileSystem.ResolvePath(Arg.Any()).Returns(s => Path.Combine(Environment.CurrentDirectory, s.Arg())); fileSystem.Exists(Arg.Any()).Returns(false); fileSystem.Exists(expectedFile).Returns(true); var context = new TemplateContext(); @@ -211,6 +217,7 @@ public void Should_GetFilePathWhenPreviousSourceWasResourcefile() { var expectedFile = Path.Combine(Environment.CurrentDirectory, ".templates", "default", "create", "releases.sbn"); var fileSystem = Substitute.For(); + fileSystem.ResolvePath(Arg.Any()).Returns(s => Path.Combine(Environment.CurrentDirectory, s.Arg())); fileSystem.Exists(Arg.Any()).Returns(false); fileSystem.Exists(expectedFile).Returns(true); var context = new TemplateContext(); @@ -227,6 +234,7 @@ public void Should_GetResourcePathWhenPreviousSourceWasFile(string sourcePath) { var expected = ReleaseTemplates.RESOURCE_PREFIX + "default/create/note"; var fileSystem = Substitute.For(); + fileSystem.ResolvePath(Arg.Any()).Returns(s => Path.Combine(Environment.CurrentDirectory, s.Arg())); fileSystem.Exists(Arg.Any()).Returns(false); var context = new TemplateContext(); context.PushSourceFile(sourcePath); @@ -241,6 +249,7 @@ public void Should_GetResourcePathWhenPreviousSourceWasFile(string sourcePath) public void Should_ThrowFileNotFoundExceptionOnInvalidFilePathsForResourceFallback(string sourcePath) { var fileSystem = Substitute.For(); + fileSystem.ResolvePath(Arg.Any()).Returns(s => Path.Combine(Environment.CurrentDirectory, s.Arg())); fileSystem.Exists(Arg.Any()).Returns(false); var context = new TemplateContext(); context.PushSourceFile(sourcePath); @@ -255,6 +264,7 @@ public void Should_GetExistingFileExtensionAsPreviousFile() { var expectedFile = Path.Combine(Environment.CurrentDirectory, ".templates", "xml", "create", "release.xml"); var fileSystem = Substitute.For(); + fileSystem.ResolvePath(Arg.Any()).Returns(s => Path.Combine(Environment.CurrentDirectory, s.Arg())); fileSystem.Exists(Arg.Any()).Returns(false); fileSystem.Exists(expectedFile).Returns(true); var context = new TemplateContext(); @@ -270,6 +280,7 @@ public void Should_GetExistingFileExtensionAsPreviousFile() public void Should_GetExpectedSourcePaths(string expectedFile) { var fileSystem = Substitute.For(); + fileSystem.ResolvePath(Arg.Any()).Returns(s => Path.Combine(Environment.CurrentDirectory, s.Arg())); fileSystem.Exists(Arg.Any()).Returns(false); fileSystem.Exists(expectedFile).Returns(true); @@ -283,6 +294,7 @@ public void Should_GetExpectedSourcePaths(string expectedFile) public void Should_GetExpectedRelativePathsWhenPreviousIsResource(string expectedFile) { var fileSystem = Substitute.For(); + fileSystem.ResolvePath(Arg.Any()).Returns(s => Path.Combine(Environment.CurrentDirectory, s.Arg())); fileSystem.Exists(Arg.Any()).Returns(false); fileSystem.Exists(expectedFile).Returns(true); var context = new TemplateContext(); @@ -299,6 +311,7 @@ public void Should_GetValidPathFromAbsolutePath() { var expectedFile = Path.Combine(Environment.CurrentDirectory, "test-file.md"); var fileSystem = Substitute.For(); + fileSystem.ResolvePath(Arg.Any()).Returns(s => Path.Combine(Environment.CurrentDirectory, s.Arg())); fileSystem.Exists(Arg.Any()).Returns(false); fileSystem.Exists(expectedFile).Returns(true); @@ -314,6 +327,7 @@ public void Should_GetValidPathFromAbsolutePathWithoutExtension(string extension { var expectedFile = Path.Combine(Environment.CurrentDirectory, "test-file." + extension); var fileSystem = Substitute.For(); + fileSystem.ResolvePath(Arg.Any()).Returns(s => Path.Combine(Environment.CurrentDirectory, s.Arg())); fileSystem.Exists(Arg.Any()).Returns(false); fileSystem.Exists(expectedFile).Returns(true); @@ -327,6 +341,7 @@ public void Should_GetValidPathFromAbsolutePathWithoutExtension(string extension public void Should_LoadReleaseTemplateFromResource(string key, string expected) { var fileSystem = Substitute.For(); + fileSystem.ResolvePath(Arg.Any()).Returns(s => Path.Combine(Environment.CurrentDirectory, s.Arg())); var loader = new TemplateLoader(new Config(), fileSystem, TemplateKind.Create); var result = loader.Load(null, default, ReleaseTemplates.RESOURCE_PREFIX + key); @@ -338,6 +353,7 @@ public void Should_LoadReleaseTemplateFromResource(string key, string expected) public void Should_ThrowExceptionOnInvalidResource() { var fileSystem = Substitute.For(); + fileSystem.ResolvePath(Arg.Any()).Returns(s => Path.Combine(Environment.CurrentDirectory, s.Arg())); var loader = new TemplateLoader(new Config(), fileSystem, TemplateKind.Create); @@ -350,6 +366,7 @@ public void Should_ThrowExceptionOnInvalidResource() public void Should_ReturnEmptyTextWhenPathIsEmpty(string path) { var fileSystem = Substitute.For(); + fileSystem.ResolvePath(Arg.Any()).Returns(s => Path.Combine(Environment.CurrentDirectory, s.Arg())); var loader = new TemplateLoader(new Config(), fileSystem, TemplateKind.Create); var result = loader.Load(null, default, path); @@ -363,6 +380,7 @@ public void Should_LoadAllTextFromFilePath() var expected = "I WAS Loaded!!!"; var testPath = Path.Combine(Environment.CurrentDirectory, "test.sbn"); var fileSystem = Substitute.For(); + fileSystem.ResolvePath(Arg.Any()).Returns(s => Path.Combine(Environment.CurrentDirectory, s.Arg())); fileSystem.ReadAllText(testPath).Returns(expected); var loader = new TemplateLoader(new Config(), fileSystem, TemplateKind.Create); diff --git a/src/GitReleaseManager.Core/Configuration/Config.cs b/src/GitReleaseManager.Core/Configuration/Config.cs index 46b16bcf..ffc774af 100644 --- a/src/GitReleaseManager.Core/Configuration/Config.cs +++ b/src/GitReleaseManager.Core/Configuration/Config.cs @@ -144,14 +144,7 @@ public Config() [Description("The directory where templates are located")] [YamlMember(Alias = "templates-dir")] - public string TemplatesDirectory - { - get => TemplatesDirectoryInfo.FullName.Replace(Environment.CurrentDirectory, string.Empty).TrimStart('\\', '/'); - set => TemplatesDirectoryInfo = new DirectoryInfo(Path.Combine(Environment.CurrentDirectory, value)); - } - - [YamlIgnore] - public DirectoryInfo TemplatesDirectoryInfo { get; private set; } + public string TemplatesDirectory { get; set; } [Description("Configuration values used when creating new releases")] [YamlMember(Alias = "create")] diff --git a/src/GitReleaseManager.Core/Helpers/FileSystem.cs b/src/GitReleaseManager.Core/Helpers/FileSystem.cs index 16979390..ba91d931 100644 --- a/src/GitReleaseManager.Core/Helpers/FileSystem.cs +++ b/src/GitReleaseManager.Core/Helpers/FileSystem.cs @@ -6,11 +6,20 @@ namespace GitReleaseManager.Core.Helpers { + using System; using System.Collections.Generic; using System.IO; + using GitReleaseManager.Core.Options; public class FileSystem : IFileSystem { + private readonly BaseSubOptions options; + + public FileSystem(BaseSubOptions options) + { + this.options = options; + } + public void Copy(string @source, string destination, bool overwrite) { File.Copy(@source, destination, overwrite); @@ -31,6 +40,16 @@ public void Delete(string path) File.Delete(path); } + public string ResolvePath(string path) + { + if (Path.IsPathRooted(path)) + { + return path; + } + + return Path.Combine(options.TargetDirectory ?? Environment.CurrentDirectory, path); + } + public string ReadAllText(string path) { return File.ReadAllText(path); diff --git a/src/GitReleaseManager.Core/Helpers/IFileSystem.cs b/src/GitReleaseManager.Core/Helpers/IFileSystem.cs index f6292622..58ed3439 100644 --- a/src/GitReleaseManager.Core/Helpers/IFileSystem.cs +++ b/src/GitReleaseManager.Core/Helpers/IFileSystem.cs @@ -21,6 +21,8 @@ public interface IFileSystem string ReadAllText(string path); + string ResolvePath(string path); + void WriteAllText(string file, string fileContents); IEnumerable DirectoryGetFiles(string directory, string searchPattern, SearchOption searchOption); diff --git a/src/GitReleaseManager.Core/Templates/TemplateLoader.cs b/src/GitReleaseManager.Core/Templates/TemplateLoader.cs index bfa1a4a9..b06dc396 100644 --- a/src/GitReleaseManager.Core/Templates/TemplateLoader.cs +++ b/src/GitReleaseManager.Core/Templates/TemplateLoader.cs @@ -7,14 +7,11 @@ namespace GitReleaseManager.Core.Templates { using System; - using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; - using System.Runtime.CompilerServices; using System.Threading.Tasks; - using AutoMapper.Configuration.Annotations; using GitReleaseManager.Core.Configuration; using GitReleaseManager.Core.Helpers; using Scriban; @@ -51,7 +48,7 @@ public string GetPath(TemplateContext context, SourceSpan callerSpan, string tem } else { - possiblePaths = GetFilePaths(context, _config, templateName, _templateKind); + possiblePaths = GetFilePaths(context, _fileSystem.ResolvePath(_config.TemplatesDirectory), templateName, _templateKind); } foreach (var path in possiblePaths) @@ -87,7 +84,7 @@ public async ValueTask LoadAsync(TemplateContext context, SourceSpan cal return await Task.FromResult(result).ConfigureAwait(false); } - private static IEnumerable GetFilePaths(TemplateContext context, Config config, string templateName, TemplateKind templateKind) + private static IEnumerable GetFilePaths(TemplateContext context, string templatesDirectory, string templateName, TemplateKind templateKind) { var extension = Path.GetExtension(templateName); var fileName = Path.GetFileNameWithoutExtension(templateName); @@ -116,7 +113,7 @@ private static IEnumerable GetFilePaths(TemplateContext context, Config if (context is null) { - testPaths = GetTestPaths(config.TemplatesDirectoryInfo?.FullName ?? string.Empty, configName, fileName); + testPaths = GetTestPaths(templatesDirectory, configName, fileName); } else if (context.CurrentSourceFile.StartsWith(ReleaseTemplates.RESOURCE_PREFIX, StringComparison.InvariantCultureIgnoreCase)) { @@ -125,7 +122,7 @@ private static IEnumerable GetFilePaths(TemplateContext context, Config // This should always be 3 items. config, template and previous resource name var splits = currentSourceSub.Split('/'); Debug.Assert(splits.Length == 3, "Resource current source is not a 3-part length"); - testPaths = GetTestPaths(config.TemplatesDirectoryInfo?.FullName, splits[1], splits[0], fileName); + testPaths = GetTestPaths(templatesDirectory, splits[1], splits[0], fileName); } else { @@ -141,37 +138,6 @@ private static IEnumerable GetFilePaths(TemplateContext context, Config return testPaths.Distinct().SelectMany(t => possibleExtensions.Select(p => t + p)); } - private static IEnumerable GetTestPaths(string baseDirectory, string configType, string templateName, string fileName = null) - { - if (fileName is null) - { - yield return Path.Combine(baseDirectory, templateName, configType, "index"); - yield return Path.Combine(baseDirectory, templateName, configType, templateName); - yield return Path.Combine(baseDirectory, configType, templateName, "index"); - yield return Path.Combine(baseDirectory, configType, templateName, templateName); - } - else - { - yield return Path.Combine(baseDirectory, templateName, configType, fileName); - } - - yield return Path.Combine(baseDirectory, configType, templateName, fileName ?? templateName); - - if (fileName is null) - { - yield return Path.Combine(baseDirectory, templateName, "index"); - } - - yield return Path.Combine(baseDirectory, templateName, fileName ?? templateName); - - if (fileName is null) - { - yield return Path.Combine(baseDirectory, configType, "index"); - } - yield return Path.Combine(baseDirectory, configType, fileName ?? templateName); - yield return Path.Combine(baseDirectory, fileName ?? templateName); - } - private static string GetResourcePath(TemplateContext context, string templateName, TemplateKind templateKind) { var fileName = Path.GetFileNameWithoutExtension(templateName); @@ -203,5 +169,37 @@ private static string GetResourcePath(TemplateContext context, string templateNa new[] { directoryName, configName, fileName }); } } + + private static IEnumerable GetTestPaths(string baseDirectory, string configType, string templateName, string fileName = null) + { + if (fileName is null) + { + yield return Path.Combine(baseDirectory, templateName, configType, "index"); + yield return Path.Combine(baseDirectory, templateName, configType, templateName); + yield return Path.Combine(baseDirectory, configType, templateName, "index"); + yield return Path.Combine(baseDirectory, configType, templateName, templateName); + } + else + { + yield return Path.Combine(baseDirectory, templateName, configType, fileName); + } + + yield return Path.Combine(baseDirectory, configType, templateName, fileName ?? templateName); + + if (fileName is null) + { + yield return Path.Combine(baseDirectory, templateName, "index"); + } + + yield return Path.Combine(baseDirectory, templateName, fileName ?? templateName); + + if (fileName is null) + { + yield return Path.Combine(baseDirectory, configType, "index"); + } + + yield return Path.Combine(baseDirectory, configType, fileName ?? templateName); + yield return Path.Combine(baseDirectory, fileName ?? templateName); + } } } \ No newline at end of file diff --git a/src/GitReleaseManager.IntegrationTests/ReleaseNotesBuilderIntegrationTests.cs b/src/GitReleaseManager.IntegrationTests/ReleaseNotesBuilderIntegrationTests.cs index 823f9240..257abdc8 100644 --- a/src/GitReleaseManager.IntegrationTests/ReleaseNotesBuilderIntegrationTests.cs +++ b/src/GitReleaseManager.IntegrationTests/ReleaseNotesBuilderIntegrationTests.cs @@ -13,6 +13,7 @@ namespace GitReleaseManager.IntegrationTests using GitReleaseManager.Core; using GitReleaseManager.Core.Configuration; using GitReleaseManager.Core.Helpers; + using GitReleaseManager.Core.Options; using GitReleaseManager.Core.Provider; using GitReleaseManager.Core.ReleaseNotes; using GitReleaseManager.Core.Templates; @@ -58,7 +59,7 @@ public async Task SingleMilestone() } else { - var fileSystem = new FileSystem(); + var fileSystem = new FileSystem(new CreateSubOptions()); var currentDirectory = Environment.CurrentDirectory; var configuration = ConfigurationProvider.Provide(currentDirectory, fileSystem); @@ -80,7 +81,7 @@ public async Task SingleMilestone3() } else { - var fileSystem = new FileSystem(); + var fileSystem = new FileSystem(new CreateSubOptions()); var currentDirectory = Environment.CurrentDirectory; var configuration = ConfigurationProvider.Provide(currentDirectory, fileSystem); diff --git a/src/GitReleaseManager.Tests/ReleaseNotesBuilderTests.cs b/src/GitReleaseManager.Tests/ReleaseNotesBuilderTests.cs index f3cf5df5..dea72c92 100644 --- a/src/GitReleaseManager.Tests/ReleaseNotesBuilderTests.cs +++ b/src/GitReleaseManager.Tests/ReleaseNotesBuilderTests.cs @@ -158,7 +158,7 @@ private static void AcceptTest(int commits, Config config, params Issue[] issues var vcsService = new VcsServiceMock(); var logger = Substitute.For(); - var fileSystem = new FileSystem(); + var fileSystem = Substitute.For(); var currentDirectory = Environment.CurrentDirectory; var configuration = config ?? ConfigurationProvider.Provide(currentDirectory, fileSystem); diff --git a/src/GitReleaseManager.Tests/ReleaseNotesExporterTests.cs b/src/GitReleaseManager.Tests/ReleaseNotesExporterTests.cs index 3d3ffa9a..0a056611 100644 --- a/src/GitReleaseManager.Tests/ReleaseNotesExporterTests.cs +++ b/src/GitReleaseManager.Tests/ReleaseNotesExporterTests.cs @@ -20,7 +20,7 @@ namespace GitReleaseManager.Tests [TestFixture] public class ReleaseNotesExporterTests { - private readonly FileSystem _fileSystem = new FileSystem(); + private readonly IFileSystem _fileSystem = Substitute.For(); private readonly string _currentDirectory = Environment.CurrentDirectory; [Test] From e4bee05b627649c150fa67c9689d4060ca1ac9e2 Mon Sep 17 00:00:00 2001 From: AdmiringWorm Date: Wed, 16 Sep 2020 06:04:47 +0200 Subject: [PATCH 05/12] (GH-217) Add option to extract embedded templates --- .../Commands/InitCommand.cs | 43 ++++++++++++++++++- .../Options/InitSubOptions.cs | 2 + 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/src/GitReleaseManager.Core/Commands/InitCommand.cs b/src/GitReleaseManager.Core/Commands/InitCommand.cs index c701e42a..ffb530af 100644 --- a/src/GitReleaseManager.Core/Commands/InitCommand.cs +++ b/src/GitReleaseManager.Core/Commands/InitCommand.cs @@ -7,10 +7,12 @@ namespace GitReleaseManager.Core.Commands { using System; + using System.IO; using System.Threading.Tasks; using GitReleaseManager.Core.Configuration; using GitReleaseManager.Core.Helpers; using GitReleaseManager.Core.Options; + using GitReleaseManager.Core.Templates; using Serilog; public class InitCommand : ICommand @@ -26,9 +28,46 @@ public InitCommand(IFileSystem fileSystem, ILogger logger) public Task Execute(InitSubOptions options) { - _logger.Information("Creating sample configuration file"); var directory = options.TargetDirectory ?? Environment.CurrentDirectory; - ConfigurationProvider.WriteSample(directory, _fileSystem); + + if (!options.ExtractTemplates) + { + _logger.Information("Creating sample configuration file"); + ConfigurationProvider.WriteSample(directory, _fileSystem); + } + else + { + _logger.Information("Extracting Embedded templates"); + + var templates = ReleaseTemplates.GetExportTemplates(); + var config = ConfigurationProvider.Provide(directory, _fileSystem); + var templatesDirectory = new DirectoryInfo(_fileSystem.ResolvePath(config.TemplatesDirectory)); + + if (!templatesDirectory.Exists) + { + templatesDirectory.Create(); + } + + foreach (var template in templates) + { + var fullFilePath = Path.Combine(templatesDirectory.FullName, template.Key); + var templateDir = Path.GetDirectoryName(fullFilePath); + if (!Directory.Exists(templateDir)) + { + Directory.CreateDirectory(templateDir); + } + else if (File.Exists(fullFilePath)) + { + _logger.Warning("File '{FilePath}' already exist. Skipping file.", template.Key); + continue; + } + + _logger.Information("Creating new file '{FilePath}'!", template.Key); + _fileSystem.WriteAllText(fullFilePath, template.Value); + } + + _logger.Information("All embedded templates has been extracted!"); + } return Task.FromResult(0); } diff --git a/src/GitReleaseManager.Core/Options/InitSubOptions.cs b/src/GitReleaseManager.Core/Options/InitSubOptions.cs index b3aca364..c7c41214 100644 --- a/src/GitReleaseManager.Core/Options/InitSubOptions.cs +++ b/src/GitReleaseManager.Core/Options/InitSubOptions.cs @@ -11,5 +11,7 @@ namespace GitReleaseManager.Core.Options [Verb("init", HelpText = "Creates a sample Yaml Configuration file in root directory")] public class InitSubOptions : BaseSubOptions { + [Option("templates", HelpText = "Extract the embedded templates to allow modification", Required = false)] + public bool ExtractTemplates { get; set; } } } \ No newline at end of file From 1d8c098c194ea6867776d35ea468c1e52b9ef128 Mon Sep 17 00:00:00 2001 From: AdmiringWorm Date: Wed, 16 Sep 2020 11:52:14 +0200 Subject: [PATCH 06/12] (GH-217) Improve template handling This includes both updated logic for the templates, as well as better whitespace handling that have plagued (IMO) the release notes for a long time. --- .../Extensions/StringExtensions.cs | 10 ++++ .../ReleaseNotes/ReleaseNotesBuilder.cs | 40 ++++++-------- .../Templates/TemplateFactory.cs | 6 ++- .../Templates/default/create/footer.sbn | 14 +++-- .../Templates/default/index.sbn | 15 ++++-- .../Templates/default/issue-details.sbn | 6 +-- .../Templates/default/issues.sbn | 7 +-- .../Templates/default/milestone.sbn | 3 +- .../Templates/default/release-info.sbn | 19 ++++--- ...rTests.CorrectlyExcludeIssues.approved.txt | 2 - ...CorrectlyUseFooterWhenEnabled.approved.txt | 9 ++++ ...ooterWithMilestoneWhenEnabled.approved.txt | 9 ++++ ...uilderTests.NoCommitsNoIssues.approved.txt | 3 -- ...Tests.NoCommitsSingularIssues.approved.txt | 2 - ...lderTests.NoCommitsSomeIssues.approved.txt | 2 - ...Tests.SingularCommitsNoIssues.approved.txt | 2 - ...SingularCommitsSingularIssues.approved.txt | 2 - ...sts.SingularCommitsSomeIssues.approved.txt | 2 - ...arCommitsWithHeaderLabelAlias.approved.txt | 2 - ...mmitsWithMilestoneDescription.approved.txt | 7 +++ ...lderTests.SomeCommitsNoIssues.approved.txt | 2 - ...sts.SomeCommitsSingularIssues.approved.txt | 2 - ...erTests.SomeCommitsSomeIssues.approved.txt | 2 - ...mmitsWithPluralizedLabelAlias.approved.txt | 2 - ...tsWithoutPluralizedLabelAlias.approved.txt | 2 - .../ReleaseNotesBuilderTests.cs | 53 +++++++++++++++++-- 26 files changed, 141 insertions(+), 84 deletions(-) create mode 100644 src/GitReleaseManager.Tests/ReleaseNotesBuilderTests.CorrectlyUseFooterWhenEnabled.approved.txt create mode 100644 src/GitReleaseManager.Tests/ReleaseNotesBuilderTests.CorrectlyUseFooterWithMilestoneWhenEnabled.approved.txt create mode 100644 src/GitReleaseManager.Tests/ReleaseNotesBuilderTests.SingularCommitsWithMilestoneDescription.approved.txt diff --git a/src/GitReleaseManager.Core/Extensions/StringExtensions.cs b/src/GitReleaseManager.Core/Extensions/StringExtensions.cs index 47098376..14e690fa 100644 --- a/src/GitReleaseManager.Core/Extensions/StringExtensions.cs +++ b/src/GitReleaseManager.Core/Extensions/StringExtensions.cs @@ -15,6 +15,16 @@ namespace GitReleaseManager.Core.Extensions internal static class StringExtensions { + public static string ReplaceMilestoneTitle(this string source, string milestoneKey, string milestoneTitle) + { + var dict = new Dictionary + { + { milestoneKey.Trim('{','}'), milestoneTitle }, + }; + + return source.ReplaceTemplate(dict); + } + public static string ReplaceTemplate(this string source, object values) { IDictionary pairs; diff --git a/src/GitReleaseManager.Core/ReleaseNotes/ReleaseNotesBuilder.cs b/src/GitReleaseManager.Core/ReleaseNotes/ReleaseNotesBuilder.cs index ebeb2104..6f1275f9 100644 --- a/src/GitReleaseManager.Core/ReleaseNotes/ReleaseNotesBuilder.cs +++ b/src/GitReleaseManager.Core/ReleaseNotes/ReleaseNotesBuilder.cs @@ -73,37 +73,27 @@ public async Task BuildReleaseNotes(string user, string repository, stri } var commitsLink = _vcsProvider.GetCommitsUrl(_user, _repository, _targetMilestone?.Title, previousMilestone?.Title); - var commitsText = string.Format(numberOfCommits == 1 ? "{0} commit" : "{0} commits", numberOfCommits); - var issuesText = string.Format(issues.Count == 1 ? "{0} issue" : "{0} issues", issues.Count); - - var footerContent = _configuration.Create.FooterContent; - - if (_configuration.Create.FooterIncludesMilestone && - !string.IsNullOrEmpty(_configuration.Create.MilestoneReplaceText)) - { - var replaceValues = new Dictionary - { - { _configuration.Create.MilestoneReplaceText.Trim('{', '}'), _milestoneTitle }, - }; - footerContent = footerContent.ReplaceTemplate(replaceValues); - } var issuesDict = GetIssuesDict(issues); var templateModel = new { - IssuesCount = issues.Count, - CommitsCount = numberOfCommits, - CommitsLink = commitsLink, - CommitsText = commitsText, - IssuesText = issuesText, - MilestoneDescription = _targetMilestone.Description, - MilestoneHtmlUrl = _targetMilestone.HtmlUrl, + Issues = new + { + issues.Count, + Items = issuesDict, + }, + Commits = new + { + Count = numberOfCommits, + HtmlUrl = commitsLink, + }, + Milestone = new + { + Target = _targetMilestone, + Previous = previousMilestone, + }, IssueLabels = issuesDict.Keys.ToList(), - Issues = issuesDict, - IncludeFooter = _configuration.Create.IncludeFooter, - FooterHeading = _configuration.Create.FooterHeading, - FooterContent = footerContent, }; var releaseNotes = await _templateFactory.RenderTemplateAsync(template, templateModel).ConfigureAwait(false); diff --git a/src/GitReleaseManager.Core/Templates/TemplateFactory.cs b/src/GitReleaseManager.Core/Templates/TemplateFactory.cs index 9b5507ae..6d9113f2 100644 --- a/src/GitReleaseManager.Core/Templates/TemplateFactory.cs +++ b/src/GitReleaseManager.Core/Templates/TemplateFactory.cs @@ -8,6 +8,7 @@ namespace GitReleaseManager.Core.Templates { using System.Threading.Tasks; using GitReleaseManager.Core.Configuration; + using GitReleaseManager.Core.Extensions; using GitReleaseManager.Core.Helpers; using Scriban; using Scriban.Runtime; @@ -49,10 +50,13 @@ public async Task RenderTemplateAsync(string templateName, object model) return await template.RenderAsync(context).ConfigureAwait(false); } - private static TemplateContext CreateTemplateContext(object model, TemplateLoader loader, string sourcePath) + private TemplateContext CreateTemplateContext(object model, TemplateLoader loader, string sourcePath) { var sc = new ScriptObject(); sc.Import(model); + sc.Add("template_kind", templateKind.ToString().ToUpperInvariant()); + sc.Add("config", config); + sc.ImportMember(typeof(StringExtensions), nameof(StringExtensions.ReplaceMilestoneTitle)); var context = new TemplateContext { TemplateLoader = loader, diff --git a/src/GitReleaseManager.Core/Templates/default/create/footer.sbn b/src/GitReleaseManager.Core/Templates/default/create/footer.sbn index 59dddbb9..b1b08303 100644 --- a/src/GitReleaseManager.Core/Templates/default/create/footer.sbn +++ b/src/GitReleaseManager.Core/Templates/default/create/footer.sbn @@ -1,4 +1,10 @@ -{{~ if include_footer ~}} -### {{ footer_heading }} -{{ footer_content }} -{{~ end ~}} +{{ if config.create.include_footer }} + +### {{ config.create.footer_heading }} + +{{ if config.create.milestone_replace_text + replace_milestone_title config.create.footer_content config.create.milestone_replace_text milestone.target.title + else + config.create.footer_content + end +end }} diff --git a/src/GitReleaseManager.Core/Templates/default/index.sbn b/src/GitReleaseManager.Core/Templates/default/index.sbn index f0ab9e02..331c809a 100644 --- a/src/GitReleaseManager.Core/Templates/default/index.sbn +++ b/src/GitReleaseManager.Core/Templates/default/index.sbn @@ -1,5 +1,10 @@ -{{ include 'release-info' ~}} -{{ include 'milestone' ~}} - -{{ include 'issues' ~}} -{{ include 'footer' ~}} +{{- + include 'release-info' + if milestone.target.description + include 'milestone' + end + include 'issues' | string.rstrip + if template_kind == "CREATE" + include 'footer' + end +~}} diff --git a/src/GitReleaseManager.Core/Templates/default/issue-details.sbn b/src/GitReleaseManager.Core/Templates/default/issue-details.sbn index 2fc7ae22..8e049a29 100644 --- a/src/GitReleaseManager.Core/Templates/default/issue-details.sbn +++ b/src/GitReleaseManager.Core/Templates/default/issue-details.sbn @@ -1,5 +1,5 @@ __{{ issue_label }}__ -{{ for issue in issues[issue_label] ~}} - {{~ include 'issue-note' ~}} -{{~ end }} +{{ for issue in issues.items[issue_label] + include 'issue-note' +end }} diff --git a/src/GitReleaseManager.Core/Templates/default/issues.sbn b/src/GitReleaseManager.Core/Templates/default/issues.sbn index 59597f1f..307b6080 100644 --- a/src/GitReleaseManager.Core/Templates/default/issues.sbn +++ b/src/GitReleaseManager.Core/Templates/default/issues.sbn @@ -1,3 +1,4 @@ -{{ for issue_label in issue_labels ~}} - {{~ include 'issue-details' ~}} -{{~ end ~}} + +{{ for issue_label in issue_labels + include 'issue-details' +end }} diff --git a/src/GitReleaseManager.Core/Templates/default/milestone.sbn b/src/GitReleaseManager.Core/Templates/default/milestone.sbn index c16e85c6..b3459681 100644 --- a/src/GitReleaseManager.Core/Templates/default/milestone.sbn +++ b/src/GitReleaseManager.Core/Templates/default/milestone.sbn @@ -1 +1,2 @@ -{{ milestone_description }} + +{{ milestone.target.description }} diff --git a/src/GitReleaseManager.Core/Templates/default/release-info.sbn b/src/GitReleaseManager.Core/Templates/default/release-info.sbn index fc2da166..6a1084a5 100644 --- a/src/GitReleaseManager.Core/Templates/default/release-info.sbn +++ b/src/GitReleaseManager.Core/Templates/default/release-info.sbn @@ -1,11 +1,10 @@ {{ - if issues_count > 0 - if commits_count > 0 - "As part of this release we had [" + commits_text + "](" + commits_link + ") which resulted in [" + issues_text + "](" + milestone_html_url + "?closed=1) being closed." - else - "As part of this release we had [" + issues_text + "](" + milestone_html_url + "?closed=1) closed." - end - else if commits_count > 0 - "As part of this release we had [" + commits_text + "](" + commits_link + ")." - end -}} + 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. +{{ else +}}As part of this release we had [{{ issues.count }} {{ issues.count | string.pluralize "issue" "issues" }}]({{ milestone.target.html_url }}?closed=1) 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 }}). +{{ end -}} diff --git a/src/GitReleaseManager.Tests/ReleaseNotesBuilderTests.CorrectlyExcludeIssues.approved.txt b/src/GitReleaseManager.Tests/ReleaseNotesBuilderTests.CorrectlyExcludeIssues.approved.txt index 38a6e57e..7247210d 100644 --- a/src/GitReleaseManager.Tests/ReleaseNotesBuilderTests.CorrectlyExcludeIssues.approved.txt +++ b/src/GitReleaseManager.Tests/ReleaseNotesBuilderTests.CorrectlyExcludeIssues.approved.txt @@ -1,7 +1,5 @@ As part of this release we had [5 commits](https://github.com/TestUser/FakeRepository/commits/1.2.3) which resulted in [2 issues](https://github.com/gep13/FakeRepository/issues?q=milestone%3A1.2.3?closed=1) being closed. - __Bug__ - [__#2__](http://example.com/2) Issue 2 - diff --git a/src/GitReleaseManager.Tests/ReleaseNotesBuilderTests.CorrectlyUseFooterWhenEnabled.approved.txt b/src/GitReleaseManager.Tests/ReleaseNotesBuilderTests.CorrectlyUseFooterWhenEnabled.approved.txt new file mode 100644 index 00000000..05a626f1 --- /dev/null +++ b/src/GitReleaseManager.Tests/ReleaseNotesBuilderTests.CorrectlyUseFooterWhenEnabled.approved.txt @@ -0,0 +1,9 @@ +As part of this release we had [2 commits](https://github.com/TestUser/FakeRepository/commits/1.2.3) which resulted in [1 issue](https://github.com/gep13/FakeRepository/issues?q=milestone%3A1.2.3?closed=1) being closed. + +__Bug__ + +- [__#6__](http://example.com/6) Issue 6 + +### I am a header + +I am content diff --git a/src/GitReleaseManager.Tests/ReleaseNotesBuilderTests.CorrectlyUseFooterWithMilestoneWhenEnabled.approved.txt b/src/GitReleaseManager.Tests/ReleaseNotesBuilderTests.CorrectlyUseFooterWithMilestoneWhenEnabled.approved.txt new file mode 100644 index 00000000..1e6f82ad --- /dev/null +++ b/src/GitReleaseManager.Tests/ReleaseNotesBuilderTests.CorrectlyUseFooterWithMilestoneWhenEnabled.approved.txt @@ -0,0 +1,9 @@ +As part of this release we had [4 commits](https://github.com/TestUser/FakeRepository/commits/1.2.3) which resulted in [1 issue](https://github.com/gep13/FakeRepository/issues?q=milestone%3A1.2.3?closed=1) being closed. + +__Feature__ + +- [__#78__](http://example.com/78) Issue 78 + +### I am a header + +I am a content with milestone 1.2.3! diff --git a/src/GitReleaseManager.Tests/ReleaseNotesBuilderTests.NoCommitsNoIssues.approved.txt b/src/GitReleaseManager.Tests/ReleaseNotesBuilderTests.NoCommitsNoIssues.approved.txt index b28b04f6..e69de29b 100644 --- a/src/GitReleaseManager.Tests/ReleaseNotesBuilderTests.NoCommitsNoIssues.approved.txt +++ b/src/GitReleaseManager.Tests/ReleaseNotesBuilderTests.NoCommitsNoIssues.approved.txt @@ -1,3 +0,0 @@ - - - diff --git a/src/GitReleaseManager.Tests/ReleaseNotesBuilderTests.NoCommitsSingularIssues.approved.txt b/src/GitReleaseManager.Tests/ReleaseNotesBuilderTests.NoCommitsSingularIssues.approved.txt index 803dd100..48c4ae86 100644 --- a/src/GitReleaseManager.Tests/ReleaseNotesBuilderTests.NoCommitsSingularIssues.approved.txt +++ b/src/GitReleaseManager.Tests/ReleaseNotesBuilderTests.NoCommitsSingularIssues.approved.txt @@ -1,7 +1,5 @@ As part of this release we had [1 issue](https://github.com/gep13/FakeRepository/issues?q=milestone%3A1.2.3?closed=1) closed. - __Bug__ - [__#1__](http://example.com/1) Issue 1 - diff --git a/src/GitReleaseManager.Tests/ReleaseNotesBuilderTests.NoCommitsSomeIssues.approved.txt b/src/GitReleaseManager.Tests/ReleaseNotesBuilderTests.NoCommitsSomeIssues.approved.txt index def47024..ab4842d3 100644 --- a/src/GitReleaseManager.Tests/ReleaseNotesBuilderTests.NoCommitsSomeIssues.approved.txt +++ b/src/GitReleaseManager.Tests/ReleaseNotesBuilderTests.NoCommitsSomeIssues.approved.txt @@ -1,6 +1,5 @@ As part of this release we had [3 issues](https://github.com/gep13/FakeRepository/issues?q=milestone%3A1.2.3?closed=1) closed. - __Bug__ - [__#1__](http://example.com/1) Issue 1 @@ -12,4 +11,3 @@ __Feature__ __Improvement__ - [__#3__](http://example.com/3) Issue 3 - diff --git a/src/GitReleaseManager.Tests/ReleaseNotesBuilderTests.SingularCommitsNoIssues.approved.txt b/src/GitReleaseManager.Tests/ReleaseNotesBuilderTests.SingularCommitsNoIssues.approved.txt index e5447ce4..ac84e154 100644 --- a/src/GitReleaseManager.Tests/ReleaseNotesBuilderTests.SingularCommitsNoIssues.approved.txt +++ b/src/GitReleaseManager.Tests/ReleaseNotesBuilderTests.SingularCommitsNoIssues.approved.txt @@ -1,3 +1 @@ As part of this release we had [1 commit](https://github.com/TestUser/FakeRepository/commits/1.2.3). - - diff --git a/src/GitReleaseManager.Tests/ReleaseNotesBuilderTests.SingularCommitsSingularIssues.approved.txt b/src/GitReleaseManager.Tests/ReleaseNotesBuilderTests.SingularCommitsSingularIssues.approved.txt index 8bad15be..620b7fba 100644 --- a/src/GitReleaseManager.Tests/ReleaseNotesBuilderTests.SingularCommitsSingularIssues.approved.txt +++ b/src/GitReleaseManager.Tests/ReleaseNotesBuilderTests.SingularCommitsSingularIssues.approved.txt @@ -1,7 +1,5 @@ As part of this release we had [1 commit](https://github.com/TestUser/FakeRepository/commits/1.2.3) which resulted in [1 issue](https://github.com/gep13/FakeRepository/issues?q=milestone%3A1.2.3?closed=1) being closed. - __Bug__ - [__#1__](http://example.com/1) Issue 1 - diff --git a/src/GitReleaseManager.Tests/ReleaseNotesBuilderTests.SingularCommitsSomeIssues.approved.txt b/src/GitReleaseManager.Tests/ReleaseNotesBuilderTests.SingularCommitsSomeIssues.approved.txt index 0acbaf4e..331e9ba8 100644 --- a/src/GitReleaseManager.Tests/ReleaseNotesBuilderTests.SingularCommitsSomeIssues.approved.txt +++ b/src/GitReleaseManager.Tests/ReleaseNotesBuilderTests.SingularCommitsSomeIssues.approved.txt @@ -1,6 +1,5 @@ As part of this release we had [1 commit](https://github.com/TestUser/FakeRepository/commits/1.2.3) which resulted in [3 issues](https://github.com/gep13/FakeRepository/issues?q=milestone%3A1.2.3?closed=1) being closed. - __Bug__ - [__#1__](http://example.com/1) Issue 1 @@ -12,4 +11,3 @@ __Feature__ __Improvement__ - [__#3__](http://example.com/3) Issue 3 - diff --git a/src/GitReleaseManager.Tests/ReleaseNotesBuilderTests.SingularCommitsWithHeaderLabelAlias.approved.txt b/src/GitReleaseManager.Tests/ReleaseNotesBuilderTests.SingularCommitsWithHeaderLabelAlias.approved.txt index ddf70689..eb9e6ab5 100644 --- a/src/GitReleaseManager.Tests/ReleaseNotesBuilderTests.SingularCommitsWithHeaderLabelAlias.approved.txt +++ b/src/GitReleaseManager.Tests/ReleaseNotesBuilderTests.SingularCommitsWithHeaderLabelAlias.approved.txt @@ -1,7 +1,5 @@ As part of this release we had [1 commit](https://github.com/TestUser/FakeRepository/commits/1.2.3) which resulted in [1 issue](https://github.com/gep13/FakeRepository/issues?q=milestone%3A1.2.3?closed=1) being closed. - __Foo__ - [__#1__](http://example.com/1) Issue 1 - diff --git a/src/GitReleaseManager.Tests/ReleaseNotesBuilderTests.SingularCommitsWithMilestoneDescription.approved.txt b/src/GitReleaseManager.Tests/ReleaseNotesBuilderTests.SingularCommitsWithMilestoneDescription.approved.txt new file mode 100644 index 00000000..0656f49b --- /dev/null +++ b/src/GitReleaseManager.Tests/ReleaseNotesBuilderTests.SingularCommitsWithMilestoneDescription.approved.txt @@ -0,0 +1,7 @@ +As part of this release we had [1 commit](https://github.com/TestUser/FakeRepository/commits/2.4.2) which resulted in [1 issue](https://github.com/gep13/FakeRepository/issues?q=milestone%3A2.4.2?closed=1) being closed. + +I am some awesome milestone description. + +__Feature__ + +- [__#5__](http://example.com/5) Issue 5 diff --git a/src/GitReleaseManager.Tests/ReleaseNotesBuilderTests.SomeCommitsNoIssues.approved.txt b/src/GitReleaseManager.Tests/ReleaseNotesBuilderTests.SomeCommitsNoIssues.approved.txt index 12c6455b..acba72c7 100644 --- a/src/GitReleaseManager.Tests/ReleaseNotesBuilderTests.SomeCommitsNoIssues.approved.txt +++ b/src/GitReleaseManager.Tests/ReleaseNotesBuilderTests.SomeCommitsNoIssues.approved.txt @@ -1,3 +1 @@ As part of this release we had [5 commits](https://github.com/TestUser/FakeRepository/commits/1.2.3). - - diff --git a/src/GitReleaseManager.Tests/ReleaseNotesBuilderTests.SomeCommitsSingularIssues.approved.txt b/src/GitReleaseManager.Tests/ReleaseNotesBuilderTests.SomeCommitsSingularIssues.approved.txt index ea72dea3..f6a9bb4c 100644 --- a/src/GitReleaseManager.Tests/ReleaseNotesBuilderTests.SomeCommitsSingularIssues.approved.txt +++ b/src/GitReleaseManager.Tests/ReleaseNotesBuilderTests.SomeCommitsSingularIssues.approved.txt @@ -1,7 +1,5 @@ As part of this release we had [5 commits](https://github.com/TestUser/FakeRepository/commits/1.2.3) which resulted in [1 issue](https://github.com/gep13/FakeRepository/issues?q=milestone%3A1.2.3?closed=1) being closed. - __Bug__ - [__#1__](http://example.com/1) Issue 1 - diff --git a/src/GitReleaseManager.Tests/ReleaseNotesBuilderTests.SomeCommitsSomeIssues.approved.txt b/src/GitReleaseManager.Tests/ReleaseNotesBuilderTests.SomeCommitsSomeIssues.approved.txt index 2ad509bc..00d7d5eb 100644 --- a/src/GitReleaseManager.Tests/ReleaseNotesBuilderTests.SomeCommitsSomeIssues.approved.txt +++ b/src/GitReleaseManager.Tests/ReleaseNotesBuilderTests.SomeCommitsSomeIssues.approved.txt @@ -1,6 +1,5 @@ As part of this release we had [5 commits](https://github.com/TestUser/FakeRepository/commits/1.2.3) which resulted in [3 issues](https://github.com/gep13/FakeRepository/issues?q=milestone%3A1.2.3?closed=1) being closed. - __Bug__ - [__#1__](http://example.com/1) Issue 1 @@ -12,4 +11,3 @@ __Feature__ __Improvement__ - [__#3__](http://example.com/3) Issue 3 - diff --git a/src/GitReleaseManager.Tests/ReleaseNotesBuilderTests.SomeCommitsWithPluralizedLabelAlias.approved.txt b/src/GitReleaseManager.Tests/ReleaseNotesBuilderTests.SomeCommitsWithPluralizedLabelAlias.approved.txt index 05461517..d2c22ab4 100644 --- a/src/GitReleaseManager.Tests/ReleaseNotesBuilderTests.SomeCommitsWithPluralizedLabelAlias.approved.txt +++ b/src/GitReleaseManager.Tests/ReleaseNotesBuilderTests.SomeCommitsWithPluralizedLabelAlias.approved.txt @@ -1,8 +1,6 @@ As part of this release we had [5 commits](https://github.com/TestUser/FakeRepository/commits/1.2.3) which resulted in [2 issues](https://github.com/gep13/FakeRepository/issues?q=milestone%3A1.2.3?closed=1) being closed. - __Bar__ - [__#1__](http://example.com/1) Issue 1 - [__#2__](http://example.com/2) Issue 2 - diff --git a/src/GitReleaseManager.Tests/ReleaseNotesBuilderTests.SomeCommitsWithoutPluralizedLabelAlias.approved.txt b/src/GitReleaseManager.Tests/ReleaseNotesBuilderTests.SomeCommitsWithoutPluralizedLabelAlias.approved.txt index 5e90ac01..6538b862 100644 --- a/src/GitReleaseManager.Tests/ReleaseNotesBuilderTests.SomeCommitsWithoutPluralizedLabelAlias.approved.txt +++ b/src/GitReleaseManager.Tests/ReleaseNotesBuilderTests.SomeCommitsWithoutPluralizedLabelAlias.approved.txt @@ -1,8 +1,6 @@ As part of this release we had [5 commits](https://github.com/TestUser/FakeRepository/commits/1.2.3) which resulted in [2 issues](https://github.com/gep13/FakeRepository/issues?q=milestone%3A1.2.3?closed=1) being closed. - __Help Wanteds__ - [__#1__](http://example.com/1) Issue 1 - [__#2__](http://example.com/2) Issue 2 - diff --git a/src/GitReleaseManager.Tests/ReleaseNotesBuilderTests.cs b/src/GitReleaseManager.Tests/ReleaseNotesBuilderTests.cs index dea72c92..c779a3f3 100644 --- a/src/GitReleaseManager.Tests/ReleaseNotesBuilderTests.cs +++ b/src/GitReleaseManager.Tests/ReleaseNotesBuilderTests.cs @@ -101,6 +101,13 @@ public void SingularCommitsWithHeaderLabelAlias() Assert.True(true); // Just to make sonarlint happy } + [Test] + public void SingularCommitsWithMilestoneDescription() + { + AcceptTest(1, CreateMilestone("2.4.2", "I am some awesome milestone description."), CreateIssue(5, "Feature")); + Assert.True(true); // Just to make sonarlint happy + } + [Test] public void SomeCommitsWithPluralizedLabelAlias() { @@ -122,6 +129,29 @@ public void SomeCommitsWithoutPluralizedLabelAlias() Assert.True(true); // Just to make sonarlint happy } + [Test] + public void CorrectlyUseFooterWhenEnabled() + { + var config = new Config(); + config.Create.IncludeFooter = true; + config.Create.FooterHeading = "I am a header"; + config.Create.FooterContent = "I am content"; + + AcceptTest(2, config, CreateIssue(6, "Bug")); + } + + [Test] + public void CorrectlyUseFooterWithMilestoneWhenEnabled() + { + var config = new Config(); + config.Create.IncludeFooter = true; + config.Create.FooterHeading = "I am a header"; + config.Create.FooterContent = "I am a content with milestone {milestone}!"; + config.Create.MilestoneReplaceText = "{milestone}"; + + AcceptTest(4, config, CreateIssue(78, "Feature")); + } + [Test] public void NoCommitsWrongIssueLabel() { @@ -145,16 +175,28 @@ public void CorrectlyExcludeIssues() private static void AcceptTest(int commits, params Issue[] issues) { - AcceptTest(commits, null, issues); + AcceptTest(commits, null, null, issues); + Assert.True(true); // Just to make sonarlint happy + } + + private static void AcceptTest(int commits, Milestone milestone, params Issue[] issues) + { + AcceptTest(commits, null, milestone, issues); Assert.True(true); // Just to make sonarlint happy } private static void AcceptTest(int commits, Config config, params Issue[] issues) + { + AcceptTest(commits, config, null, issues); + Assert.True(true); // Just to make sonarlint happy + } + + private static void AcceptTest(int commits, Config config, Milestone milestone, params Issue[] issues) { var owner = "TestUser"; var repository = "FakeRepository"; var milestoneNumber = 1; - var milestoneTitle = "1.2.3"; + milestone ??= CreateMilestone("1.2.3"); var vcsService = new VcsServiceMock(); var logger = Substitute.For(); @@ -162,7 +204,7 @@ private static void AcceptTest(int commits, Config config, params Issue[] issues var currentDirectory = Environment.CurrentDirectory; var configuration = config ?? ConfigurationProvider.Provide(currentDirectory, fileSystem); - vcsService.Milestones.Add(CreateMilestone(milestoneTitle)); + vcsService.Milestones.Add(milestone); vcsService.NumberOfCommits = commits; @@ -185,16 +227,17 @@ private static void AcceptTest(int commits, Config config, params Issue[] issues .Returns(Task.FromResult((IEnumerable)vcsService.Milestones)); var builder = new ReleaseNotesBuilder(vcsProvider, logger, fileSystem, configuration, new TemplateFactory(fileSystem, configuration, TemplateKind.Create)); - var notes = builder.BuildReleaseNotes(owner, repository, milestoneTitle, ReleaseTemplates.DEFAULT_NAME).Result; + var notes = builder.BuildReleaseNotes(owner, repository, milestone.Title, ReleaseTemplates.DEFAULT_NAME).Result; Approvals.Verify(notes); } - private static Milestone CreateMilestone(string version) + private static Milestone CreateMilestone(string version, string description = null) { return new Milestone { Title = version, + Description = description, Number = 1, HtmlUrl = "https://github.com/gep13/FakeRepository/issues?q=milestone%3A" + version, Version = new Version(version), From 60bc027c7182d9ce42d3d08aa6506a80e3171480 Mon Sep 17 00:00:00 2001 From: AdmiringWorm Date: Wed, 16 Sep 2020 12:29:49 +0200 Subject: [PATCH 07/12] (GH-217) Hide footer yaml properties to encourage usage of scriban templates The hidden properties can still be used, but will not be part of the sample that gets generated when running 'init'. --- .../Configuration/ConfigSerializer.cs | 22 +++++++++++++++++++ .../Configuration/CreateConfig.cs | 13 +++++------ .../ConfigurationTests.cs | 9 ++++++-- 3 files changed, 35 insertions(+), 9 deletions(-) diff --git a/src/GitReleaseManager.Core/Configuration/ConfigSerializer.cs b/src/GitReleaseManager.Core/Configuration/ConfigSerializer.cs index b53a6072..4e54394f 100644 --- a/src/GitReleaseManager.Core/Configuration/ConfigSerializer.cs +++ b/src/GitReleaseManager.Core/Configuration/ConfigSerializer.cs @@ -85,8 +85,30 @@ private static void SetConfigurationSamples(object config, Type configType, Text foreach (var property in configType.GetProperties(BindingFlags.Public | BindingFlags.Instance)) { var sampleAttribute = property.GetCustomAttribute(); + var yamlMemberAttribute = property.GetCustomAttribute(); var propertyType = property.PropertyType; + if (yamlMemberAttribute != null && sampleAttribute is null) + { + if (yamlMemberAttribute.DefaultValuesHandling == DefaultValuesHandling.OmitDefaults) + { + if (propertyType.IsValueType) + { + property.SetValue(config, Activator.CreateInstance(propertyType)); + } + else + { + property.SetValue(config, null); + } + continue; + } + else if (yamlMemberAttribute.DefaultValuesHandling == DefaultValuesHandling.OmitNull) + { + property.SetValue(config, null); + continue; + } + } + if (propertyType.IsClass && propertyType != typeof(string) && propertyType != typeof(DirectoryInfo)) { var subConfig = property.GetValue(config); diff --git a/src/GitReleaseManager.Core/Configuration/CreateConfig.cs b/src/GitReleaseManager.Core/Configuration/CreateConfig.cs index 6f9cd314..302df86b 100644 --- a/src/GitReleaseManager.Core/Configuration/CreateConfig.cs +++ b/src/GitReleaseManager.Core/Configuration/CreateConfig.cs @@ -6,28 +6,27 @@ namespace GitReleaseManager.Core.Configuration { + using System.ComponentModel; using GitReleaseManager.Core.Attributes; using YamlDotNet.Serialization; public class CreateConfig { + [Description("Enable generation of footer content in the release notes. Extract the recommended templates by running 'init --templates' and edit the footer.sbn file to provide the wanted footer content.")] [Sample(true)] [YamlMember(Alias = "include-footer")] public bool IncludeFooter { get; set; } - [Sample("Where to get it")] - [YamlMember(Alias = "footer-heading")] + [YamlMember(Alias = "footer-heading", DefaultValuesHandling = DefaultValuesHandling.OmitNull)] public string FooterHeading { get; set; } - [Sample("You can download this release from\n[chocolatey](https://chocolatey.org/packages/chocolateyGUI/{milestone})")] - [YamlMember(Alias = "footer-content")] + [YamlMember(Alias = "footer-content", DefaultValuesHandling = DefaultValuesHandling.OmitNull)] public string FooterContent { get; set; } - [YamlMember(Alias = "footer-includes-milestone")] + [YamlMember(Alias = "footer-includes-milestone", DefaultValuesHandling = DefaultValuesHandling.OmitDefaults)] public bool FooterIncludesMilestone { get; set; } - [Sample("{milestone}")] - [YamlMember(Alias = "milestone-replace-text")] + [YamlMember(Alias = "milestone-replace-text", DefaultValuesHandling = DefaultValuesHandling.OmitNull)] public string MilestoneReplaceText { get; set; } [YamlMember(Alias = "include-sha-section")] diff --git a/src/GitReleaseManager.Tests/ConfigurationTests.cs b/src/GitReleaseManager.Tests/ConfigurationTests.cs index d8ad033f..b33c9344 100644 --- a/src/GitReleaseManager.Tests/ConfigurationTests.cs +++ b/src/GitReleaseManager.Tests/ConfigurationTests.cs @@ -106,7 +106,7 @@ public void Should_WriteSample_String_Values() var text = builder.ToString(); // Then - Assert.That(text, Contains.Substring("# footer-heading: Where to get it")); + Assert.That(text, Contains.Substring("# sha-section-line-format: '- `{1}\t{0}`'")); Assert.That(text, Contains.Substring("#default-branch: master")); } @@ -125,7 +125,12 @@ public void Should_WriteSample_Multiline_String_Values() var text = builder.ToString(); // Then - var expectedText = string.Format("# footer-content: >-{0}# You can download this release from{0}#{0}# [chocolatey](https://chocolatey.org/packages/chocolateyGUI/{{milestone}})", Environment.NewLine); + var expectedText = string.Format( + "# issue-comment: |-{0}" + + "# :tada: This issue has been resolved in version {{milestone}} :tada:{0}" + + "#{0}" + + "# The release is available on:{0}", + Environment.NewLine); Assert.That(text, Contains.Substring(expectedText)); } } From b9dde95f82aa0b1115231052758236de1b1e98c5 Mon Sep 17 00:00:00 2001 From: AdmiringWorm Date: Thu, 17 Sep 2020 00:48:57 +0200 Subject: [PATCH 08/12] (docs) Add documentation regarding overriding templates --- .../configuration/create-configuration.md | 9 +- .../configuration/default-configuration.md | 7 +- .../configuration/template-configuration.md | 259 ++++++++++++++++++ 3 files changed, 270 insertions(+), 5 deletions(-) create mode 100644 docs/input/docs/configuration/template-configuration.md diff --git a/docs/input/docs/configuration/create-configuration.md b/docs/input/docs/configuration/create-configuration.md index 9a7b897d..5723ad2d 100644 --- a/docs/input/docs/configuration/create-configuration.md +++ b/docs/input/docs/configuration/create-configuration.md @@ -15,6 +15,13 @@ release can be located. Take for example the GitReleaseManager configuration file which is used by the [Chocolatey GUI](https://github.com/chocolatey/ChocolateyGUI) project: +:::{.alert .alert-warning} +Configuring the footer is no longer recommended. Going forward the recommendation +is to configure the footer using seperate template files. +Please see [Template Configuration](template-configuration#editing-the-templates) +for more information. +::: + ```yaml create: include-footer: true @@ -28,7 +35,7 @@ This would result in the following [release notes](https://github.com/chocolatey/ChocolateyGUI/releases/tag/0.13.1) being generated: -![Example Release Notes](../images/example-release-notes.png) +![Example Release Notes](../images/example-release-notes.png){.img-responsive} :::{.alert .alert-info} The generated URL for the link to Chocolatey.org includes the milestone number. diff --git a/docs/input/docs/configuration/default-configuration.md b/docs/input/docs/configuration/default-configuration.md index c389a652..a4fc818e 100644 --- a/docs/input/docs/configuration/default-configuration.md +++ b/docs/input/docs/configuration/default-configuration.md @@ -12,11 +12,10 @@ when no yaml file is placed in the root directory): ```yaml create: + # Please see + # https://gittools.github.io/GitReleaseManager/docs/configuration/template-configuration#editing-the-templates + # configuration for configuring footers include-footer: false - footer-heading: '' - footer-content: '' - footer-includes-milestone: false - milestone-replace-text: '' include-sha-section: false sha-section-heading: "SHA256 Hashes of the release artifacts" sha-section-line-format: "- `{1}\t{0}`" diff --git a/docs/input/docs/configuration/template-configuration.md b/docs/input/docs/configuration/template-configuration.md new file mode 100644 index 00000000..3ce987d8 --- /dev/null +++ b/docs/input/docs/configuration/template-configuration.md @@ -0,0 +1,259 @@ +--- +Order: 11 +Title: Template Configuration +--- + +Welcome to the documentation on how to configure each available step +of release notes generation by using Scriban templates. +While you can still use the old way of configuring templates (footer only) in +the yaml file, going forward it is recommended to use the new aproach by +extracting and editing the template files you wish to change instead. + +## How are templates resolved + +:::{.alert .alert-info} +If you will be passing in a custom absolute path to a template, +then you can skip this section entirely (assuming you won't be importing +any files either). +::: + +Before we can go into how you can edit your templates, we first need to talk +about how are the templates resolved. +First of all, there is a new property that can be used to specify the base +directory of the templates directory located in the yaml configuration file. +This configuration is called `templates-dir` and will be used for all relative +paths (and for template names). + +The templates are resolved in the following order (`` is the value +specified in the `templates-dir` directory, and `` is the name passed +to GitReleaseManage. _defaults to `default`_). + +We will be using the [create](../commands/create) as an example in these paths. +Additional commands are planned to be supported, but for now these templates +can only be used when calling `GitReleaseManager create`. + +**For abbreviation, file extensions are omitted from the pats. +File extension are expected to be either `.sbn` or `.scriban` unless otherwise +specified when passing in the template name when calling `GitReleaseManager`.** + +### Resolving the initial index file + +1. `//create/index` +2. `//create/` +3. `/create//index` +4. `/create//` +5. `//index` +6. `//` +7. `/create/index` (_will disable fallback to embedded resource if found_) +8. `/create/` (_will disable fallback to embedded resource if found_) +9. `/` (_will disable fallback to embedded resource if found_) + +In the above resolution, if there is no file that can be found, GitReleaseManager +will try to fall back into the resources that are embedded within. + +### Resolving child files + +Each index file (and subsequent children) can include additional files that +needs to be imported (Please see the [Scriban documentation]() for this). + +When this is being used, some paths are removed and others may be added. +In these paths the following substitution values are being used. + +- `` == The base directory as specified by the `templates-dir` property. +- `