1/* 2 * Copyright (c) 2021-2023, Arm Limited. All rights reserved. 3 * 4 * SPDX-License-Identifier: BSD-3-Clause 5 */ 6 7/* eslint-env es6 */ 8 9"use strict"; 10 11const Handlebars = require("handlebars"); 12const Q = require("q"); 13const _ = require("lodash"); 14 15const ccConventionalChangelog = require("conventional-changelog-conventionalcommits/conventional-changelog"); 16const ccParserOpts = require("conventional-changelog-conventionalcommits/parser-opts"); 17const ccRecommendedBumpOpts = require("conventional-changelog-conventionalcommits/conventional-recommended-bump"); 18const ccWriterOpts = require("conventional-changelog-conventionalcommits/writer-opts"); 19 20const execa = require("execa"); 21 22const readFileSync = require("fs").readFileSync; 23const 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 */ 30Handlebars.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 */ 42Handlebars.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 50function 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 /* 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 /* 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 202module.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}; 234