Chris Kay | 025c87f | 2021-11-09 20:05:38 +0000 | [diff] [blame] | 1 | /* |
Chris Kay | 1f8e7a9 | 2023-05-17 17:23:37 +0100 | [diff] [blame] | 2 | * Copyright (c) 2021-2023, Arm Limited. All rights reserved. |
Chris Kay | 025c87f | 2021-11-09 20:05:38 +0000 | [diff] [blame] | 3 | * |
| 4 | * SPDX-License-Identifier: BSD-3-Clause |
| 5 | */ |
| 6 | |
| 7 | /* eslint-env es6 */ |
| 8 | |
| 9 | "use strict"; |
| 10 | |
| 11 | const Handlebars = require("handlebars"); |
| 12 | const Q = require("q"); |
| 13 | const _ = require("lodash"); |
| 14 | |
| 15 | const ccConventionalChangelog = require("conventional-changelog-conventionalcommits/conventional-changelog"); |
| 16 | const ccParserOpts = require("conventional-changelog-conventionalcommits/parser-opts"); |
| 17 | const ccRecommendedBumpOpts = require("conventional-changelog-conventionalcommits/conventional-recommended-bump"); |
| 18 | const ccWriterOpts = require("conventional-changelog-conventionalcommits/writer-opts"); |
| 19 | |
| 20 | const execa = require("execa"); |
| 21 | |
| 22 | const readFileSync = require("fs").readFileSync; |
| 23 | const resolve = require("path").resolve; |
| 24 | |
| 25 | /* |
| 26 | * Register a Handlebars helper that lets us generate Markdown lists that can support multi-line |
| 27 | * strings. This is driven by inconsistent formatting of breaking changes, which may be multiple |
| 28 | * lines long and can terminate the list early unintentionally. |
| 29 | */ |
| 30 | Handlebars.registerHelper("tf-a-mdlist", function (indent, options) { |
| 31 | const spaces = new Array(indent + 1).join(" "); |
| 32 | const first = spaces + "- "; |
| 33 | const nth = spaces + " "; |
| 34 | |
| 35 | return first + options.fn(this).replace(/\n(?!\s*\n)/gm, `\n${nth}`).trim() + "\n"; |
| 36 | }); |
| 37 | |
| 38 | /* |
| 39 | * Register a Handlebars helper that concatenates multiple variables. We use this to generate the |
| 40 | * title for the section partials. |
| 41 | */ |
| 42 | Handlebars.registerHelper("tf-a-concat", function () { |
| 43 | let argv = Array.prototype.slice.call(arguments, 0); |
| 44 | |
| 45 | argv.pop(); |
| 46 | |
| 47 | return argv.join(""); |
| 48 | }); |
| 49 | |
| 50 | function writerOpts(config) { |
| 51 | /* |
| 52 | * Flatten the configuration's sections list. This helps us iterate over all of the sections |
| 53 | * when we don't care about the hierarchy. |
| 54 | */ |
| 55 | |
| 56 | const flattenSections = function (sections) { |
| 57 | return sections.flatMap(section => { |
| 58 | const subsections = flattenSections(section.sections || []); |
| 59 | |
| 60 | return [section].concat(subsections); |
| 61 | }) |
| 62 | }; |
| 63 | |
| 64 | const flattenedSections = flattenSections(config.sections); |
| 65 | |
| 66 | /* |
| 67 | * Register a helper to return a restructured version of the note groups that includes notes |
| 68 | * categorized by their section. |
| 69 | */ |
| 70 | Handlebars.registerHelper("tf-a-notes", function (noteGroups, options) { |
| 71 | const generateTemplateData = function (sections, notes) { |
| 72 | return (sections || []).flatMap(section => { |
| 73 | const templateData = { |
| 74 | title: section.title, |
| 75 | sections: generateTemplateData(section.sections, notes), |
| 76 | notes: notes.filter(note => section.scopes?.includes(note.commit.scope)), |
| 77 | }; |
| 78 | |
| 79 | /* |
| 80 | * Don't return a section if it contains no notes and no sub-sections. |
| 81 | */ |
| 82 | if ((templateData.sections.length == 0) && (templateData.notes.length == 0)) { |
| 83 | return []; |
| 84 | } |
| 85 | |
| 86 | return [templateData]; |
| 87 | }); |
| 88 | }; |
| 89 | |
| 90 | return noteGroups.map(noteGroup => { |
| 91 | return { |
| 92 | title: noteGroup.title, |
| 93 | sections: generateTemplateData(config.sections, noteGroup.notes), |
| 94 | notes: noteGroup.notes.filter(note => |
| 95 | !flattenedSections.some(section => section.scopes?.includes(note.commit.scope))), |
| 96 | }; |
| 97 | }); |
| 98 | }); |
| 99 | |
| 100 | /* |
| 101 | * Register a helper to return a restructured version of the commit groups that includes commits |
| 102 | * categorized by their section. |
| 103 | */ |
| 104 | Handlebars.registerHelper("tf-a-commits", function (commitGroups, options) { |
| 105 | const generateTemplateData = function (sections, commits) { |
| 106 | return (sections || []).flatMap(section => { |
| 107 | const templateData = { |
| 108 | title: section.title, |
| 109 | sections: generateTemplateData(section.sections, commits), |
| 110 | commits: commits.filter(commit => section.scopes?.includes(commit.scope)), |
| 111 | }; |
| 112 | |
| 113 | /* |
| 114 | * Don't return a section if it contains no notes and no sub-sections. |
| 115 | */ |
| 116 | if ((templateData.sections.length == 0) && (templateData.commits.length == 0)) { |
| 117 | return []; |
| 118 | } |
| 119 | |
| 120 | return [templateData]; |
| 121 | }); |
| 122 | }; |
| 123 | |
| 124 | return commitGroups.map(commitGroup => { |
| 125 | return { |
| 126 | title: commitGroup.title, |
| 127 | sections: generateTemplateData(config.sections, commitGroup.commits), |
| 128 | commits: commitGroup.commits.filter(commit => |
| 129 | !flattenedSections.some(section => section.scopes?.includes(commit.scope))), |
| 130 | }; |
| 131 | }); |
| 132 | }); |
| 133 | |
| 134 | const writerOpts = ccWriterOpts(config) |
| 135 | .then(writerOpts => { |
| 136 | const ccWriterOptsTransform = writerOpts.transform; |
| 137 | |
| 138 | /* |
| 139 | * These configuration properties can't be injected directly into the template because |
| 140 | * they themselves are templates. Instead, we register them as partials, which allows |
| 141 | * them to be evaluated as part of the templates they're used in. |
| 142 | */ |
| 143 | Handlebars.registerPartial("commitUrl", config.commitUrlFormat); |
| 144 | Handlebars.registerPartial("compareUrl", config.compareUrlFormat); |
| 145 | Handlebars.registerPartial("issueUrl", config.issueUrlFormat); |
| 146 | |
| 147 | /* |
| 148 | * Register the partials that allow us to recursively create changelog sections. |
| 149 | */ |
| 150 | |
| 151 | const notePartial = readFileSync(resolve(__dirname, "./templates/note.hbs"), "utf-8"); |
| 152 | const noteSectionPartial = readFileSync(resolve(__dirname, "./templates/note-section.hbs"), "utf-8"); |
| 153 | const commitSectionPartial = readFileSync(resolve(__dirname, "./templates/commit-section.hbs"), "utf-8"); |
| 154 | |
| 155 | Handlebars.registerPartial("tf-a-note", notePartial); |
| 156 | Handlebars.registerPartial("tf-a-note-section", noteSectionPartial); |
| 157 | Handlebars.registerPartial("tf-a-commit-section", commitSectionPartial); |
| 158 | |
| 159 | /* |
| 160 | * Override the base templates so that we can generate a changelog that looks at least |
| 161 | * similar to the pre-Conventional Commits TF-A changelog. |
| 162 | */ |
| 163 | writerOpts.mainTemplate = readFileSync(resolve(__dirname, "./templates/template.hbs"), "utf-8"); |
| 164 | writerOpts.headerPartial = readFileSync(resolve(__dirname, "./templates/header.hbs"), "utf-8"); |
| 165 | writerOpts.commitPartial = readFileSync(resolve(__dirname, "./templates/commit.hbs"), "utf-8"); |
| 166 | writerOpts.footerPartial = readFileSync(resolve(__dirname, "./templates/footer.hbs"), "utf-8"); |
| 167 | |
| 168 | writerOpts.transform = function (commit, context) { |
| 169 | /* |
Chris Kay | 1f8e7a9 | 2023-05-17 17:23:37 +0100 | [diff] [blame] | 170 | * Feedback on the generated changelog has shown that having build system changes |
| 171 | * appear at the top of a section throws some people off. We make an exception for |
| 172 | * scopeless `build`-type changes and treat them as though they actually have the |
| 173 | * `build` scope. |
| 174 | */ |
| 175 | |
| 176 | if ((commit.type === "build") && (commit.scope == null)) { |
| 177 | commit.scope = "build"; |
| 178 | } |
| 179 | |
| 180 | /* |
Chris Kay | 025c87f | 2021-11-09 20:05:38 +0000 | [diff] [blame] | 181 | * Fix up commit trailers, which for some reason are not correctly recognized and |
| 182 | * end up showing up in the breaking changes. |
| 183 | */ |
| 184 | |
| 185 | commit.notes.forEach(note => { |
| 186 | const trailers = execa.sync("git", ["interpret-trailers", "--parse"], { |
| 187 | input: note.text |
| 188 | }).stdout; |
| 189 | |
| 190 | note.text = note.text.replace(trailers, "").trim(); |
| 191 | }); |
| 192 | |
| 193 | return ccWriterOptsTransform(commit, context); |
| 194 | }; |
| 195 | |
| 196 | return writerOpts; |
| 197 | }); |
| 198 | |
| 199 | return writerOpts; |
| 200 | } |
| 201 | |
| 202 | module.exports = function (parameter) { |
| 203 | const config = parameter || {}; |
| 204 | |
| 205 | return Q.all([ |
| 206 | ccConventionalChangelog(config), |
| 207 | ccParserOpts(config), |
| 208 | ccRecommendedBumpOpts(config), |
| 209 | writerOpts(config) |
| 210 | ]).spread(( |
| 211 | conventionalChangelog, |
| 212 | parserOpts, |
| 213 | recommendedBumpOpts, |
| 214 | writerOpts |
| 215 | ) => { |
| 216 | if (_.isFunction(parameter)) { |
| 217 | return parameter(null, { |
| 218 | gitRawCommitsOpts: { noMerges: null }, |
| 219 | conventionalChangelog, |
| 220 | parserOpts, |
| 221 | recommendedBumpOpts, |
| 222 | writerOpts |
| 223 | }); |
| 224 | } else { |
| 225 | return { |
| 226 | conventionalChangelog, |
| 227 | parserOpts, |
| 228 | recommendedBumpOpts, |
| 229 | writerOpts |
| 230 | }; |
| 231 | } |
| 232 | }); |
| 233 | }; |