index.js 8.7 KB

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