gitlogplus
Version:
Git log parser for Node.JS
213 lines (169 loc) • 5.48 kB
JavaScript
module.exports = gitlogplus
var exec = require('child_process').exec
, execSync = require('child_process').execSync
, existsSync = require('fs').existsSync
, debug = require('debug')('gitlog')
, extend = require('lodash.assign')
, delimiter = '\t'
, fields =
{ hash: '%H'
, abbrevHash: '%h'
, treeHash: '%T'
, abbrevTreeHash: '%t'
, parentHashes: '%P'
, abbrevParentHashes: '%P'
, authorName: '%an'
, authorEmail: '%ae'
, authorDate: '%ai'
, authorDateRel: '%ar'
, committerName: '%cn'
, committerEmail: '%ce'
, committerDate: '%cd'
, committerDateRel: '%cr'
, subject: '%s'
, body: '%b'
, rawBody: '%B'
}
, notOptFields = [ 'status', 'files' ]
/***
Add optional parameter to command
*/
function addOptional(command, options) {
var cmdOptional = [ 'author', 'since', 'after', 'until', 'before', 'committer' ]
for (var i = cmdOptional.length; i--;) {
if (options[cmdOptional[i]]) {
command += ' --' + cmdOptional[i] + '="' + options[cmdOptional[i]] + '"'
}
}
return command
}
function gitlogplus(options, cb) {
if (!options.repo) throw new Error('Repo required!')
if (!existsSync(options.repo)) throw new Error('Repo location does not exist');
var defaultOptions =
{ number: 10
, fields: [ 'abbrevHash', 'hash', 'subject', 'authorName' ]
, nameStatus:true
, findCopiesHarder:false
, all:false
, execOptions: { cwd: options.repo }
}
// Set defaults
options = extend({}, defaultOptions, options)
extend(options.execOptions, defaultOptions.execOptions)
// Start constructing command
var command = 'git log -m --first-parent '
if (options.findCopiesHarder){
command += '--find-copies-harder '
}
if (options.all){
command += '--all '
}
if (options.from) {
command += options.from + '..'
if (options.to) {
command += options.to
}
}
else {
command += '-n ' + options.number
}
command = addOptional(command, options)
// Start of custom format
command += ' --pretty="@begin@'
// Iterating through the fields and adding them to the custom format
options.fields.forEach(function(field) {
if (!fields[field] && field.indexOf(notOptFields) === -1) throw new Error('Unknown field: ' + field)
command += delimiter + fields[field]
})
// Close custom format
command += '@end@"'
// Append branch (revision range) if specified
if (options.branch) {
command += ' ' + options.branch
}
if (options.file) {
command += ' -- ' + options.file
}
//File and file status
command += fileNameAndStatus(options)
debug('command', options.execOptions, command)
if (!cb) {
// run Sync
var stdout = execSync(command, options.execOptions).toString()
, commits = stdout.split('@begin@')
if (commits[0] === '' ){
commits.shift()
}
debug('commits',commits)
commits = parseCommits(commits, options.fields,options.nameStatus)
return commits
}
exec(command, options.execOptions, function(err, stdout, stderr) {
debug('stdout',stdout)
var commits = stdout.split('@begin@')
if (commits[0] === '' ){
commits.shift()
}
debug('commits',commits)
commits = parseCommits(commits, options.fields, options.nameStatus)
cb(stderr || err, commits)
})
}
function fileNameAndStatus(options) {
return options.nameStatus ? ' --name-status' : '';
}
function parseCommits(commits, fields, nameStatus) {
return commits.map(function(commit) {
var parts = commit.split('@end@')
commit = parts[0].split(delimiter)
if (parts[1]) {
var parseNameStatus = parts[1].trimLeft().split('\n');
// Removes last empty char if exists
if (parseNameStatus[parseNameStatus.length - 1] === ''){
parseNameStatus.pop()
}
// Split each line into it's own delimitered array
parseNameStatus.forEach(function(d, i) {
parseNameStatus[i] = d.split(delimiter);
});
// 0 will always be status, last will be the filename as it is in the commit,
// anything inbetween could be the old name if renamed or copied
parseNameStatus = parseNameStatus.reduce(function(a, b) {
var tempArr = [ b[ 0 ], b[ b.length - 1 ] ];
// If any files in between loop through them
for (var i = 1, len = b.length - 1; i < len; i++) {
// If status R then add the old filename as a deleted file + status
// Other potentials are C for copied but this wouldn't require the original deleting
if (b[ 0 ].slice(0, 1) === 'R'){
tempArr.push('D', b[ i ]);
}
}
return a.concat(tempArr);
}, [])
commit = commit.concat(parseNameStatus)
}
debug('commit', commit)
// Remove the first empty char from the array
commit.shift()
var parsed = {}
if (nameStatus){
// Create arrays for non optional fields if turned on
notOptFields.forEach(function(d) {
parsed[d] = [];
})
}
commit.forEach(function(commitField, index) {
if (fields[index]) {
parsed[fields[index]] = commitField
} else {
if (nameStatus){
var pos = (index - fields.length) % notOptFields.length
debug('nameStatus', (index - fields.length) ,notOptFields.length,pos,commitField)
parsed[notOptFields[pos]].push(commitField)
}
}
})
return parsed
})
}