From a0406e1a62cbbefe3bedc8a8b01af7e27385f824 Mon Sep 17 00:00:00 2001 From: ocombe Date: Sun, 25 May 2014 22:54:32 +0200 Subject: [PATCH] feat: Added the changelog and release gulp tasks Usage: `gulp changelog` and `gulp release` The release task includes the changelog task. Fixes #62 --- changelog.js | 205 +++++++++++++++++++++++++++++++++++++++++++++++++++ gulpfile.js | 117 ++++++++++++++++++++++++++--- package.json | 18 +++-- 3 files changed, 322 insertions(+), 18 deletions(-) create mode 100644 changelog.js diff --git a/changelog.js b/changelog.js new file mode 100644 index 00000000..a9eeacb7 --- /dev/null +++ b/changelog.js @@ -0,0 +1,205 @@ +#!/usr/bin/env node + +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 = '\n# %s (%s)\n\n'; +var LINK_ISSUE = '[#%s](https://github.com/Famous/famous-angular/issues/%s)'; +var LINK_COMMIT = '[%s](https://github.com/Famous/famous-angular/commit/%s)'; + +var EMPTY_COMPONENT = '$$'; + + +var warn = function() { + console.error('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|Fixes)\s#(\d+)/); + if (match) msg.closes.push(parseInt(match[1])); + }); + + match = raw.match(/BREAKING CHANGE:([\s\S]*)/); + if (match) { + msg.breaking = match[1]; + } + + + msg.body = lines.join('\n'); + match = msg.subject.match(/^(\w*)\s?\:\s?(.*)$/); + + if (!match || !match[1] || !match[2]) { + warn('Incorrect message: %s %s', msg.hash, msg.subject); + return null; + } + + msg.type = match[1]; +// msg.component = match[2]; + msg.subject = match[2]; + + 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, printCommitLinks) { + printCommitLinks = printCommitLinks === undefined ? true : printCommitLinks; + var components = Object.getOwnPropertyNames(section).sort(); + + if (!components.length || section['$$'].length <= 0) return; + + stream.write(util.format('\n## %s\n\n', title)); + + components.forEach(function(name) { + var prefix = '-'; + var nested = section[name].length > 1; + + 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) { + if (printCommitLinks) { + stream.write(util.format('%s %s\n (%s', prefix, commit.subject, linkToCommit(commit.hash))); + if (commit.closes.length) { + stream.write(',\n ' + commit.closes.map(linkToIssue).join(', ')); + } + stream.write(')\n'); + } else { + stream.write(util.format('%s %s', prefix, commit.subject)); + } + }); + }); + + stream.write('\n'); +}; + + +var readGitLog = function(grep, from) { + var deferred = 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); + }); + + deferred.resolve(commits); + }); + + return deferred.promise; +}; + + +var writeChangelog = function(stream, commits, version) { + var sections = { + fix: {}, + feat: {}, + perf: {}, + docs: {}, + 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); + } + + if (commit.breaking) { + sections.breaks[component] = sections.breaks[component] || []; + sections.breaks[component].push({ + subject: util.format("due to %s,\n %s", linkToCommit(commit.hash), commit.breaking), + 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, 'Performance Improvements', sections.perf); + printSection(stream, 'Documentation', sections.docs); + printSection(stream, 'Breaking Changes', sections.breaks, false); +} + + +var getPreviousTag = function() { + var deferred = q.defer(); + child.exec(GIT_TAG_CMD, function(code, stdout, stderr) { + if (code) deferred.reject('Cannot get the previous tag.'); + else deferred.resolve(stdout.replace('\n', '')); + }); + return deferred.promise; +}; + + +var generate = function(version, file) { + getPreviousTag().then(function(tag) { +// console.log('Reading git log since', tag); + readGitLog('^fix|^feat|^perf|^docs|BREAKING', 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]); +} \ No newline at end of file diff --git a/gulpfile.js b/gulpfile.js index ccb1a7c6..e4d08304 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -1,10 +1,5 @@ -var SITE_DIR = 'famous-angular-docs/'; var EXAMPLES_DIR = 'famous-angular-examples/'; - var EXPRESS_PORT = 4000; -var EXPRESS_DOCS_ROOT = __dirname + '/' + SITE_DIR + '_site'; - -var LIVERELOAD_PORT = 35729; // Load plugins var gulp = require('gulp'), @@ -17,11 +12,9 @@ var gulp = require('gulp'), concat = require('gulp-concat'), notify = require('gulp-notify'), cache = require('gulp-cache'), - livereload = require('gulp-livereload'), - server = livereload(), gutil = require('gulp-util'), - pkg = require('./package.json'), - exec = require('gulp-exec'); + exec = require('gulp-exec'), + pkg = require('./package.json'); // Clean gulp.task('clean', function() { @@ -31,7 +24,6 @@ gulp.task('clean', function() { // Build for dist gulp.task('build', ['clean'], function(event) { - var header = require('gulp-header'); var banner = ['/**', ' * <%= pkg.name %> - <%= pkg.description %>', @@ -110,6 +102,7 @@ gulp.task('build-to-examples', ['clean'], function(event) { // Watch gulp.task('watch-examples', function(event) { + var livereload = require('gulp-livereload'); var server = livereload(); // Watch .js files gulp.watch([ @@ -124,6 +117,108 @@ gulp.task('watch-examples', function(event) { ); }); +var promptBump = function(callback) { + var prompt = require('gulp-prompt'); + var semver = require('semver'); + + return gulp.src('') + .pipe(prompt.prompt({ + type: 'list', + name: 'bump', + message: 'What type of version bump would you like to do ? (current version is '+pkg.version+')', + choices: [ + 'patch ('+pkg.version+' --> '+semver.inc(pkg.version, 'patch')+')', + 'minor ('+pkg.version+' --> '+semver.inc(pkg.version, 'minor')+')', + 'major ('+pkg.version+' --> '+semver.inc(pkg.version, 'major')+')', + 'none (exit)' + ] + }, function(res){var newVer; + if(res.bump.match(/^patch/)) { + newVer = semver.inc(pkg.version, 'patch'); + } else if(res.bump.match(/^minor/)) { + newVer = semver.inc(pkg.version, 'minor'); + } else if(res.bump.match(/^major/)) { + newVer = semver.inc(pkg.version, 'major'); + } + if(newVer && typeof callback === 'function') { + return callback(newVer); + } else { + return; + } + })); +} + +var makeChangelog = function(newVer) { + var streamqueue = require('streamqueue'); + var stream = streamqueue({ objectMode: true }); + + stream.queue(gulp.src('').pipe(exec('node ./changelog.js ' + newVer, { pipeStdout: true }))); + stream.queue(gulp.src('CHANGELOG.md')); + + return stream.done() + .pipe(concat('CHANGELOG.md')) + .pipe(gulp.dest('./')); +} + +// Make changelog +gulp.task('changelog', function(event) { + var prompt = require('gulp-prompt'); + var semver = require('semver'); + + return promptBump(makeChangelog); +}) + +gulp.task('release', ['docs'], function() { // docs task includes build task + var jeditor = require("gulp-json-editor"); + + return promptBump(function(newVer) { + var streamqueue = require('streamqueue'); + var stream = streamqueue({ objectMode: true }); + + // make the changelog + stream.queue(makeChangelog(newVer)); + + // update the main project version number + stream.queue( + gulp.src('package.json') + .pipe(jeditor({ + 'version': newVer + })) + .pipe(gulp.dest("./")) + ); + + stream.queue( + gulp.src('bower.json') + .pipe(jeditor({ + 'version': newVer + })) + .pipe(gulp.dest("./")) + ); + + // update docs dependency + stream.queue( + gulp.src('famous-angular-docs/bower.json') + .pipe(jeditor(function(json) { + json.dependencies['famous-angular'] = newVer; + return json; // must return JSON object. + })) + .pipe(gulp.dest("famous-angular-docs")) + ); + + // update examples dependency + stream.queue( + gulp.src(EXAMPLES_DIR+'bower.json') + .pipe(jeditor(function(json) { + json.dependencies['famous-angular'] = newVer; + return json; // must return JSON object. + })) + .pipe(gulp.dest(EXAMPLES_DIR)) + ); + + return stream.done(); + }); +}) + // Default task gulp.task('dev', function() { var express = require('express'); @@ -132,4 +227,4 @@ gulp.task('dev', function() { app.use(express.static(EXAMPLES_DIR + 'app/')); app.listen(EXPRESS_PORT); gulp.start('watch-examples'); -}); +}); \ No newline at end of file diff --git a/package.json b/package.json index 09657db0..1e5c9071 100755 --- a/package.json +++ b/package.json @@ -34,26 +34,30 @@ "gulp-exec": "^2.0.1", "gulp-header": "^1.0.2", "gulp-jshint": "^1.5.3", + "gulp-json-editor": "^2.0.2", "gulp-livereload": "^1.3.1", "gulp-minify-css": "~0.3.1", "gulp-notify": "^1.2.5", + "gulp-prompt": "^0.1.1", "gulp-rename": "^1.2.0", "gulp-stylus": "^1.0.0", "gulp-uglify": "^0.2.1", "gulp-util": "^2.2.14", "karma": "^0.12.1", + "karma-chrome-launcher": "~0.1.3", + "karma-firefox-launcher": "~0.1.3", "karma-jasmine": "~0.1.5", "karma-ng-html2js-preprocessor": "^0.1.0", "karma-ng-scenario": "^0.1.0", + "karma-requirejs": "~0.2.1", + "karma-spec-reporter": "0.0.13", "lodash": "^2.4.1", "minimist": "0.0.8", "mkdirp": "^0.5.0", - "semver": "^2.3.0", - "winston": "^0.7.3", - "karma-chrome-launcher": "~0.1.3", - "karma-firefox-launcher": "~0.1.3", - "karma-spec-reporter": "0.0.13", + "qq": "^0.3.5", "requirejs": "~2.1.11", - "karma-requirejs": "~0.2.1" + "semver": "^2.3.0", + "streamqueue": "0.0.7", + "winston": "^0.7.3" } -} +} \ No newline at end of file