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