| /* |
| * Copyright (c) 2021-2023, Arm Limited. All rights reserved. |
| * |
| * SPDX-License-Identifier: BSD-3-Clause |
| */ |
| |
| /* eslint-env es6 */ |
| |
| "use strict"; |
| |
| const Handlebars = require("handlebars"); |
| const Q = require("q"); |
| const _ = require("lodash"); |
| |
| const ccConventionalChangelog = require("conventional-changelog-conventionalcommits/conventional-changelog"); |
| const ccParserOpts = require("conventional-changelog-conventionalcommits/parser-opts"); |
| const ccRecommendedBumpOpts = require("conventional-changelog-conventionalcommits/conventional-recommended-bump"); |
| const ccWriterOpts = require("conventional-changelog-conventionalcommits/writer-opts"); |
| |
| const execa = require("execa"); |
| |
| const readFileSync = require("fs").readFileSync; |
| const resolve = require("path").resolve; |
| |
| /* |
| * Register a Handlebars helper that lets us generate Markdown lists that can support multi-line |
| * strings. This is driven by inconsistent formatting of breaking changes, which may be multiple |
| * lines long and can terminate the list early unintentionally. |
| */ |
| Handlebars.registerHelper("tf-a-mdlist", function (indent, options) { |
| const spaces = new Array(indent + 1).join(" "); |
| const first = spaces + "- "; |
| const nth = spaces + " "; |
| |
| return first + options.fn(this).replace(/\n(?!\s*\n)/gm, `\n${nth}`).trim() + "\n"; |
| }); |
| |
| /* |
| * Register a Handlebars helper that concatenates multiple variables. We use this to generate the |
| * title for the section partials. |
| */ |
| Handlebars.registerHelper("tf-a-concat", function () { |
| let argv = Array.prototype.slice.call(arguments, 0); |
| |
| argv.pop(); |
| |
| return argv.join(""); |
| }); |
| |
| function writerOpts(config) { |
| /* |
| * Flatten the configuration's sections list. This helps us iterate over all of the sections |
| * when we don't care about the hierarchy. |
| */ |
| |
| const flattenSections = function (sections) { |
| return sections.flatMap(section => { |
| const subsections = flattenSections(section.sections || []); |
| |
| return [section].concat(subsections); |
| }) |
| }; |
| |
| const flattenedSections = flattenSections(config.sections); |
| |
| /* |
| * Register a helper to return a restructured version of the note groups that includes notes |
| * categorized by their section. |
| */ |
| Handlebars.registerHelper("tf-a-notes", function (noteGroups, options) { |
| const generateTemplateData = function (sections, notes) { |
| return (sections || []).flatMap(section => { |
| const templateData = { |
| title: section.title, |
| sections: generateTemplateData(section.sections, notes), |
| notes: notes.filter(note => section.scopes?.includes(note.commit.scope)), |
| }; |
| |
| /* |
| * Don't return a section if it contains no notes and no sub-sections. |
| */ |
| if ((templateData.sections.length == 0) && (templateData.notes.length == 0)) { |
| return []; |
| } |
| |
| return [templateData]; |
| }); |
| }; |
| |
| return noteGroups.map(noteGroup => { |
| return { |
| title: noteGroup.title, |
| sections: generateTemplateData(config.sections, noteGroup.notes), |
| notes: noteGroup.notes.filter(note => |
| !flattenedSections.some(section => section.scopes?.includes(note.commit.scope))), |
| }; |
| }); |
| }); |
| |
| /* |
| * Register a helper to return a restructured version of the commit groups that includes commits |
| * categorized by their section. |
| */ |
| Handlebars.registerHelper("tf-a-commits", function (commitGroups, options) { |
| const generateTemplateData = function (sections, commits) { |
| return (sections || []).flatMap(section => { |
| const templateData = { |
| title: section.title, |
| sections: generateTemplateData(section.sections, commits), |
| commits: commits.filter(commit => section.scopes?.includes(commit.scope)), |
| }; |
| |
| /* |
| * Don't return a section if it contains no notes and no sub-sections. |
| */ |
| if ((templateData.sections.length == 0) && (templateData.commits.length == 0)) { |
| return []; |
| } |
| |
| return [templateData]; |
| }); |
| }; |
| |
| return commitGroups.map(commitGroup => { |
| return { |
| title: commitGroup.title, |
| sections: generateTemplateData(config.sections, commitGroup.commits), |
| commits: commitGroup.commits.filter(commit => |
| !flattenedSections.some(section => section.scopes?.includes(commit.scope))), |
| }; |
| }); |
| }); |
| |
| const writerOpts = ccWriterOpts(config) |
| .then(writerOpts => { |
| const ccWriterOptsTransform = writerOpts.transform; |
| |
| /* |
| * These configuration properties can't be injected directly into the template because |
| * they themselves are templates. Instead, we register them as partials, which allows |
| * them to be evaluated as part of the templates they're used in. |
| */ |
| Handlebars.registerPartial("commitUrl", config.commitUrlFormat); |
| Handlebars.registerPartial("compareUrl", config.compareUrlFormat); |
| Handlebars.registerPartial("issueUrl", config.issueUrlFormat); |
| |
| /* |
| * Register the partials that allow us to recursively create changelog sections. |
| */ |
| |
| const notePartial = readFileSync(resolve(__dirname, "./templates/note.hbs"), "utf-8"); |
| const noteSectionPartial = readFileSync(resolve(__dirname, "./templates/note-section.hbs"), "utf-8"); |
| const commitSectionPartial = readFileSync(resolve(__dirname, "./templates/commit-section.hbs"), "utf-8"); |
| |
| Handlebars.registerPartial("tf-a-note", notePartial); |
| Handlebars.registerPartial("tf-a-note-section", noteSectionPartial); |
| Handlebars.registerPartial("tf-a-commit-section", commitSectionPartial); |
| |
| /* |
| * Override the base templates so that we can generate a changelog that looks at least |
| * similar to the pre-Conventional Commits TF-A changelog. |
| */ |
| writerOpts.mainTemplate = readFileSync(resolve(__dirname, "./templates/template.hbs"), "utf-8"); |
| writerOpts.headerPartial = readFileSync(resolve(__dirname, "./templates/header.hbs"), "utf-8"); |
| writerOpts.commitPartial = readFileSync(resolve(__dirname, "./templates/commit.hbs"), "utf-8"); |
| writerOpts.footerPartial = readFileSync(resolve(__dirname, "./templates/footer.hbs"), "utf-8"); |
| |
| writerOpts.transform = function (commit, context) { |
| /* |
| * Feedback on the generated changelog has shown that having build system changes |
| * appear at the top of a section throws some people off. We make an exception for |
| * scopeless `build`-type changes and treat them as though they actually have the |
| * `build` scope. |
| */ |
| |
| if ((commit.type === "build") && (commit.scope == null)) { |
| commit.scope = "build"; |
| } |
| |
| /* |
| * Fix up commit trailers, which for some reason are not correctly recognized and |
| * end up showing up in the breaking changes. |
| */ |
| |
| commit.notes.forEach(note => { |
| const trailers = execa.sync("git", ["interpret-trailers", "--parse"], { |
| input: note.text |
| }).stdout; |
| |
| note.text = note.text.replace(trailers, "").trim(); |
| }); |
| |
| return ccWriterOptsTransform(commit, context); |
| }; |
| |
| return writerOpts; |
| }); |
| |
| return writerOpts; |
| } |
| |
| module.exports = function (parameter) { |
| const config = parameter || {}; |
| |
| return Q.all([ |
| ccConventionalChangelog(config), |
| ccParserOpts(config), |
| ccRecommendedBumpOpts(config), |
| writerOpts(config) |
| ]).spread(( |
| conventionalChangelog, |
| parserOpts, |
| recommendedBumpOpts, |
| writerOpts |
| ) => { |
| if (_.isFunction(parameter)) { |
| return parameter(null, { |
| gitRawCommitsOpts: { noMerges: null }, |
| conventionalChangelog, |
| parserOpts, |
| recommendedBumpOpts, |
| writerOpts |
| }); |
| } else { |
| return { |
| conventionalChangelog, |
| parserOpts, |
| recommendedBumpOpts, |
| writerOpts |
| }; |
| } |
| }); |
| }; |