git-stats/lib/index.js

401 lines
10 KiB
JavaScript
Raw Normal View History

2015-01-25 20:49:49 +01:00
// Dependencies
2015-02-01 19:05:12 +01:00
var Ul = require("ul")
, Fs = require("fs")
2015-01-26 08:56:49 +01:00
, Moment = require("moment")
2015-01-27 15:17:22 +01:00
, CliBox = require("cli-box")
, Couleurs = require("couleurs")()
2015-01-25 20:49:49 +01:00
;
// Constants
const STORE_PATH = Ul.USER_DIR + "/.git-stats"
, LEVELS = [
2015-01-27 15:17:22 +01:00
"⬚"
, "▢"
, "▤"
, "▣"
2015-02-01 18:50:18 +01:00
, "■"
2015-01-27 15:17:22 +01:00
]
2015-01-27 16:46:56 +01:00
, DAYS = [
2015-01-27 12:16:22 +01:00
"Sun"
, "Mon"
, "Tue"
, "Wed"
, "Thu"
, "Fri"
, "Sat"
]
, DATE_FORMAT = "MMM D, YYYY"
;
2015-01-25 20:54:02 +01:00
2015-01-25 20:49:49 +01:00
// Constructor
var GitStats = module.exports = {};
2015-01-26 08:56:49 +01:00
/**
* record
* Records a new commit.
*
* @name record
* @function
* @param {Object} data The commit data containing:
*
2015-01-26 09:47:13 +01:00
* - `date` (String|Date): The date object or a string in a format that can be parsed.
2015-01-26 08:56:49 +01:00
* - `url` (String): The repository remote url.
* - `hash` (String): The commit hash.
*
* @param {Function} callback The callback function.
2015-02-09 14:11:47 +01:00
* @return {GitStats} The `GitStats` object.
2015-01-26 08:56:49 +01:00
*/
2015-01-25 20:54:02 +01:00
GitStats.record = function (data, callback) {
2015-01-26 08:58:03 +01:00
// Validate data
2015-01-26 08:56:49 +01:00
callback = callback || function (err) { if (err) throw err; };
data = Object(data);
if (typeof data.date === "string") {
2015-01-26 09:47:13 +01:00
data.date = new Moment(new Date(data.date));
2015-01-26 08:56:49 +01:00
}
2015-01-26 09:21:40 +01:00
if (!data.date || !/^Moment|Date$/.test(data.date.constructor.name)) {
2015-02-09 14:11:47 +01:00
callback(new Error("The date field should be a string or a date object."));
return GitStats;
2015-01-26 08:56:49 +01:00
}
if (typeof data.hash !== "string" || !data.hash) {
2015-02-09 14:11:47 +01:00
callback(new Error("Invalid hash."));
return GitStats;
2015-01-26 08:56:49 +01:00
}
if (typeof data.url !== "string" || !data.url) {
callback(new Error("Invalid url field. This commit is not recorded into the git-stats history since you haven't added the remote url. You can import the previous commits using the git-stats-importer tool."));
2015-02-09 14:11:47 +01:00
return GitStats;
2015-01-26 08:56:49 +01:00
}
2015-01-26 08:58:03 +01:00
// Get stats
GitStats.get(function (err, stats) {
2015-01-25 20:54:02 +01:00
stats = stats || {};
var day = data.date.format(DATE_FORMAT)
2015-01-26 08:56:49 +01:00
, today = stats[day] = Object(stats[day])
, repo = today[data.url] = Object(today[data.url])
;
repo[data.hash] = { date: data.date };
2015-02-01 19:05:12 +01:00
GitStats.save(stats, callback);
2015-01-25 20:54:02 +01:00
});
2015-02-09 14:11:47 +01:00
return GitStats;
2015-01-25 20:54:02 +01:00
};
2015-01-26 09:05:46 +01:00
/**
* get
* Gets the git stats.
*
* @name get
* @function
* @param {Function} callback The callback function.
2015-02-09 14:11:47 +01:00
* @return {GitStats} The `GitStats` object.
2015-01-26 09:05:46 +01:00
*/
GitStats.get = function (callback) {
2015-02-01 19:05:12 +01:00
Fs.readFile(STORE_PATH, "utf-8", function (err, data) {
if (err && err.code === "ENOENT") {
return GitStats.save({}, function (err) {
callback(err, {});
});
}
2015-02-01 19:05:12 +01:00
if (err) { return callback(err); }
try {
data = JSON.parse(data);
} catch (e) {
return callback(e);
}
callback(null, data);
});
2015-02-09 14:11:47 +01:00
return GitStats;
};
2015-02-08 19:15:25 +01:00
/**
* save
* Saves the provided stats.
*
* @name save
* @function
* @param {Object} stats The stats to be saved.
* @param {Function} callback The callback function.
2015-02-09 14:11:47 +01:00
* @return {GitStats} The `GitStats` object.
2015-02-08 19:15:25 +01:00
*/
2015-02-01 19:05:12 +01:00
GitStats.save = function (stats, callback) {
Fs.writeFile(STORE_PATH, JSON.stringify(stats, null, 1), callback);
2015-02-09 14:11:47 +01:00
return GitStats;
2015-02-08 19:15:25 +01:00
};
2015-02-01 19:05:12 +01:00
2015-02-08 19:15:25 +01:00
/**
* iterateDays
2015-02-15 19:20:49 +01:00
* Iterate through the days, calling the callback function on each day.
2015-02-08 19:15:25 +01:00
*
* @name iterateDays
* @function
* @param {Object} data An object containing the following fields:
*
* - `start` (Moment): A `Moment` date object representing the start date (default: *an year ago*).
* - `end` (Moment): A `Moment` date object representing the end date (default: *now*).
* - `format` (String): The format of the date (default: `"MMM D, YYYY"`).
*
* @param {Function} callback The callback function called with the current day formatted (type: string) and the `Moment` date object.
2015-02-09 14:11:47 +01:00
* @return {GitStats} The `GitStats` object.
2015-02-08 19:15:25 +01:00
*/
GitStats.iterateDays = function (data, callback) {
2015-01-25 20:54:02 +01:00
if (typeof data === "function") {
callback = data;
data = undefined;
}
// Merge the defaults
data = Ul.merge({
end: Moment()
, start: Moment().subtract(1, "years")
, format: DATE_FORMAT
}, data);
var start = data.start
, end = data.end
, tomrrow = Moment(end.format(DATE_FORMAT), DATE_FORMAT).add(1, "days")
, endStr = tomrrow.format(DATE_FORMAT)
, cDay = null
;
while (start.format(DATE_FORMAT) !== endStr) {
cDay = start.format(data.format);
2015-01-27 12:07:15 +01:00
callback(cDay, start);
2015-02-08 19:16:08 +01:00
start.add(1, "days");
2015-01-25 20:54:02 +01:00
}
2015-02-09 14:11:47 +01:00
return GitStats;
2015-01-25 20:49:49 +01:00
};
2015-01-26 10:27:48 +01:00
2015-02-08 19:15:25 +01:00
/**
* graph
* Creates an object with the stats on the provided period (default: *last year*).
*
* @name graph
* @function
* @param {Object} data The object passed to the `iterateDays` method.
* @param {Function} callback The callback function.
2015-02-09 14:11:47 +01:00
* @return {GitStats} The `GitStats` object.
2015-02-08 19:15:25 +01:00
*/
2015-01-26 10:27:48 +01:00
GitStats.graph = function (data, callback) {
if (typeof data === "function") {
callback = data;
data = undefined;
}
// Get commits
GitStats.get(function (err, stats) {
2015-01-26 10:27:48 +01:00
if (err) { return callback(err); }
var cDayObj = null
, year = {}
2015-01-26 10:27:48 +01:00
;
// Iterate days
GitStats.iterateDays(data, function (cDay) {
2015-01-26 10:27:48 +01:00
cDayObj = year[cDay] = {
_: stats[cDay] || {}
, c: 0
};
Object.keys(cDayObj._).forEach(function (c) {
cDayObj.c += Object.keys(cDayObj._[c]).length;
});
});
2015-01-26 10:27:48 +01:00
callback(null, year);
});
2015-02-09 14:11:47 +01:00
return GitStats;
};
2015-02-08 19:15:25 +01:00
/**
* calendar
* Creates the calendar data for the provided period (default: *last year*).
*
* @name calendar
* @function
* @param {Object} data The object passed to the `graph` method.
* @param {Function} callback The callback function.
2015-02-09 14:11:47 +01:00
* @return {GitStats} The `GitStats` object.
2015-02-08 19:15:25 +01:00
*/
GitStats.calendar = function (data, callback) {
GitStats.graph(data, function (err, graph) {
if (err) { return callback(err); }
2015-02-01 14:43:45 +01:00
var cal = { total: 0, days: {}, cStreak: 0, lStreak: 0, max: 0 }
, cDay = null
, days = Object.keys(graph)
, levels = null
, cLevel = 0
;
days.forEach(function (c) {
cDay = graph[c];
cal.total += cDay.c;
2015-02-01 14:43:45 +01:00
if (cDay.c > cal.max) {
cal.max = cDay.c;
}
2015-01-27 12:25:25 +01:00
if (cDay.c > 0) {
if (++cal.cStreak > cal.lStreak) {
cal.lStreak = cal.cStreak;
}
} else {
cal.cStreak = 0;
}
});
levels = cal.max / (LEVELS.length * 2);
days.forEach(function (c) {
cDay = graph[c];
cal.days[c] = {
c: cDay.c
, level: !levels
? 0 : (cLevel = Math.round(cDay.c / levels)) >= 4
? 4 : !cLevel && cDay.c > 0 ? 1 : cLevel
};
});
callback(null, cal);
});
2015-02-09 14:11:47 +01:00
return GitStats;
};
2015-02-08 19:15:25 +01:00
/**
* ansiCalendar
* Creates the ANSI contributions calendar.
*
* @name ansiCalendar
* @function
* @param {Object} data The object passed to the `calendar` method.
* @param {Function} callback The callback function.
2015-02-09 14:11:47 +01:00
* @return {GitStats} The `GitStats` object.
2015-02-08 19:15:25 +01:00
*/
GitStats.ansiCalendar = function (data, callback) {
if (typeof data === "function") {
callback = data;
data = undefined;
}
2015-01-27 12:07:15 +01:00
var year = []
, months = new Array(52) // Stores the months depending on their first week
2015-01-27 12:13:59 +01:00
, cWeek = [" ", " ", " ", " ", " ", " ", " "]
2015-02-01 15:19:03 +01:00
, monthHack = "MM"
2015-01-27 12:07:15 +01:00
, sDay = ""
, cDayObj = null
2015-01-27 12:13:59 +01:00
, strYear = ""
, strMonths = ""
2015-01-27 12:13:59 +01:00
, w = 0
, d = 0
, when = "the last year"
2015-02-09 10:51:48 +01:00
, dataClone = {
start: data.start ? Moment(data.start.format(DATE_FORMAT), DATE_FORMAT) : null
, end: data.end ? Moment(data.end.format(DATE_FORMAT), DATE_FORMAT) : null
}
2015-01-27 12:07:15 +01:00
;
dataClone.s = data.start.format(DATE_FORMAT);
dataClone.e = data.end.format(DATE_FORMAT);
if (Moment().subtract(1, "years").format(DATE_FORMAT) !== dataClone.s
|| Moment().format(DATE_FORMAT) !== dataClone.e) {
when = [Couleurs.bold(dataClone.s), Couleurs.bold(dataClone.e)].join(" ");
}
GitStats.calendar(data, function (err, cal) {
if (err) { return callback(err); }
2015-02-09 10:51:48 +01:00
GitStats.iterateDays(dataClone, function (cDay, mDay) {
2015-01-27 12:07:15 +01:00
sDay = mDay.format("ddd");
2015-02-01 15:01:11 +01:00
2015-01-27 12:07:15 +01:00
cDayObj = cal.days[cDay];
if (!cDayObj) return;
2015-01-27 12:07:15 +01:00
if (sDay === "Sun" && Object.keys(cWeek).length) {
year.push(cWeek);
2015-01-27 12:13:59 +01:00
cWeek = [" ", " ", " ", " ", " ", " ", " "];
2015-01-27 12:07:15 +01:00
}
// Store the new month this week
if (mDay.format("D") === "1") {
months[year.length] = mDay.format("MMM");
}
cWeek[DAYS.indexOf(sDay)] = LEVELS[cDayObj.level];
2015-01-27 12:07:15 +01:00
});
if (cWeek.length) {
year.push(cWeek);
}
2015-01-27 12:13:59 +01:00
for (d = 0; d < 7; ++d) {
for (w = 0; w < year.length; ++w) {
strYear += " " + year[w][d];
}
strYear += "\n";
}
2015-02-01 15:01:11 +01:00
// Add day names
2015-01-27 15:17:22 +01:00
strYear = strYear.split("\n").map(function (c, i) {
if (i > 6) { return; }
2015-01-27 16:46:56 +01:00
return DAYS[i] + c;
2015-01-27 12:16:22 +01:00
}).join("\n");
2015-01-27 12:13:59 +01:00
// Months label
monthHack = "MMMM"; //Left padding
2015-02-18 10:29:01 +01:00
for (var i = 0; i < months.length; i++) {
// The length of strMonths should always be 2*(i+1) (at the i-th column)
if (!months[i]) {
strMonths += new Array(2*(i+1)-strMonths.length+1).join(" ");
} else {
strMonths += months[i];
}
}
2015-02-18 10:29:01 +01:00
strYear = monthHack + strMonths + "\n" + strYear;
2015-01-27 12:25:25 +01:00
strYear +=
2015-02-01 14:36:26 +01:00
new Array(5 + 2 * Math.ceil(365 / 7)).join("-")
+ "\n" + "Commits in " + when + ": " + cal.total
2015-02-01 14:36:26 +01:00
+ " | " + "Longest Streak: " + cal.lStreak + " days"
+ " | " + "Current Streak: " + cal.cStreak + " days"
2015-02-01 14:43:45 +01:00
+ " | " + "Max a day: " + cal.max
2015-01-27 12:25:25 +01:00
;
2015-01-27 15:17:22 +01:00
strYear = new CliBox({
w: 10
, h: 10
, marks: {
nw: "╔"
, n: "═"
, ne: "╗"
, e: "║"
, se: "╝"
, s: "═"
, sw: "╚"
, w: "║"
, b: " "
}
}, {
text: strYear
, stretch: true
, hAlign: "left"
2015-02-01 15:01:11 +01:00
}).toString();
2015-02-01 15:19:03 +01:00
strYear = strYear.replace(monthHack, new Array(monthHack.length + 1).join(" "));
2015-01-27 15:17:22 +01:00
2015-02-01 15:01:11 +01:00
callback(null, strYear);
2015-01-26 10:27:48 +01:00
});
2015-02-09 14:11:47 +01:00
return GitStats;
2015-01-26 10:27:48 +01:00
};