aboutsummaryrefslogtreecommitdiffstats
path: root/changelog.js
diff options
context:
space:
mode:
authorVojta Jina2012-03-17 03:10:03 -0700
committerIgor Minar2012-03-29 07:22:13 -0700
commit4557881cf84f168855fc8615e174f24d6c2dd6ce (patch)
treefbc956578b95dc1af6ffa85158fe55fbba44b175 /changelog.js
parentaf0ad6561c0d75c4f155b07e9cfc36a983af55bd (diff)
downloadangular.js-4557881cf84f168855fc8615e174f24d6c2dd6ce.tar.bz2
chore(release scripts): auto release scripts
Diffstat (limited to 'changelog.js')
-rwxr-xr-xchangelog.js201
1 files changed, 201 insertions, 0 deletions
diff --git a/changelog.js b/changelog.js
new file mode 100755
index 00000000..0083d29b
--- /dev/null
+++ b/changelog.js
@@ -0,0 +1,201 @@
+#!/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]);
+}