writer-opts.js 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. 'use strict'
  2. const addBangNotes = require('./add-bang-notes')
  3. const compareFunc = require('compare-func')
  4. const Q = require('q')
  5. const readFile = Q.denodeify(require('fs').readFile)
  6. const resolve = require('path').resolve
  7. const releaseAsRe = /release-as:\s*\w*@?([0-9]+\.[0-9]+\.[0-9a-z]+(-[0-9a-z.]+)?)\s*/i
  8. /**
  9. * Handlebar partials for various property substitutions based on commit context.
  10. */
  11. const owner = '{{#if this.owner}}{{~this.owner}}{{else}}{{~@root.owner}}{{/if}}'
  12. const host = '{{~@root.host}}'
  13. const repository = '{{#if this.repository}}{{~this.repository}}{{else}}{{~@root.repository}}{{/if}}'
  14. module.exports = function (config) {
  15. config = defaultConfig(config)
  16. const commitUrlFormat = expandTemplate(config.commitUrlFormat, {
  17. host,
  18. owner,
  19. repository
  20. })
  21. const compareUrlFormat = expandTemplate(config.compareUrlFormat, {
  22. host,
  23. owner,
  24. repository
  25. })
  26. const issueUrlFormat = expandTemplate(config.issueUrlFormat, {
  27. host,
  28. owner,
  29. repository,
  30. id: '{{this.issue}}',
  31. prefix: '{{this.prefix}}'
  32. })
  33. return Q.all([
  34. readFile(resolve(__dirname, './templates/template.hbs'), 'utf-8'),
  35. readFile(resolve(__dirname, './templates/header.hbs'), 'utf-8'),
  36. readFile(resolve(__dirname, './templates/commit.hbs'), 'utf-8'),
  37. readFile(resolve(__dirname, './templates/footer.hbs'), 'utf-8')
  38. ])
  39. .spread((template, header, commit, footer) => {
  40. const writerOpts = getWriterOpts(config)
  41. writerOpts.mainTemplate = template
  42. writerOpts.headerPartial = header
  43. .replace(/{{compareUrlFormat}}/g, compareUrlFormat)
  44. writerOpts.commitPartial = commit
  45. .replace(/{{commitUrlFormat}}/g, commitUrlFormat)
  46. .replace(/{{issueUrlFormat}}/g, issueUrlFormat)
  47. writerOpts.footerPartial = footer
  48. return writerOpts
  49. })
  50. }
  51. function findTypeEntry (types, commit) {
  52. const typeKey = (commit.revert ? 'revert' : (commit.type || '')).toLowerCase()
  53. return types.find((entry) => {
  54. if (entry.type !== typeKey) {
  55. return false
  56. }
  57. if (entry.scope && entry.scope !== commit.scope) {
  58. return false
  59. }
  60. return true
  61. })
  62. }
  63. function getWriterOpts (config) {
  64. config = defaultConfig(config)
  65. return {
  66. transform: (commit, context) => {
  67. let discard = true
  68. const issues = []
  69. const entry = findTypeEntry(config.types, commit)
  70. // adds additional breaking change notes
  71. // for the special case, test(system)!: hello world, where there is
  72. // a '!' but no 'BREAKING CHANGE' in body:
  73. addBangNotes(commit)
  74. // Add an entry in the CHANGELOG if special Release-As footer
  75. // is used:
  76. if ((commit.footer && releaseAsRe.test(commit.footer)) ||
  77. (commit.body && releaseAsRe.test(commit.body))) {
  78. discard = false
  79. }
  80. commit.notes.forEach(note => {
  81. note.title = 'BREAKING CHANGES'
  82. discard = false
  83. })
  84. // breaking changes attached to any type are still displayed.
  85. if (discard && (entry === undefined ||
  86. entry.hidden)) return
  87. if (entry) commit.type = entry.section
  88. if (commit.scope === '*') {
  89. commit.scope = ''
  90. }
  91. if (typeof commit.hash === 'string') {
  92. commit.shortHash = commit.hash.substring(0, 7)
  93. }
  94. if (typeof commit.subject === 'string') {
  95. // Issue URLs.
  96. config.issuePrefixes.join('|')
  97. const issueRegEx = '(' + config.issuePrefixes.join('|') + ')' + '([0-9]+)'
  98. const re = new RegExp(issueRegEx, 'g')
  99. commit.subject = commit.subject.replace(re, (_, prefix, issue) => {
  100. issues.push(prefix + issue)
  101. const url = expandTemplate(config.issueUrlFormat, {
  102. host: context.host,
  103. owner: context.owner,
  104. repository: context.repository,
  105. id: issue,
  106. prefix: prefix
  107. })
  108. return `[${prefix}${issue}](${url})`
  109. })
  110. // User URLs.
  111. commit.subject = commit.subject.replace(/\B@([a-z0-9](?:-?[a-z0-9/]){0,38})/g, (_, user) => {
  112. // TODO: investigate why this code exists.
  113. if (user.includes('/')) {
  114. return `@${user}`
  115. }
  116. const usernameUrl = expandTemplate(config.userUrlFormat, {
  117. host: context.host,
  118. owner: context.owner,
  119. repository: context.repository,
  120. user: user
  121. })
  122. return `[@${user}](${usernameUrl})`
  123. })
  124. }
  125. // remove references that already appear in the subject
  126. commit.references = commit.references.filter(reference => {
  127. if (issues.indexOf(reference.prefix + reference.issue) === -1) {
  128. return true
  129. }
  130. return false
  131. })
  132. return commit
  133. },
  134. groupBy: 'type',
  135. // the groupings of commit messages, e.g., Features vs., Bug Fixes, are
  136. // sorted based on their probable importance:
  137. commitGroupsSort: (a, b) => {
  138. const commitGroupOrder = ['Reverts', 'Performance Improvements', 'Bug Fixes', 'Features']
  139. const gRankA = commitGroupOrder.indexOf(a.title)
  140. const gRankB = commitGroupOrder.indexOf(b.title)
  141. if (gRankA >= gRankB) {
  142. return -1
  143. } else {
  144. return 1
  145. }
  146. },
  147. commitsSort: ['scope', 'subject'],
  148. noteGroupsSort: 'title',
  149. notesSort: compareFunc
  150. }
  151. }
  152. // merge user set configuration with default configuration.
  153. function defaultConfig (config) {
  154. config = config || {}
  155. config.types = config.types || [
  156. { type: 'feat', section: 'Features' },
  157. { type: 'feature', section: 'Features' },
  158. { type: 'fix', section: 'Bug Fixes' },
  159. { type: 'perf', section: 'Performance Improvements' },
  160. { type: 'revert', section: 'Reverts' },
  161. { type: 'docs', section: 'Documentation', hidden: true },
  162. { type: 'style', section: 'Styles', hidden: true },
  163. { type: 'chore', section: 'Miscellaneous Chores', hidden: true },
  164. { type: 'refactor', section: 'Code Refactoring', hidden: true },
  165. { type: 'test', section: 'Tests', hidden: true },
  166. { type: 'build', section: 'Build System', hidden: true },
  167. { type: 'ci', section: 'Continuous Integration', hidden: true }
  168. ]
  169. config.issueUrlFormat = config.issueUrlFormat ||
  170. '{{host}}/{{owner}}/{{repository}}/issues/{{id}}'
  171. config.commitUrlFormat = config.commitUrlFormat ||
  172. '{{host}}/{{owner}}/{{repository}}/commit/{{hash}}'
  173. config.compareUrlFormat = config.compareUrlFormat ||
  174. '{{host}}/{{owner}}/{{repository}}/compare/{{previousTag}}...{{currentTag}}'
  175. config.userUrlFormat = config.userUrlFormat ||
  176. '{{host}}/{{user}}'
  177. config.issuePrefixes = config.issuePrefixes || ['#']
  178. return config
  179. }
  180. // expand on the simple mustache-style templates supported in
  181. // configuration (we may eventually want to use handlebars for this).
  182. function expandTemplate (template, context) {
  183. let expanded = template
  184. Object.keys(context).forEach(key => {
  185. expanded = expanded.replace(new RegExp(`{{${key}}}`, 'g'), context[key])
  186. })
  187. return expanded
  188. }