index.js 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
  1. /*
  2. * Copyright (c) 2021, 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. * Fix up commit trailers, which for some reason are not correctly recognized and
  141. * end up showing up in the breaking changes.
  142. */
  143. commit.notes.forEach(note => {
  144. const trailers = execa.sync("git", ["interpret-trailers", "--parse"], {
  145. input: note.text
  146. }).stdout;
  147. note.text = note.text.replace(trailers, "").trim();
  148. });
  149. return ccWriterOptsTransform(commit, context);
  150. };
  151. return writerOpts;
  152. });
  153. return writerOpts;
  154. }
  155. module.exports = function (parameter) {
  156. const config = parameter || {};
  157. return Q.all([
  158. ccConventionalChangelog(config),
  159. ccParserOpts(config),
  160. ccRecommendedBumpOpts(config),
  161. writerOpts(config)
  162. ]).spread((
  163. conventionalChangelog,
  164. parserOpts,
  165. recommendedBumpOpts,
  166. writerOpts
  167. ) => {
  168. if (_.isFunction(parameter)) {
  169. return parameter(null, {
  170. gitRawCommitsOpts: { noMerges: null },
  171. conventionalChangelog,
  172. parserOpts,
  173. recommendedBumpOpts,
  174. writerOpts
  175. });
  176. } else {
  177. return {
  178. conventionalChangelog,
  179. parserOpts,
  180. recommendedBumpOpts,
  181. writerOpts
  182. };
  183. }
  184. });
  185. };