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