changelog.js 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
  1. #!/usr/bin/env node
  2. // TODO(vojta): pre-commit hook for validating messages
  3. // TODO(vojta): report errors, currently Q silence everything which really sucks
  4. 'use strict';
  5. var child = require('child_process');
  6. var fs = require('fs');
  7. var util = require('util');
  8. var q = require('qq');
  9. var GIT_LOG_CMD = 'git log --grep="%s" -E --format=%s %s..HEAD';
  10. var GIT_TAG_CMD = 'git describe --tags --abbrev=0';
  11. var HEADER_TPL = '<a name="%s"></a>\n# %s (%s)\n\n';
  12. var LINK_ISSUE = '[#%s](https://github.com/esvit/ng-table/issues/%s)';
  13. var LINK_COMMIT = '[%s](https://github.com/esvit/ng-table/commit/%s)';
  14. var EMPTY_COMPONENT = '$$';
  15. var warn = function() {
  16. console.log('WARNING:', util.format.apply(null, arguments));
  17. };
  18. var parseRawCommit = function(raw) {
  19. if (!raw) return null;
  20. var lines = raw.split('\n');
  21. var msg = {}, match;
  22. msg.hash = lines.shift();
  23. msg.subject = lines.shift();
  24. msg.closes = [];
  25. msg.breaks = [];
  26. lines.forEach(function(line) {
  27. match = line.match(/(?:Closes|Fixes)\s#(\d+)/);
  28. if (match) msg.closes.push(parseInt(match[1]));
  29. });
  30. match = raw.match(/BREAKING CHANGE:([\s\S]*)/);
  31. if (match) {
  32. msg.breaking = match[1];
  33. }
  34. msg.body = lines.join('\n');
  35. match = msg.subject.match(/^(.*)\((.*)\)\:\s(.*)$/);
  36. if (!match || !match[1] || !match[3]) {
  37. warn('Incorrect message: %s %s', msg.hash, msg.subject);
  38. return null;
  39. }
  40. msg.type = match[1];
  41. msg.component = match[2];
  42. msg.subject = match[3];
  43. return msg;
  44. };
  45. var linkToIssue = function(issue) {
  46. return util.format(LINK_ISSUE, issue, issue);
  47. };
  48. var linkToCommit = function(hash) {
  49. return util.format(LINK_COMMIT, hash.substr(0, 8), hash);
  50. };
  51. var currentDate = function() {
  52. var now = new Date();
  53. var pad = function(i) {
  54. return ('0' + i).substr(-2);
  55. };
  56. return util.format('%d-%s-%s', now.getFullYear(), pad(now.getMonth() + 1), pad(now.getDate()));
  57. };
  58. var printSection = function(stream, title, section, printCommitLinks) {
  59. printCommitLinks = printCommitLinks === undefined ? true : printCommitLinks;
  60. var components = Object.getOwnPropertyNames(section).sort();
  61. if (!components.length) return;
  62. stream.write(util.format('\n## %s\n\n', title));
  63. components.forEach(function(name) {
  64. var prefix = '-';
  65. var nested = section[name].length > 1;
  66. if (name !== EMPTY_COMPONENT) {
  67. if (nested) {
  68. stream.write(util.format('- **%s:**\n', name));
  69. prefix = ' -';
  70. } else {
  71. prefix = util.format('- **%s:**', name);
  72. }
  73. }
  74. section[name].forEach(function(commit) {
  75. if (printCommitLinks) {
  76. stream.write(util.format('%s %s\n (%s', prefix, commit.subject, linkToCommit(commit.hash)));
  77. if (commit.closes.length) {
  78. stream.write(',\n ' + commit.closes.map(linkToIssue).join(', '));
  79. }
  80. stream.write(')\n');
  81. } else {
  82. stream.write(util.format('%s %s\n', prefix, commit.subject));
  83. }
  84. });
  85. });
  86. stream.write('\n');
  87. };
  88. var readGitLog = function(grep, from) {
  89. var deferred = q.defer();
  90. // TODO(vojta): if it's slow, use spawn and stream it instead
  91. child.exec(util.format(GIT_LOG_CMD, grep, '%H%n%s%n%b%n==END==', from), function(code, stdout, stderr) {
  92. var commits = [];
  93. stdout.split('\n==END==\n').forEach(function(rawCommit) {
  94. var commit = parseRawCommit(rawCommit);
  95. if (commit) commits.push(commit);
  96. });
  97. deferred.resolve(commits);
  98. });
  99. return deferred.promise;
  100. };
  101. var writeChangelog = function(stream, commits, version) {
  102. var sections = {
  103. fix: {},
  104. feat: {},
  105. perf: {},
  106. breaks: {}
  107. };
  108. sections.breaks[EMPTY_COMPONENT] = [];
  109. commits.forEach(function(commit) {
  110. var section = sections[commit.type];
  111. var component = commit.component || EMPTY_COMPONENT;
  112. if (section) {
  113. section[component] = section[component] || [];
  114. section[component].push(commit);
  115. }
  116. if (commit.breaking) {
  117. sections.breaks[component] = sections.breaks[component] || [];
  118. sections.breaks[component].push({
  119. subject: util.format("due to %s,\n %s", linkToCommit(commit.hash), commit.breaking),
  120. hash: commit.hash,
  121. closes: []
  122. });
  123. }
  124. });
  125. stream.write(util.format(HEADER_TPL, version, version, currentDate()));
  126. printSection(stream, 'Bug Fixes', sections.fix);
  127. printSection(stream, 'Features', sections.feat);
  128. printSection(stream, 'Performance Improvements', sections.perf);
  129. printSection(stream, 'Breaking Changes', sections.breaks, false);
  130. };
  131. var getPreviousTag = function() {
  132. var deferred = q.defer();
  133. child.exec(GIT_TAG_CMD, function(code, stdout, stderr) {
  134. if (code) deferred.reject('Cannot get the previous tag.');
  135. else deferred.resolve(stdout.replace('\n', ''));
  136. });
  137. return deferred.promise;
  138. };
  139. var generate = function(version, file) {
  140. getPreviousTag().then(function(tag) {
  141. console.log('Reading git log since', tag);
  142. readGitLog('^fix|^feat|^perf|BREAKING', tag).then(function(commits) {
  143. console.log('Parsed', commits.length, 'commits');
  144. console.log('Generating changelog to', file || 'stdout', '(', version, ')');
  145. writeChangelog(file ? fs.createWriteStream(file) : process.stdout, commits, version);
  146. });
  147. });
  148. };
  149. // publish for testing
  150. exports.parseRawCommit = parseRawCommit;
  151. exports.printSection = printSection;
  152. // hacky start if not run by jasmine :-D
  153. if (process.argv.join('').indexOf('jasmine-node') === -1) {
  154. generate(process.argv[2], process.argv[3]);
  155. }