gimme-stat
Version:
Simple tool for git repositories to generate progress statistic.
425 lines (386 loc) • 16.5 kB
JavaScript
;
//git log --no-merges --pretty=short --stat --branches --since 2017/09/14 --until 2018/01/01
//https://regex101.com/r/HjC7th/1
//https://www.npmjs.com/package/console-progress
//https://www.npmjs.com/package/msee
//https://www.npmjs.com/package/json2md
//https://stackoverflow.com/questions/11509830/how-to-add-color-to-githubs-readme-md-file - colors
let fs = require('fs');
require.extensions['.ejs'] = (module, filename) => { module.exports = fs.readFileSync(filename, 'utf8'); };
var stat = async function (config, specialParams) {
// const config = require('./env');
const git = require('git-cmd');
const _ = require('lodash');
const util = require('util');
const openFileStream = util.promisify(fs.open);
const writeFile = util.promisify(fs.writeFile);
const Table = require('cli-table');
const moment = require('moment'); require('twix');
let allDaysInPeriod = [];
let table = new Table({
head: ["Author", "Commits ", "Insertions", "Deletions", "% of changes"],
});
if (config.init) {
let configPath = `${process.cwd()}/gimme.config.js`;
const checkExistence = util.promisify(fs.access);
try {
await checkExistence(configPath);
console.log('Fail: gimme.config.js already exists.');
} catch (err) {
fs.createReadStream(`${__dirname}/default-config.js`).pipe(fs.createWriteStream(configPath));
console.log(`Config file created at ${process.cwd()}/gimme.config.js`);
}
return;
}
let repositories = config.cwd;
function getStat(rep, since, until) {
let cmd = git([
'log',
'--no-merges',
'--pretty=medium',
'--stat',
'--all'
],
{ cwd: rep }
);
if (since) {
cmd.push(`--since=${since}`);
}
if (until) {
cmd.push(`--until=${until}`);
}
return cmd.oneline({ encoding: 'string' });
}
let resultText = '';
for (let rep of repositories) {
if (config.prepull) {
await git(['pull', 'origin'], { cwd: rep }).pass();
}
resultText += await getStat(rep, config.since, config.until);
}
let commits = resultText.split(/^commit .{40,40}$/mi);
let resultStat = {
changed: 0,
authors: {},
daily: {}
};
for (let commit of commits) {
if (!commit) {
continue;
}
let author = (/Author: (.+)( $| <)/mi).exec(commit)[1];
author = config.userAliases[author] || author;
if (config.users[0] === "" || config.users.includes(author)) {
if (config.ignoreUsers.some(user => user === author)) {
continue;
}
if (!resultStat.authors[author]) {
resultStat.authors[author] = {
name: author,
commits: 0,
changed: 0,
insertions: 0,
deletions: 0,
byExt: {
other: {
name: 'other',
changed: 0,
percent: 0,
extensions: []
}
}
};
for (let ext of config.statExtensions) {
resultStat.authors[author].byExt[ext] = {
name: ext,
changed: 0,
percent: 0,
extensions: []
};
}
}
resultStat.authors[author].commits += 1;
let fileChangesRegExp = new RegExp(`^(.+?)(\\.(${config.statExtensions.join('|')}))* +\\| +(\\d+) ((\\+|-)+)`, 'gmi');
let info;
while (info = fileChangesRegExp.exec(commit)) {
let fileName = info[0];
if (config.statIgnore.some(regExp => regExp.test(fileName))) {
continue;
}
let changed = (info[5] || '').length,
changesArr = Array.from(info[5]),
insertions = changesArr.filter(x => x === '+').length,
fileExt = (info[3] || 'other').toLowerCase(),
deletions = changed - insertions;
if (fileExt === 'other') {
let data = /(\.(\w{2,5}))* +\| +(\d+) ((\+|-)+)/.exec(info[0]);
resultStat.authors[author].byExt[fileExt].extensions.push(data[2]);
}
resultStat.changed += changed;
resultStat.authors[author].changed += changed;
resultStat.authors[author].insertions += insertions;
resultStat.authors[author].deletions += deletions;
resultStat.authors[author].byExt[fileExt].changed += changed;
}
if (config.daily) {
let day = (/Date:(.+)/mi).exec(commit)[1];
day = (new Date(Date.parse(day))).toDateString();
if (!resultStat.daily[day]) {
resultStat.daily[day] = {
date: day,
commits: 0,
changed: 0,
insertions: 0,
deletions: 0,
};
}
resultStat.daily[day].commits += 1;
let info;
while (info = fileChangesRegExp.exec(commit)) {
let fileName = info[0];
if (config.statIgnore.some(regExp => regExp.test(fileName))) {
continue;
}
let changed = (info[5] || '').length,
changesArr = Array.from(info[5]),
insertions = changesArr.filter(x => x === '+').length,
deletions = changed - insertions;
resultStat.daily[day].changed += changed;
resultStat.daily[day].insertions += insertions;
resultStat.daily[day].deletions += deletions;
}
}
}
}
resultStat.authors = _(resultStat.authors).map(author => {
author.percent = author.changed / resultStat.changed;
author.graphPercent = _.ceil(author.percent * 100, 0);
let filledBarLenght = Math.floor(author.graphPercent / 100 * config.barSize);
let insertionsPrecent = author.insertions / (author.insertions + author.deletions);
let deletionsPrecent = author.deletions / (author.insertions + author.deletions);
if (config.barType == 'default') {
author.graphLine = Array.from({ length: config.barSize }).map((x, index) => (index + 1) <= (filledBarLenght) ? '█' : '░').join('');
}
if (config.barType == 'detailed') {
author.graphLine = Array.from({ length: config.barSize }).map((x, index) => {
if ((index + 1) <= filledBarLenght) {
if (filledBarLenght == 1) {
if (insertionsPrecent > deletionsPrecent) {
return '+';
}
else {
return '-';
}
}
else if (filledBarLenght > 2) {
if (index + 1 < filledBarLenght - (filledBarLenght * deletionsPrecent)) {
return '-';
}
else {
return '+';
}
}
}
else {
return ' ';
}
}).join('');
}
if (config.table) {
table.push(
[author.name, author.commits, author.insertions, author.deletions, _.ceil(author.percent * 100, 2).toFixed(2)]
);
}
author.byExt = _(author.byExt).map(ext => {
ext.percent = ext.changed / author.changed;
ext.graphPercent = _.ceil(ext.percent * 100, 0);
let filledBarLenghtExt = Math.floor(ext.graphPercent / 100 * config.barSize);
if (config.barType == 'default') {
ext.graphLine = Array.from({ length: config.barSize }).map((x, index) =>
(index + 1) <= (filledBarLenghtExt) ? '█' : '░').join('');
}
if (config.barType == 'detailed') {
ext.graphLine = Array.from({ length: config.barSize }).map((x, index) => {
if ((index + 1) <= filledBarLenghtExt) {
if (filledBarLenghtExt >= 1 && filledBarLenghtExt < 2) {
if (insertionsPrecent > deletionsPrecent) {
return '+';
}
else {
return '-';
}
}
else {
if (index + 1 < filledBarLenghtExt - (filledBarLenghtExt * deletionsPrecent)) {
return '-';
}
else {
return '+';
}
}
}
else {
return ' ';
}
}).join('');
}
ext.extensions = _.uniq(ext.extensions).filter(x => x);
return ext;
}).filter(x => x.changed).orderBy('changed', 'desc').value();
return author;
}).orderBy('changed', 'desc').value();
if (config.daily) {
let maxChanged = _(resultStat.daily).toArray().maxBy('changed');
resultStat.daily = _(resultStat.daily).map(day => {
day.percent = day.changed / maxChanged.changed;
day.graphPercent = _.ceil((day.percent * 100), 0);
let filledBarLenghtDay = Math.floor(day.graphPercent / 100 * config.barSize);
let insertionsPrecentDay = day.insertions / (day.insertions + day.deletions);
let deletionsPrecentDay = day.deletions / (day.insertions + day.deletions);
if (config.barType == 'default') {
day.graphLine = Array.from({ length: config.barSize }).map((x, index) =>
(index + 1) <= (day.graphPercent) ? '█' : '░').join('');
}
if (config.barType == 'detailed') {
day.graphLine = Array.from({ length: config.barSize }).map((x, index) => {
if ((index + 1) <= filledBarLenghtDay) {
if (filledBarLenghtDay == 1) {
if (insertionsPrecentDay > deletionsPrecentDay) {
return '+';
}
else {
return '-';
}
}
else {
if (index + 1 < filledBarLenghtDay - (filledBarLenghtDay * deletionsPrecentDay)) {
return '-';
}
else {
return '+';
}
}
}
else {
if (index == config.barSize - 1) {
return ' ';
}
return ' ';
}
}).join('');
}
switch (day.commits.toString().length) {
case 1:
day.commits += ' '; //alignment to 3 digits limit
break;
case 2:
day.commits += ' '; //alignment to 3 digits limit
break;
default:
break;
}
return day;
}).value();
}
if (config.table) {
table.sort((a, b) => b[4] - a[4]); //table sorting desc
}
if (config.daily) {
let lastDay = resultStat.daily[0].date;
let firstDay = resultStat.daily[resultStat.daily.length - 1].date;
firstDay = Date.parse(firstDay);
lastDay = Date.parse(lastDay);
var itr = moment.twix(new Date(firstDay), new Date(lastDay)).iterate("days");
while (itr.hasNext()) {
let progressBar = '';
if (config.barType == 'default') {
while (progressBar.length < config.barSize) {
progressBar += '░';
}
}
if (config.barType == 'detailed') {
while (progressBar.length <= config.barSize) {
progressBar += ' ';
}
}
let obj = { date: itr.next().toDate().toDateString(), commits: "0 ", changed: 0, insertions: 0, deletions: 0, graphLine: progressBar, graphPercent: 0, percent: 0 };
allDaysInPeriod.push(obj)
}
allDaysInPeriod.forEach((emptyDay, index) => {
resultStat.daily.forEach(day => {
if (day.date === emptyDay.date) {
allDaysInPeriod[index] = day;
}
})
});
resultStat.daily = allDaysInPeriod;
}
let text = require('./template.cmd.ejs');
function getSpaces(length) {
let space = '';
while (space.length < length) {
space += ' ';
}
space += 'commits|changes';
return space;
}
let compiled = _.template(text, {
'imports': {
'_': _,
authors: resultStat.authors,
daily: config.daily ? resultStat.daily : '',
repositories: repositories,
config: config,
table: config.table ? table.toString() : '',
header: config.daily ? getSpaces((config.barSize + config.lmargin - (config.barType=='default'? 6:3))) : '',
minSize: (text) => {
while (text.length < config.lmargin) {
text += ' ';
}
return text;
}
}
});
let consoleText = compiled(resultStat)
.replace(/(\r*\n){2,}(.+)/gm, '\r\n$2')
.replace(/\$br/gm, '');
if(specialParams.output == "cmd"){
console.log(consoleText);
}
if(specialParams.output == "text"){
return {json: resultStat.authors, text: consoleText};
}
if(specialParams.output == "json"){
return {json: resultStat.authors};
}
let mdFilePath = _.isString(config.appendToMd) ? config.appendToMd : 'Result.md';
let mdTemplate = require('./template.md.ejs');
let compiledmb = _.template(mdTemplate, {
'imports': {
'_': _,
authors: resultStat.authors,
daily: resultStat.daily,
repositories: repositories,
config: config,
table: table.toString(),
header: config.daily ? getSpaces((config.barSize + config.lmargin - (config.barType=='default'? 6:3))) : '',
minSize: (text) => {
while (text.length < config.lmargin) {
text += ' ';
}
return text;
}
}
});
if (config.appendToMd) {
let file = await openFileStream(mdFilePath, 'w');
await writeFile(file, compiledmb(table).replace(/^\s*\n/gm, ''));
console.log(`\r\n\r\n >>>>>>>>>>> Saved to ${mdFilePath} <<<<<<<<<<<<<<`)
}
if(specialParams.output == "md"){
let md = compiledmb(table).replace(/^\s*\n/gm, '');
return {json: resultStat.authors, md: md};
}
}
module.exports = stat;