diff --git a/tasks/git_changelog_generate.js b/tasks/git_changelog_generate.js index 76d7911..85aff6d 100644 --- a/tasks/git_changelog_generate.js +++ b/tasks/git_changelog_generate.js @@ -3,293 +3,313 @@ * https://github.com/rafinskipg/git-changelog */ +var fs = require('fs'); var child = require('child_process'); -var defaults = require('./defaults'); +var format = require('util').format; + var _ = require('lodash'); -var fs = require('fs'); -var util = require('util'); var q = require('q'); -var OPTS = {}; -var PROVIDER, GIT_LOG_CMD, GIT_NOTAG_LOG_CMD, - //ALLOWED_COMMITS = '^fix|^feat|^docs|BREAKING', - //git-describe - Show the most recent tag that is reachable from a commit - GIT_TAG_CMD = 'git describe --tags --abbrev=0', - HEADER_TPL = '%s\n# %s (%s)\n\n', - LINK_ISSUE, - LINK_COMMIT, - GIT_REPO_URL_CMD = 'git config --get remote.origin.url', - EMPTY_COMPONENT = '$$'; +var defaults = require('./defaults'); + +//ALLOWED_COMMITS = '^fix|^feat|^docs|BREAKING', +//git-describe - Show the most recent tag that is reachable from a commit +var GIT_TAG_CMD = 'git describe --tags --abbrev=0'; +var HEADER_TPL = '%s\n# %s (%s)\n\n'; +var GIT_REPO_URL_CMD = 'git config --get remote.origin.url'; +var EMPTY_COMPONENT = '$$'; +var GIT_LOG_CMD; +var GIT_NOTAG_LOG_CMD; +var LINK_ISSUE; +var LINK_COMMIT; // I have to clean that mess -function initOptions(params){ - OPTS = _.defaults(params, defaults); - OPTS.msg = 'name: '+ OPTS.app_name +';'; - OPTS.msg += 'file: '+ OPTS.file +';'; - OPTS.msg += 'grep_commits: '+ OPTS.grep_commits +';'; - OPTS.msg += 'debug: '+ OPTS.debug +';'; - OPTS.msg += 'version: '+ OPTS.version +';'; -} +var Changelog = function Changelog() { + this.options = {}; + this.options.msg = ''; +}; + +Changelog.prototype.message = function message() { + Array.prototype.slice.call(arguments).forEach(function(value, index) { + this.options.msg += (index ? ': ' : '') + value; + }, this); + + this.options.msg += ';'; +}; + +Changelog.prototype.initOptions = function initOptions(params) { + this.options = _.defaults(params, defaults); -var init = function(params){ + this.message('name', this.options.app_name); + this.message('file', this.options.file); + this.message('grep_commits', this.options.grep_commits); + this.message('debug', this.options.debug); + this.message('version', this.options.version); +}; + +Changelog.prototype.init = function init(params) { + var self = this; var deferred = q.defer(); - initOptions(params); + this.initOptions(params); + + this.getRepoUrl().then(function(url) { + var provider; + + self.options.repo_url = url; + self.message('remote', self.options.repo_url); - getRepoUrl().then(function(url){ - OPTS.repo_url = url; - OPTS.msg += 'remote: '+ OPTS.repo_url+';'; - //G \ B \ --- - PROVIDER = OPTS.repo_url.indexOf('github.com') !== -1 ? 'G' :'B'; + provider = self.options.repo_url.indexOf('github.com') !== -1 ? 'G' :'B'; //Log commits - GIT_LOG_CMD = 'git log ' + OPTS.branch_name + ' --grep="%s" -E --format=%s %s..HEAD'; - GIT_NOTAG_LOG_CMD = 'git log ' + OPTS.branch_name + ' --grep="%s" -E --format=%s'; + GIT_LOG_CMD = 'git log ' + self.options.branch_name + ' --grep="%s" -E --format=%s %s..HEAD'; + GIT_NOTAG_LOG_CMD = 'git log ' + self.options.branch_name + ' --grep="%s" -E --format=%s'; //This is just in case they differ their urls at some point in the future. Also brings the posibility of adding more providers LINK_ISSUE = ({ - G: '[#%s]('+OPTS.repo_url+'/issues/%s)', - B : '[#%s]('+OPTS.repo_url+'/issues/%s)'}) - [PROVIDER]; + G: '[#%s]('+self.options.repo_url+'/issues/%s)', + B : '[#%s]('+self.options.repo_url+'/issues/%s)'}) + [provider]; - LINK_COMMIT = ({ - G: '[%s]('+OPTS.repo_url+'/commit/%s)', - B: '[%s]('+OPTS.repo_url+'/commits/%s)'}) - [PROVIDER]; + LINK_COMMIT = ({ + G: '[%s]('+self.options.repo_url+'/commit/%s)', + B: '[%s]('+self.options.repo_url+'/commits/%s)'}) + [provider]; - deferred.resolve(OPTS); + deferred.resolve(self.options); }) - .catch(function(){ - OPTS.msg += 'not remote;'; + .catch(function() { + self.message('not remote'); deferred.reject("Sorry, you doesn't have configured any origin remote or passed a `repo_url` config value"); }); return deferred.promise; }; +Changelog.prototype.parseRawCommit = function parseRawCommit(raw) { + if (!raw) { + return null; + } -var parseRawCommit = function(raw) { - if (!raw) { return null; } - - var lines = raw.split('\n'); - var msg = {}, match; - - msg.closes = []; - msg.breaks = []; - - lines.forEach(function(line) { - match = line.match(/(?:Closes|Fixes)\s#(\d+)/); - if (match) { msg.closes.push(parseInt(match[1], 10)); } - }); + var lines = raw.split('\n'); + var msg = {}, match; - msg.hash = lines.shift(); - msg.subject = lines.shift(); + msg.closes = []; + msg.breaks = []; - match = raw.match(/BREAKING CHANGE:([\s\S]*)/); + lines.forEach(function(line) { + match = line.match(/(?:Closes|Fixes)\s#(\d+)/); if (match) { - msg.breaking = match[1]; + msg.closes.push(parseInt(match[1], 10)); } + }); + msg.hash = lines.shift(); + msg.subject = lines.shift(); - msg.body = lines.join('\n'); - match = msg.subject.match(/^(.*)\((.*)\)\:\s(.*)$/); + match = raw.match(/BREAKING CHANGE:([\s\S]*)/); + if (match) { + msg.breaking = match[1]; + } - if(!match){ - match = msg.subject.match(/^(.*)\:\s(.*)$/); - if(!match){ - warn('Incorrect message: %s %s', msg.hash, msg.subject); - return null; - } - msg.type = match[1]; - msg.subject = match[2]; + msg.body = lines.join('\n'); + match = msg.subject.match(/^(.*)\((.*)\)\:\s(.*)$/); - return msg; + if (!match) { + match = msg.subject.match(/^(.*)\:\s(.*)$/); + if (!match) { + this.warn('Incorrect message: %s %s', msg.hash, msg.subject); + return null; } - msg.type = match[1]; - msg.component = match[2]; - msg.subject = match[3]; + msg.subject = match[2]; + return msg; -}; + } + msg.type = match[1]; + msg.component = match[2]; + msg.subject = match[3]; -var linkToIssue = function(issue) { - return util.format(LINK_ISSUE, issue, issue); + return msg; }; - -var linkToCommit = function(hash) { - return util.format(LINK_COMMIT, hash.substr(0, 8), hash); +Changelog.prototype.linkToIssue = function linkToIssue(issue) { + return format(LINK_ISSUE, issue, issue); }; +Changelog.prototype.linkToCommit = function linkToCommit(hash) { + return format(LINK_COMMIT, hash.substr(0, 8), hash); +}; -var currentDate = function() { - var now = new Date(); - var pad = function(i) { - return ('0' + i).substr(-2); - }; +Changelog.prototype.currentDate = function currentDate() { + 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())); + return format('%d-%s-%s', now.getFullYear(), pad(now.getMonth() + 1), pad(now.getDate())); }; +Changelog.prototype.printSection = function printSection(stream, title, section, printCommitLinks) { + printCommitLinks = printCommitLinks === undefined ? true : printCommitLinks; + var components = Object.keys(section).sort(); + + if (!components.length) { + return; + } -var printSection = function(stream, title, section, printCommitLinks) { - printCommitLinks = printCommitLinks === undefined ? true : printCommitLinks; - var components = Object.getOwnPropertyNames(section).sort(); + stream.write(format('\n## %s\n\n', title)); - if (!components.length) {return; } + components.forEach(function(name) { + var prefix = '-'; + var nested = section[name].length > 1; - stream.write(util.format('\n## %s\n\n', title)); + if (name !== EMPTY_COMPONENT) { + if (nested) { + stream.write(format('- **%s:**\n', name)); + prefix = ' -'; + } else { + prefix = format('- **%s:**', name); + } + } - components.forEach(function(name) { - var prefix = '-'; - var nested = section[name].length > 1; + section[name].forEach(function(commit) { + if (printCommitLinks) { + stream.write(format('%s %s\n (%s', prefix, commit.subject, this.linkToCommit(commit.hash))); - if (name !== EMPTY_COMPONENT) { - if (nested) { - stream.write(util.format('- **%s:**\n', name)); - prefix = ' -'; - } else { - prefix = util.format('- **%s:**', name); - } + if (commit.closes.length) { + stream.write(',\n ' + commit.closes.map(this.linkToIssue).join(', ')); } + stream.write(')\n'); + } else { + stream.write(format('%s %s\n', prefix, commit.subject)); + } + }, this); + }, this); - 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\n', prefix, commit.subject)); - } - }); - }); + stream.write('\n'); +}; - stream.write('\n'); +Changelog.prototype.printSalute = function printSalute(stream) { + stream.write('\n\n---\n'); + stream.write('*Generated with [git-changelog](https://github.com/rafinskipg/git-changelog). If you have any problem or suggestion, create an issue.* :) **Thanks** '); }; -var printSalute = function(stream){ - stream.write('\n\n---\n'); - stream.write('*Generated with [git-changelog](https://github.com/rafinskipg/git-changelog). If you have any problem or suggestion, create an issue.* :) **Thanks** '); -} +Changelog.prototype.readGitLog = function prototype( git_log_command, from) { + var self = this; + var deferred = q.defer(); -var readGitLog = function( git_log_command, from) { - var deferred = q.defer(); + git_log_command = git_log_command === GIT_LOG_CMD ? format(git_log_command, this.options.grep_commits, '%H%n%s%n%b%n==END==', from) : format(git_log_command, this.options.grep_commits, '%H%n%s%n%b%n==END=='); - git_log_command = git_log_command === GIT_LOG_CMD ? util.format(git_log_command, OPTS.grep_commits, '%H%n%s%n%b%n==END==', from) : util.format(git_log_command, OPTS.grep_commits, '%H%n%s%n%b%n==END=='); - - log('Executing : ', git_log_command); + this.log('Executing : ', git_log_command); - child.exec(git_log_command , {timeout: 1000}, function(code, stdout, stderr) { + child.exec(git_log_command , {timeout: 1000}, function(code, stdout, stderr) { + var commits = []; - var commits = []; + stdout.split('\n==END==\n').forEach(function(rawCommit) { + var commit = self.parseRawCommit(rawCommit); + if (commit) { + commits.push(commit); + } + }); - stdout.split('\n==END==\n').forEach(function(rawCommit) { + deferred.resolve(commits); + }); - var commit = parseRawCommit(rawCommit); - if (commit) {commits.push(commit);} - }); + return deferred.promise; +}; - deferred.resolve(commits); - }); +Changelog.prototype.writeChangelog = function writeChangelog(stream, commits) { + var sections = { + fix: {}, + feat: {}, + breaks: {}, + style: {}, + refactor: {}, + test: {}, + chore: {}, + docs: {} + }; + + sections.breaks[EMPTY_COMPONENT] = []; + + this.organizeCommitsInSections(commits, sections); + + stream.write(format(HEADER_TPL, this.options.version, this.options.app_name, this.options.version, this.currentDate())); + this.printSection(stream, 'Bug Fixes', sections.fix); + this.printSection(stream, 'Features', sections.feat); + this.printSection(stream, 'Refactor', sections.refactor, false); + this.printSection(stream, 'Style', sections.style, false); + this.printSection(stream, 'Test', sections.test, false); + this.printSection(stream, 'Chore', sections.chore, false); + this.printSection(stream, 'Documentation', sections.docs, false); + if (sections.breaks[EMPTY_COMPONENT].length > 0 ) { + this.printSection(stream, 'Breaking Changes', sections.breaks, false); + } - return deferred.promise; + this.printSalute(stream); }; +Changelog.prototype.organizeCommitsInSections = function organizeCommitsInSections(commits, sections) { + commits.forEach(function(commit) { + var section = sections[commit.type]; + var component = commit.component || EMPTY_COMPONENT; -var writeChangelog = function(stream, commits) { - var sections = { - fix: {}, - feat: {}, - breaks: {}, - style: {}, - refactor: {}, - test: {}, - chore: {}, - docs: {} - }; - - sections.breaks[EMPTY_COMPONENT] = []; - - organizeCommitsInSections(commits, sections) - - stream.write(util.format(HEADER_TPL, OPTS.version, OPTS.app_name, OPTS.version, currentDate())); - printSection(stream, 'Bug Fixes', sections.fix); - printSection(stream, 'Features', sections.feat); - printSection(stream, 'Refactor', sections.refactor, false); - printSection(stream, 'Style', sections.style, false); - printSection(stream, 'Test', sections.test, false); - printSection(stream, 'Chore', sections.chore, false); - printSection(stream, 'Documentation', sections.docs, false); - if(sections.breaks[EMPTY_COMPONENT].length > 0 ) { - printSection(stream, 'Breaking Changes', sections.breaks, false); + if (section) { + section[component] = section[component] || []; + section[component].push(commit); } - printSalute(stream); -}; + if (commit.breaking) { + sections.breaks[component] = sections.breaks[component] || []; + sections.breaks[component].push({ + subject: format("due to %s,\n %s", this.linkToCommit(commit.hash), commit.breaking), + hash: commit.hash, + closes: [] + }); + } + }, this); -var organizeCommitsInSections = function(commits, sections){ - commits.forEach(function(commit) { - var section = sections[commit.type]; - var component = commit.component || EMPTY_COMPONENT; + return sections; +}; - if (section) { - section[component] = section[component] || []; - section[component].push(commit); - } +Changelog.prototype.getPreviousTag = function getPreviousTag() { + var deferred = q.defer(); - 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: [] - }); - } + if (this.options.tag) { + deferred.resolve(this.options.tag); + } else if (this.options.tag === false) { + deferred.resolve(false); + } else { + //IF we dont find a previous tag, we get all the commits from the beggining - The bigbang of the code + child.exec(GIT_TAG_CMD, function(code, stdout, stderr) { + if (code ) { + deferred.resolve(); + } else { + deferred.resolve(stdout.replace('\n', '')); + } }); - return sections; -} - - - -var getPreviousTag = function() { - var deferred = q.defer(); - if(OPTS.tag){ - deferred.resolve(OPTS.tag); - }else if(OPTS.tag === false){ - deferred.resolve(false); - }else{ - //IF we dont find a previous tag, we get all the commits from the beggining - The bigbang of the code - child.exec(GIT_TAG_CMD, function(code, stdout, stderr) { - if (code ){ deferred.resolve(); - }else{ - deferred.resolve(stdout.replace('\n', '')); - } - }); - } + } - return deferred.promise; + return deferred.promise; }; - -var getRepoUrl = function() { +Changelog.prototype.getRepoUrl = function getRepoUrl() { var deferred = q.defer(); - if(OPTS.repo_url){ - deferred.resolve(OPTS.repo_url); - }else{ + if (this.options.repo_url) { + deferred.resolve(this.options.repo_url); + } else { //IF we dont find a previous tag, we get all the commits from the beggining - The bigbang of the code child.exec(GIT_REPO_URL_CMD, function(code, stdout, stderr) { - if (code){ + if (code) { deferred.reject(); } else { stdout = stdout.replace('\n', '').replace('.git', ''); - deferred.resolve(stdout); + deferred.resolve(stdout); } }); } @@ -297,63 +317,59 @@ var getRepoUrl = function() { return deferred.promise; }; - -var generate = function(params) { +Changelog.prototype.generate = function generate(params) { + var self = this; var deferred = q.defer(); - init(params) - .then(function(){ - return getPreviousTag() - }) - .then(function(tag) { - var fn ; - - if(typeof(tag) !== 'undefined' && tag !== false){ - log('Reading git log since', tag); - OPTS.msg += 'since tag: '+ tag +';'; - fn = function(){ return readGitLog(GIT_LOG_CMD, tag);}; - }else{ - log('Reading git log since the beggining'); - OPTS.msg += 'since beggining;'; - fn = function(){ return readGitLog(GIT_NOTAG_LOG_CMD);}; + + this.init(params).then(function() { + return self.getPreviousTag(); + }).then(function(tag) { + var fn; + + if (typeof(tag) !== 'undefined' && tag !== false) { + self.log('Reading git log since', tag); + self.message('since tag', tag); + + fn = function() { + return self.readGitLog(GIT_LOG_CMD, tag); } - fn().then(function(commits) { - OPTS.msg += 'parsed commits:'+ commits.length +';'; - log('Parsed', commits.length, 'commits'); - log('Generating changelog to', OPTS.file || 'stdout', '(', OPTS.version, ')'); - writeChangelog(OPTS.file ? fs.createWriteStream(OPTS.file) : process.stdout, commits); - deferred.resolve(OPTS); - }) - .catch(function(err){ - console.log('error', err); - }) - - }) - .catch(function(err){ + }else{ + self.log('Reading git log since the beggining'); + self.message('since beggining'); + + fn = function() { + return self.readGitLog(GIT_NOTAG_LOG_CMD); + } + } + + fn().then(function(commits) { + self.message('parsed commits', commits.length); + self.log('Parsed', commits.length, 'commits'); + self.log('Generating changelog to', self.options.file || 'stdout', '(', self.options.version, ')'); + + self.writeChangelog(self.options.file ? fs.createWriteStream(self.options.file) : process.stdout, commits); + + deferred.resolve(self.options); + }).catch(function(err) { + console.log('error', err); + }); + }).catch(function(err) { console.log('Error generating changelog ', err); deferred.reject(err); - }) + }); return deferred.promise; }; - -function log(){ - if(OPTS.debug){ +Changelog.prototype.log = function log() { + if (this.options.debug) { console.log.apply(console, arguments); } -} - -var warn = function() { - if(OPTS.debug){ - console.log('WARNING:', util.format.apply(null, arguments)); - } }; +Changelog.prototype.warn = function warn() { + this.log('WARNING:', format.apply(null, arguments)); +}; -// publish for testing -exports.parseRawCommit = parseRawCommit; -exports.organizeCommitsInSections = organizeCommitsInSections; -exports.generate = generate; -exports.getRepoUrl = getRepoUrl; - +module.exports = new Changelog();