UNPKG

ggit

Version:

Local promise-returning git command wrappers

270 lines (242 loc) 7.58 kB
var spawn = require('child_process').spawn; var path = require('path'); var fs = require('fs'); var moment = require('moment'); // todo: get back to working state // var metrics = require('./metrics'); var optimist = require('optimist'); var Table = require('cli-table'); var config = { filename: '', report: '', commits: 30 }; function parseCommit(data) { console.assert(data, 'null commit data'); var lines = data.split('\n'); console.assert(lines.length > 3, 'invalid commit\n', data); // console.log('commit lines\n', lines); var commitLine = lines[0].trim(); var authorLine = lines[1].split(':')[1].trim(); var dateLine = lines[2]; dateLine = dateLine.substr(dateLine.indexOf(':') + 1); dateLine = dateLine.trim(); // console.log(dateLine); lines.splice(0, 3); lines = lines.filter(function (str) { return str; }); lines = lines.map(function (str) { str = str.trim(); return str; }); var description = lines.join('\n'); return { commit: commitLine, author: authorLine, date: moment(dateLine), description: description }; } function getGitRootFolder(cb) { console.assert(typeof cb === 'function', 'expect callback function, not', cb); var git = spawn('git', ['rev-parse', '--show-toplevel']); var topLevelFolder = null; git.stdout.setEncoding('utf-8'); git.stdout.on('data', function (data) { data.trim(); if (/fatal/.test(data)) { throw new Error('Could not determine git top folder\n' + data); } topLevelFolder = data.trim(); }); git.stderr.setEncoding('utf-8'); git.stderr.on('data', function (data) { throw new Error('Could not determine git top folder\n' + data); }); git.on('exit', function (code) { cb(topLevelFolder); }); } function getFileRevision(commit, filename, cb) { console.assert(commit, 'missing commit code'); console.assert(filename, 'missing filename'); var args = ['show', commit + ':' + filename]; console.log('getting file revision', args); var git = spawn('git', args); var contents = ''; git.stdout.setEncoding('utf-8'); git.stdout.on('data', function (data) { data.trim(); // console.log(data); contents += data; }); git.stderr.setEncoding('utf-8'); git.stderr.on('data', function (data) { throw new Error('Could not get file\n' + filename + '\n' + data); }); git.on('exit', function (code) { cb(contents); }); } function getGitLog(filename, cb) { console.assert(typeof cb === 'function', 'expect callback function, not', cb); filename = path.resolve(filename); console.log('fetching history for', filename); getGitRootFolder(function (rootFolder) { console.assert(rootFolder, 'could not find git root folder'); rootFolder = rootFolder.trim(); rootFolder = rootFolder.replace(/\//g, '\\'); console.log('filename', filename); console.log('repo root folder', rootFolder); var workingFolder = process.cwd(); console.log('working folder', workingFolder); var relativePath = path.relative(workingFolder, filename); var repoPath = path.relative(rootFolder, filename); // use -n <number> to limit history // oe --since <date> var args = ['log', '--no-decorate', '-n ' + config.commits]; args.push(relativePath); console.log('git log command', args); var git = spawn('git', args); var commits = []; git.stdout.setEncoding('utf-8'); git.stdout.on('data', function (data) { data.trim(); var separatedData = data.split('commit'); separatedData = separatedData.filter(function (str) { str.trim(); return str && str !== '\n'; }); var info = separatedData.map(parseCommit); commits = commits.concat(info); }); git.stderr.setEncoding('utf-8'); git.stderr.on('data', function (data) { throw new Error('Could not get git log for\n' + filename + '\n' + data); }); git.on('exit', function (code) { cb(repoPath, commits); }); }); } function run(options) { console.assert(options, 'missing options'); console.assert(options.filename, 'missing input filename'); config.filename = options.filename; config.report = options.report || path.basename(args.filename) + '.complexityHistory.json'; config.commits = options.commits || config.commits; getGitLog(options.filename, writeComplexityHistory); } function writeComplexityHistory(filename, commits) { console.assert(filename, 'missing filename'); console.assert(Array.isArray(commits), 'expected commits'); filename = filename.replace(/\\/g, '/'); console.log('fetching revisions for', filename, 'for', commits.length, 'revisions'); var titles = ['Date', 'LOC', 'Cyclomatic', 'Halstead', 'Author']; var rows = []; commits.forEach(function (revision) { getFileRevision(revision.commit, filename, function (contents) { var report = metrics.getSourceComplexity(contents); console.assert(report, 'missing report for', filename, 'commit', revision.commit); //console.log(revision.date, 'LOC', report.aggregate.complexity.cyclomatic, // 'Halstead', report.aggregate.complexity.halstead.difficulty.toFixed(0)); rows.push([revision.date, report.aggregate.complexity.sloc.logical, report.aggregate.complexity.cyclomatic, +report.aggregate.complexity.halstead.difficulty.toFixed(0), revision.author, revision.description]); if (rows.length === commits.length) { var comparison = function (a, b) { var first = a[0]; var second = b[0]; if (first < second) { return -1; } else if (first > second) { return 1; } else { return 0; } }; rows.sort(comparison); rows = rows.map(function (row) { row[0] = row[0].format('YYYY/MM/DD HH:mm:ss'); return row; }); var table = new Table({ head: titles }); rows.forEach(function (row) { table.push(row.slice(0, 5)); }); console.log(table.toString()); var reportArray = rows.map(function (row) { return { date: row[0], loc: row[1], cyclomatic: row[2], halstead: row[3], author: row[4], description: row[5] }; }); var fileReport = { filename: filename, complexityHistory: reportArray }; fs.writeFileSync(config.report, JSON.stringify(fileReport, null, 2), 'utf-8'); console.log('Saved report text', config.report); } }); }); } if (!module.parent) { var args = optimist.usage('Report file source complexity through history.\n' + 'Usage: $0 <source filename>') .default({ help: false, filename: '', report: '', commits: 20 }).alias('h', 'help').describe('help', 'show usage help and exit') .alias('i', 'filename').alias('f', 'filename').string('filename') .describe('filename', 'input source filename') .alias('r', 'report').string('report') .describe('report', 'output report filename') .alias('n', 'commits').describe('commits', 'maximum commits to trace, should be positive') .argv; if (!args.filename) { args.filename = args._[0]; } if (!args.filename || args.help) { optimist.showHelp(); console.error('missing input filename'); process.exit(1); } if (!args.filename || args.help) { optimist.showHelp(); console.log('Current command line arguments\n', args); process.exit(0); } if (!args.report) { args.report = path.basename(args.filename) + '.complexityHistory.json'; } if (typeof args.commits !== 'number' || args.commits < 1) { optimist.showHelp(); console.error('invalid maximum commits', args.commits); process.exit(1); } run({ filename: args.filename, report: args.report, commits: args.commits }); } module.exports = { fileComplexityHistorian: { run: run } };