diff options
Diffstat (limited to 'changelog.js')
| -rwxr-xr-x | changelog.js | 201 | 
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]); +} | 
