![]()
Cannot retrieve contributors at this time
| #!/usr/bin/env node | |
| // TODO(vojta): pre-commit hook for validating messages | |
| // TODO(vojta): report errors, currently Q silence everything which really sucks | |
| var child = require('child_process'); | |
| var fs = require('fs'); | |
| var util = require('util'); | |
| var q = require('qq'); | |
| var GIT_LOG_CMD = 'git log --grep="%s" -E --format=%s %s..HEAD'; | |
| var GIT_TAG_CMD = 'git describe --tags --abbrev=0'; | |
| var HEADER_TPL = '<a name="%s"></a>\n# %s (%s)\n\n'; | |
| var LINK_ISSUE = '[#%s](https://github.com/angular/angular.js/issues/%s)'; | |
| var LINK_COMMIT = '[%s](https://github.com/angular/angular.js/commit/%s)'; | |
| var EMPTY_COMPONENT = '$$'; | |
| var MAX_SUBJECT_LENGTH = 80; | |
| var warn = function() { | |
| console.log('WARNING:', util.format.apply(null, arguments)); | |
| }; | |
| var parseRawCommit = function(raw) { | |
| if (!raw) return null; | |
| var lines = raw.split('\n'); | |
| var msg = {}, match; | |
| msg.hash = lines.shift(); | |
| msg.subject = lines.shift(); | |
| msg.closes = []; | |
| msg.breaks = []; | |
| lines.forEach(function(line) { | |
| match = line.match(/Closes\s#(\d+)/); | |
| if (match) msg.closes.push(parseInt(match[1])); | |
| match = line.match(/Breaks\s(.*)/); | |
| if (match) msg.breaks.push(match[1]); | |
| }); | |
| msg.body = lines.join('\n'); | |
| match = msg.subject.match(/^(.*)\((.*)\)\:\s(.*)$/); | |
| if (!match || !match[1] || !match[3]) { | |
| warn('Incorrect message: %s %s', msg.hash, msg.subject); | |
| return null; | |
| } | |
| if (match[3].length > MAX_SUBJECT_LENGTH) { | |
| warn('Too long subject: %s %s', msg.hash, msg.subject); | |
| match[3] = match[3].substr(0, MAX_SUBJECT_LENGTH); | |
| } | |
| msg.type = match[1]; | |
| msg.component = match[2]; | |
| msg.subject = match[3]; | |
| return msg; | |
| }; | |
| var linkToIssue = function(issue) { | |
| return util.format(LINK_ISSUE, issue, issue); | |
| }; | |
| var linkToCommit = function(hash) { | |
| return util.format(LINK_COMMIT, hash.substr(0, 8), hash); | |
| }; | |
| var currentDate = function() { | |
| var now = new Date(); | |
| var pad = function(i) { | |
| return ('0' + i).substr(-2); | |
| }; | |
| return util.format('%d-%s-%s', now.getFullYear(), pad(now.getMonth() + 1), pad(now.getDate())); | |
| }; | |
| var printSection = function(stream, title, section) { | |
| var NESTED = true; | |
| var components = Object.getOwnPropertyNames(section).sort(); | |
| if (!components.length) return; | |
| stream.write(util.format('\n## %s\n\n', title)); | |
| components.forEach(function(name) { | |
| var prefix = '-'; | |
| if (name !== EMPTY_COMPONENT) { | |
| if (NESTED) { | |
| stream.write(util.format('- **%s:**\n', name)); | |
| prefix = ' -'; | |
| } else { | |
| prefix = util.format('- **%s:**', name); | |
| } | |
| } | |
| section[name].forEach(function(commit) { | |
| stream.write(util.format('%s %s (%s', prefix, commit.subject, linkToCommit(commit.hash))); | |
| if (commit.closes.length) { | |
| stream.write(', closes ' + commit.closes.map(linkToIssue).join(', ')); | |
| } | |
| stream.write(')\n'); | |
| }); | |
| }); | |
| stream.write('\n'); | |
| }; | |
| var readGitLog = function(grep, from) { | |
| var deffered = q.defer(); | |
| // TODO(vojta): if it's slow, use spawn and stream it instead | |
| child.exec(util.format(GIT_LOG_CMD, grep, '%H%n%s%n%b%n==END==', from), function(code, stdout, stderr) { | |
| var commits = []; | |
| stdout.split('\n==END==\n').forEach(function(rawCommit) { | |
| var commit = parseRawCommit(rawCommit); | |
| if (commit) commits.push(commit); | |
| }); | |
| deffered.resolve(commits); | |
| }); | |
| return deffered.promise; | |
| }; | |
| var writeChangelog = function(stream, commits, version) { | |
| var sections = { | |
| fix: {}, | |
| feat: {}, | |
| breaks: {} | |
| }; | |
| sections.breaks[EMPTY_COMPONENT] = []; | |
| commits.forEach(function(commit) { | |
| var section = sections[commit.type]; | |
| var component = commit.component || EMPTY_COMPONENT; | |
| if (section) { | |
| section[component] = section[component] || []; | |
| section[component].push(commit); | |
| } | |
| commit.breaks.forEach(function(breakMsg) { | |
| sections.breaks[EMPTY_COMPONENT].push({ | |
| subject: breakMsg, | |
| hash: commit.hash, | |
| closes: [] | |
| }); | |
| }); | |
| }); | |
| stream.write(util.format(HEADER_TPL, version, version, currentDate())); | |
| printSection(stream, 'Bug Fixes', sections.fix); | |
| printSection(stream, 'Features', sections.feat); | |
| printSection(stream, 'Breaking Changes', sections.breaks); | |
| } | |
| var getPreviousTag = function() { | |
| var deffered = q.defer(); | |
| child.exec(GIT_TAG_CMD, function(code, stdout, stderr) { | |
| if (code) deffered.reject('Cannot get the previous tag.'); | |
| else deffered.resolve(stdout.replace('\n', '')); | |
| }); | |
| return deffered.promise; | |
| }; | |
| var generate = function(version, file) { | |
| getPreviousTag().then(function(tag) { | |
| console.log('Reading git log since', tag); | |
| readGitLog('^fix|^feat|Breaks', tag).then(function(commits) { | |
| console.log('Parsed', commits.length, 'commits'); | |
| console.log('Generating changelog to', file || 'stdout', '(', version, ')'); | |
| writeChangelog(file ? fs.createWriteStream(file) : process.stdout, commits, version); | |
| }); | |
| }); | |
| }; | |
| // publish for testing | |
| exports.parseRawCommit = parseRawCommit; | |
| // hacky start if not run by jasmine :-D | |
| if (process.argv.join('').indexOf('jasmine-node') === -1) { | |
| generate(process.argv[2], process.argv[3]); | |
| } |