Skip to content

Commit

Permalink
feat: auto changelog from commits msg
Browse files Browse the repository at this point in the history
  • Loading branch information
ocombe committed Jul 14, 2014
1 parent 5a5d7f1 commit c089e08
Show file tree
Hide file tree
Showing 4 changed files with 426 additions and 15 deletions.
115 changes: 104 additions & 11 deletions gulpfile.js
Original file line number Diff line number Diff line change
@@ -1,26 +1,119 @@
var gulp = require('gulp');

gulp.task('build', function() {
var build = function(newVer) {
var rename = require('gulp-rename'),
uglify = require('gulp-uglify'),
header = require('gulp-header'),
pkg = require('./package.json'),
banner = ['/**',
' * <%= pkg.name %> - <%= pkg.description %>',
' * @version v<%= pkg.version %>',
' * @link <%= pkg.homepage %>',
' * @license <%= pkg.license %>',
' * @author <%= pkg.author %>',
' */',
''].join('\n');
' * <%= pkg.name %> - <%= pkg.description %>',
' * @version v<%= pkg.version %>',
' * @link <%= pkg.homepage %>',
' * @license <%= pkg.license %>',
' * @author <%= pkg.author %>',
' */',
''].join('\n');

return gulp.src('src/ocLazyLoad.js')
.pipe(header(banner, { pkg : pkg } ))
.pipe(header(banner, {pkg: pkg}))
.pipe(gulp.dest('dist'))
.pipe(gulp.dest('examples/example1/js/'))
.pipe(gulp.dest('examples/example2/js/'))
.pipe(uglify())
.pipe(header(banner, { pkg : pkg } ))
.pipe(header(banner, {pkg: pkg}))
.pipe(rename({suffix: '.min'}))
.pipe(gulp.dest('dist'));
});
}

gulp.task('build', function() {
return build();
});

var promptBump = function(callback) {
var prompt = require('gulp-prompt'),
semver = require('semver'),
pkg = require('./package.json');

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'),
stream = streamqueue({objectMode: true}),
exec = require('gulp-exec'),
concat = require('gulp-concat'),
clean = require('gulp-clean');

stream.queue(gulp.src('').pipe(exec('node ./src/changelog.js ' + newVer, {pipeStdout: true})));
stream.queue(gulp.src('CHANGELOG.md').pipe(clean()));

return stream.done()
.pipe(concat('CHANGELOG.md'))
.pipe(gulp.dest('./'));
}

// Make changelog
gulp.task('changelog', function(event) {
var prompt = require('gulp-prompt');

return promptBump(makeChangelog);
})

gulp.task('release', function() {
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("./"))
);

stream.queue(build(newVer));

return stream.done();
});
})

16 changes: 12 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "gulp build"
"build": "gulp build"
},
"repository": {
"type": "git",
Expand All @@ -29,9 +29,17 @@
"url": "https://github.com/ocombe/ocLazyLoad/issues"
},
"devDependencies": {
"gulp": "^3.8.0",
"gulp-header": "^1.0.2",
"gulp": "^3.8.6",
"gulp-clean": "^0.3.1",
"gulp-concat": "^2.3.3",
"gulp-exec": "^2.0.1",
"gulp-header": "^1.0.5",
"gulp-json-editor": "^2.0.2",
"gulp-prompt": "^0.1.1",
"gulp-rename": "^1.2.0",
"gulp-uglify": "^0.3.0"
"gulp-uglify": "^0.3.1",
"qq": "^0.3.5",
"semver": "^2.3.1",
"streamqueue": "^0.1.1"
}
}
204 changes: 204 additions & 0 deletions src/changelog.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
#!/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 = '<a name="%s"></a>\n# %s (%s)\n\n';
var LINK_ISSUE = '[#%s](https://github.com/ocombe/ocLazyLoad/issues/%s)';
var LINK_COMMIT = '[%s](https://github.com/ocombe/ocLazyLoad/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]);
}
Loading

0 comments on commit c089e08

Please sign in to comment.