xref: /rk3399_ARM-atf/tools/conventional-changelog-tf-a/index.js (revision 92a44d5587533cd359c29f871d4a3939f6f5bdc8)
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