#!/usr/bin/env node // This script validates commit messages using the validator and outputs // valid commits to stdout and invalid ones to stderr. // // For more details see the help: // // $ node bin/validate -h // var program = require('commander'); var https = require('https'); var util = require('util'); var chalk = require('chalk'); var fs = require('fs'); var path = require('path'); var execSync = require('child_process').execSync; var readline = require('readline'); var CommitMessage = require('..'); var executable = path.relative(process.cwd(), process.argv[1]); program .description('Validate commit messages and output all valid commmits to ' + 'stdout and invalid ones to stderr.\n\n' + ' Examples:\n\n' + ' # validate messages directly\n' + ' node '+executable+' \'Fix bug\' \'Change color\'\n' + ' \n' + ' # validate from given file\n' + ' node '+executable+' file .git/COMMIT_EDITMSG\n' + ' \n' + ' # validate from a list of commit hashes\n' + ' echo "a243d1f 1060ada" | node '+executable+' stdin\n' + ' \n' + ' # validate last 5 commits (not including merge commits)\n' + ' git rev-list --all --no-merges -5 | node '+executable+' stdin\n' + ' git rev-list --all --since="2 weeks ago" | node '+executable+' stdin\n' + ' # validate last 3 commits and show list of paths each commit modified\n' + ' git log --format=%h --all -3 | node '+executable+' stdin -o \'--name-status\'\n' + ' # validate all commits made by \'\' (regex)\n' + ' git rev-list --all --author=\'\' | node '+executable+' stdin\n' + ' # show only commits that have warnings (Unix only)\n' + ' git rev-list --all | node '+executable+' stdin | grep -B 7 -A 2 \'^warning \'\n' + ' \n' + ' # validate github repositories\n' + ' node '+executable+' github clns/node-commit-msg\n' + ' node '+executable+' github -t git/git\n' + ' node '+executable+' github -v -l 120 torvalds/linux\n' + ' node '+executable+' github -c test/resources/angular --extra-config \'{"references": {"alwaysCheck": "never"}}\' angular/angular.js') .option('--no-colors', 'output without any colors') .option('-c, --config [s]', 'the dir path to the package.json config to use', process.cwd()) .option('--extra-config [o]', 'extra config object (as json) to overwrite the existing config', '{}'); program .command('* ') .description('Validate the given message(s)') .action(validateMessages); program .command('file ') .description('Validate the message(s) read from file(s)') .action(validateFile); program .command('stdin') .description('Validate commit hashes read from stdin, one per line or separated by spaces;\n' + ' each commit should be from the current directory\'s repository') .option('-o, --log-options ', 'options to pass to `git-log`, usually for pretty printing', '') .action(validateStdin); program .command('github ') .description('Validate a remote repository from GitHub') .option('-t, --token [s]', 'GitHub token; if not specified it will fetch anonymously') .option('-l, --limit [n]', 'how many commits to process, default is 100', 100) .option('-o, --offset [n]', 'the nth commit to start processing, default is 0', 0) .option('-v, --verbose', 'display GitHub rate limit') .action(validateGithub); program.parse(process.argv); function getConfig() { var config = CommitMessage.resolveConfigSync(program.config); return CommitMessage.Config(JSON.parse(program.extraConfig), CommitMessage.Config(config)); } // Process commits, based on method function validateMessages(msgs) { var message; var validateMsg = function() { message = msgs.shift(); if (!message) return; CommitMessage.parse(message, getConfig(), function(err, validator) { if (err) { console.error(err); process.exit(1); } outputCommit(validator); validateMsg(); // continue }); } validateMsg(); } function validateFile(files) { var msgs = []; files.forEach(function(f) { msgs.push(fs.readFileSync(f, {'encoding': 'utf8'})); }); validateMessages(msgs); } function validateStdin() { var command = this; var config = getConfig(); var rl = readline.createInterface({ input: process.stdin }); var commits = []; var started = false; var sha, cmd, message, output; var validateSha = function() { sha = commits.shift(); if (!sha) return; cmd = util.format('git show -s --format=%B %s', sha); try { message = execSync(cmd, { cwd: process.cwd(), encoding: 'utf8', stdio: [null] }); } catch(e) { console.error(e.toString()); console.error('Make sure the commit hash is found at the beginning of each line'); process.exit(1); } cmd = util.format('git log %s -1 %s', command.logOptions, sha); try { output = execSync(cmd, { cwd: process.cwd(), encoding: 'utf8', stdio: [null] }); } catch(e) { console.error(e.toString()); process.exit(1); } CommitMessage.parse(message, config, function(err, validator) { if (err) { console.error(err); process.exit(1); } outputCommit(validator, output); validateSha(); // continue }); } rl.on('line', function (line) { line.split(' ').forEach(function(c) { commits.push(c); }); if (!started) { validateSha(); started = true; } }); } function validateGithub(repository) { var command = this; var config = getConfig(); var limit = command.limit; var offset = command.offset; var defaultLimit = 100; // get maximum number of records for efficiency var ct = 0; var page = parseInt(offset / defaultLimit); var offset = offset - page * defaultLimit; var path = util.format('/repos/%s/commits', repository); var options = { hostname: 'api.github.com', path: path+'?page='+(page+1)+'&per_page='+defaultLimit, headers: { 'User-Agent': 'clns/node-commit-msg', 'Accept': 'application/vnd.github.v3+json' } }; if (command.token) { options.headers.Authorization = 'token ' + command.token; if (config.references && config.references.github && !config.references.github.token) { config.references.github.token = command.token; } } var body, commits, more, d, output; var handleCommit = function() { d = commits.shift(); if (d) { output = util.format( 'commit %s\n' + 'Author: %s <%s>\n' + 'Date: %s\n\n' + '%s', d.sha, d.commit.author.name, d.commit.author.email, d.commit.author.date, d.commit.message.replace(/^/gm, ' ') ); CommitMessage.parse(d.commit.message, config, function(err, validator) { if (err) { console.error(err); process.exit(1); } outputCommit(validator, output); validator = null; handleCommit(); // continue }); } else if (more) { options.path = path + '?page=' + (Math.ceil(ct / defaultLimit)+1) + '&per_page=' + defaultLimit; get(); } }; var get = function() { https.get(options, function(res) { if (command.verbose && res.headers && res.headers['x-ratelimit-limit']) { console.log('X-Ratelimit-Remaining: %d / %d', res.headers['x-ratelimit-remaining'], res.headers['x-ratelimit-limit']); } if (res.statusCode !== 200) { console.error('%d %s', res.statusCode, res.statusMessage); process.exit(1); } body = ''; res.on('data', function(chunk) { body += chunk.toString(); }); res.on('end', function () { commits = JSON.parse(body).slice(offset, limit-ct+offset); offset = 0; ct += commits.length; more = !(ct >= limit || commits.length < defaultLimit); handleCommit(); }); }); } // end GitHub get(); } // Output validation results var totalCommits = 0; var validCommits = 0; var commitsWithWarn = 0; // Output the given output (or validator.message if output not given) // to stdout or stderr, including the error messages. // Valid commits will be green. function outputCommit(validator, output) { if (!output) { output = validator.message; } totalCommits++; if (!validator.hasErrors()) { validCommits++; } if (validator.hasWarnings()) { commitsWithWarn++; } var hasMessages = validator.hasErrors() || validator.hasWarnings(); var log = validator.hasErrors() ? console.error : console.log; var logColor = function(o) { if (!program.noColors && !validator.hasErrors()) { o = chalk.green(o); } log(o); } logColor(output); if (hasMessages) { log(validator.formattedMessages); } log(); } process.on('beforeExit', function() { console.log('Summary:'); console.log('- %d/%d valid/total (with warnings: %d)', validCommits, totalCommits, commitsWithWarn); console.log('- %d% success rate and %d% warnings', totalCommits ? Math.ceil(validCommits / totalCommits * 100) : 0, totalCommits ? Math.ceil(commitsWithWarn / totalCommits * 100) : 0); if(totalCommits !== validCommits){ process.exit(10); } });