#!/usr/bin/env node var arrayToTable = require('array-to-table') var program = require('commander') var breeze = require('breeze') var github = require("github") var semver = require('semver') var moment = require('moment') var pkg = require('../package.json') var log = require('npmlog') var file = require('easy-file') // Directory Location var configurationFileLocation = [__dirname, '.config'].join('/') // Regex Helpers var scopeRegex = /^\w+\s?\(([^\)]+)\)\:/ var shaRegex = /(\b[0-9a-f]{7,40})/img var issueRegex = /\#([0-9]+)/ig // Configure logger log.heading = "niji" program.version(pkg.version) program.option('-o, --out [directory]', 'Location of the directory to output changelog') program.option('-r, --repo [name]', 'Repository name') program.option('-u, --user [name]', 'Repository username') program.option('-h, --head [name]', 'Commit head') program.option('-b, --base [name]', 'Commit base') program.option('-f, --format [option]', 'Changelog Format Type (table (default), node, node+, angular)') program.option('-s, --sort [option]', 'Sorting option (time (default), type, scope, author)') program.option('-t, --token [token]', 'Github Token') program.option('-c, --configure [token]', 'Configure github token for future reference') program.parse(process.argv) function getVersion (head) { if (!head) return 'Unknown' var parts = head.split('/') return head.indexOf('/') > -1 ? 'v' + semver.clean(parts[parts.length-1]) : head } function sort (fields) { return function (a, b) { return fields.map(function (o) { var dir = 1 if (o[0] === '-') dir = -1, o=o.substring(1) return a[o] > b[o] ? dir : a[o] < b[o] ? -(dir) : 0 }).reduce(function (p, n) { return p ? p : n }, 0) } } function determineType (message) { message = message ? message.toLowerCase() : '' switch (true) { case (message.indexOf('feat') === 0): return 'Feature' case (message.indexOf('docs') === 0): return 'Documentation' case (message.indexOf('style') === 0): return 'Style' case (message.indexOf('test') === 0): return 'Tests' case (message.indexOf('perf') === 0): return 'Performance' case (message.indexOf('chore') === 0): return 'Chore' case (message.indexOf('release') === 0): return 'Release' case (message.indexOf('misc') === 0): return 'Misc' case (message.indexOf('fix') !== -1): return 'Fixed' case (message.indexOf('revert') !== -1): return 'Reverted' case (message.indexOf('add') !== -1): return 'Added' case (message.indexOf('change') !== -1): return 'Changed' default: return 'Changed' } } function determineScope (message) { var matches = message.match(scopeRegex) return matches && matches.length ? matches[1] : "" } function determineReferences (message) { var repoUrl = [ 'https://github.com/', program.user, '/', program.repo, '/' ].join('') var issues = message.match(issueRegex) var output = "" ;(issues || []).forEach(function (id) { output += "[`" + id + "`](" + repoUrl + "commit/" + id + "), " }) return output.length ? output.substr(0, output.length - 2) : output } function determineDescription (message) { var repoUrl = [ 'https://github.com/', program.user, '/', program.repo, '/' ].join('') var index // Get first entry message = message.split("\n") message = message.shift() // Remove type and scope. if (message.indexOf(':') > -1) { message = message.split(":") message.shift() message = message.join(":") } // Replace issues with link ;(message.match(issueRegex) || []).forEach(function (issueId) { var id = issueId.replace('#', '') message = message.replace(issueId, '[`' + issueId + '`](' + repoUrl + 'issues/' + id + ')') }) // Replace sha with link ;(message.match(shaRegex) || []).forEach(function (sha) { message = message.replace(sha, '[`' + sha.substr(0, 7) + '`](' + repoUrl + 'commit/' + sha + ')') }) // Return message return message } function generateAngularChangelog (commits) { var version = getVersion(program.head) let versionLink = ['[`', version, '`](https://github.com/', program.user, '/', program.repo, '/tree/', program.head, ')'].join('') let changelog = {} let output = [] output.push(['## ', versionLink, ' ', `(${ moment().format('YYYY-MM-DD') })`].join('')) output.push('') commits.filter(commit => commit.Type && commit.Scope).sort((a, b) => { let aScope = a.Scope.toLowerCase() let bScope = b.Scope.toLowerCase() return aScope === bScope ? 0 : aScope < bScope ? -1 : 1 }).forEach(commit => { if (!commit.Type || !commit.Scope) return if (!changelog[commit.Type]) { changelog[commit.Type] = [] } changelog[commit.Type].push([ '-', `**${commit.Scope}:**`, commit.Description, `(${commit.Link})` ].join(' ')) }) Object.keys(changelog).forEach(type => { output.push([ `### ${type}`, changelog[type].join('\r\n') ].join('\r\n')) output.push('') }) return output.join('\r\n') } function generateNodeChangelogResult (commits) { var output = [] var version = getVersion(program.head) output.push(['## ', moment().format('YYYY-MM-DD'), ', Version ', version].join('')) output.push('### Notable Changes') output.push('') output.push('- Enter Notable Changes Here') output.push('') output.push('### Commits') commits.forEach(function (commit) { output.push(['- [', commit.Link, '] ' + (commit.Scope ? ('**' + commit.Type + '(' + commit.Scope + ')' + ':** ') : ''), commit.Description, ' (', commit.Author, ')'].join('')) }) return output.join("\r\n") } function generateDefaultChangelogResult (commits) { var output = [] var version = getVersion(program.head) output.push(['## [', version, '] | ', moment().format('YYYY-MM-DD')].join('')) output.push(['*branch:', '[`', program.head, '`](https://github.com/', program.user, '/', program.repo, '/tree/', program.head, ')*'].join('')) output.push('') try { output.push(arrayToTable(commits)) } catch (e) { console.log(e) } output.push(['[', version, ']: ', 'https://github.com/', program.user, '/', program.repo, '/compare/', program.base, '...', program.head].join('')) return output.join("\r\n") } function compareCommits (gh) { return function (next) { gh.repos.compareCommits({ user: program.user, repo: program.repo, base: program.base, head: program.head }, next) } } function transformCommits (next, results) { var commits = [] results.commits.forEach(function (entry) { var message = entry.commit.message.toLowerCase() var uri = entry.html_url.split('/') if (message.indexOf('merge') === 0 || message.indexOf(':') === -1) { return } commits.push({ "Type": determineType(entry.commit.message), "Scope": determineScope(entry.commit.message), "Link": ['[`', uri[uri.length - 1].substring(0, 10) ,'`](', entry.html_url, ')'].join(''), "Description": determineDescription(entry.commit.message), "References": determineReferences(entry.commit.message), "Author": entry.commit.author.name }) }) if (program.sort === 'type,scope') { commits = commits.sort(sort(['Type', 'Scope'])) } if (program.sort === 'scope,type') { commits = commits.sort(sort(['Scope', 'Type'])) } if (program.sort === 'scope') { commits = commits.sort(sort(['Scope'])) } if (program.sort === 'type') { commits = commits.sort(sort(['Type'])) } if (program.sort === 'author') { commits = commits.sort(sort(['Author'])) } next(null, commits) } function outputReleaseResultsFile (next, commits) { console.info('coming soon...') } function consoleLogReleaseResults (next, commits) { console.log( program.format === 'node' ? generateNodeChangelogResult(commits) : program.format === 'angular' ? generateAngularChangelog(commits) : generateDefaultChangelogResult(commits) ) } if (program.configure) { log.info('setting', 'configuration file: %s', configurationFileLocation) log.info('config', 'set "token" "%s"', program.configure) try { file.write(configurationFileLocation, JSON.stringify({ token: program.configure }, null, 2)) } catch (e) { log.error('config', e.message) process.exit(1) } finally { log.info('ok') process.exit(0) } } file.read(configurationFileLocation, function (config) { var flow = breeze() var gh = new github({ version: "3.0.0", headers: { "user-agent": "changelog-generator" } }) if (!program.token && config) { config = JSON.parse(config) program.token = config.token } gh.authenticate({ type: "token", token: program.token }) flow.then(compareCommits(gh)) flow.then(transformCommits) flow.some(program.out, outputReleaseResultsFile) flow.none(consoleLogReleaseResults) flow.catch(function onError (err) { console.error('Unable to generate release results:', err.message) console.error(err.stack) }) })