blob: 7d57c15040cfaaad8a69f7e70c10cc4855771106 [file] [log] [blame]
Chris Kay025c87f2021-11-09 20:05:38 +00001/*
Chris Kay1f8e7a92023-05-17 17:23:37 +01002 * Copyright (c) 2021-2023, Arm Limited. All rights reserved.
Chris Kay025c87f2021-11-09 20:05:38 +00003 *
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 /*
Chris Kay1f8e7a92023-05-17 17:23:37 +0100170 * 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 /*
Chris Kay025c87f2021-11-09 20:05:38 +0000181 * 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};