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