2015-01-25 20:49:49 +01:00
|
|
|
// Dependencies
|
|
|
|
var FsExtra = require("fs-extra")
|
|
|
|
, Ul = require("ul")
|
2015-01-26 08:56:49 +01:00
|
|
|
, Moment = require("moment")
|
2015-01-25 20:49:49 +01:00
|
|
|
;
|
|
|
|
|
2015-01-27 11:44:05 +01:00
|
|
|
// Constants
|
|
|
|
const STORE_PATH = Ul.USER_DIR + "/.git-stats"
|
|
|
|
, LEVELS = 5
|
2015-01-27 12:07:15 +01:00
|
|
|
, SQUARES = ["⬚", "O", "▢", "▤", "▣", "⬛"]
|
|
|
|
, DAYS = {
|
|
|
|
Sun: 0
|
|
|
|
, Mon: 1
|
|
|
|
, Tue: 2
|
|
|
|
, Wed: 3
|
|
|
|
, Thu: 4
|
|
|
|
, Fri: 5
|
|
|
|
, Sat: 6
|
|
|
|
}
|
2015-01-27 12:16:22 +01:00
|
|
|
, DAYS_ARR = [
|
|
|
|
"Sun"
|
|
|
|
, "Mon"
|
|
|
|
, "Tue"
|
|
|
|
, "Wed"
|
|
|
|
, "Thu"
|
|
|
|
, "Fri"
|
|
|
|
, "Sat"
|
|
|
|
]
|
2015-01-27 11:44:05 +01:00
|
|
|
;
|
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.
|
|
|
|
* @return {undefined}
|
|
|
|
*/
|
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-01-26 08:56:49 +01:00
|
|
|
return callback(new Error("The date field should be a string or a date object."));
|
|
|
|
}
|
|
|
|
|
|
|
|
if (typeof data.hash !== "string" || !data.hash) {
|
|
|
|
return callback(new Error("Invalid hash."));
|
|
|
|
}
|
|
|
|
|
|
|
|
if (typeof data.url !== "string" || !data.url) {
|
|
|
|
return callback(new Error("Invalid url field."));
|
|
|
|
}
|
|
|
|
|
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 || {};
|
2015-01-26 10:27:48 +01:00
|
|
|
var day = data.date.format("MMM D, YYYY")
|
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-01-25 20:54:02 +01:00
|
|
|
FsExtra.writeJSON(STORE_PATH, stats, callback);
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2015-01-26 09:05:46 +01:00
|
|
|
/**
|
|
|
|
* get
|
|
|
|
* Gets the git stats.
|
|
|
|
*
|
|
|
|
* @name get
|
|
|
|
* @function
|
|
|
|
* @param {Function} callback The callback function.
|
|
|
|
* @return {undefined}
|
|
|
|
*/
|
2015-01-27 11:44:05 +01:00
|
|
|
GitStats.get = function (callback) {
|
|
|
|
FsExtra.readJSON(STORE_PATH, callback);
|
|
|
|
};
|
|
|
|
|
|
|
|
GitStats.iterateDays = function (data, callback) {
|
|
|
|
|
2015-01-25 20:54:02 +01:00
|
|
|
if (typeof data === "function") {
|
|
|
|
callback = data;
|
2015-01-27 11:44:05 +01:00
|
|
|
data = undefined;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Merge the defaults
|
|
|
|
data = Ul.merge({
|
|
|
|
end: Moment()
|
|
|
|
, start: Moment().subtract(1, "years")
|
|
|
|
, format: "MMM D, YYYY"
|
|
|
|
}, data);
|
|
|
|
|
|
|
|
var start = data.start
|
|
|
|
, end = data.end
|
|
|
|
, cDay = null
|
|
|
|
;
|
|
|
|
|
|
|
|
while (!start.isAfter(end)) {
|
|
|
|
cDay = start.format(data.format);
|
2015-01-27 12:07:15 +01:00
|
|
|
callback(cDay, start);
|
2015-01-27 11:44:05 +01:00
|
|
|
start.add(1, "days")
|
2015-01-25 20:54:02 +01:00
|
|
|
}
|
2015-01-25 20:49:49 +01:00
|
|
|
};
|
2015-01-26 10:27:48 +01:00
|
|
|
|
|
|
|
GitStats.graph = function (data, callback) {
|
2015-01-27 11:44:05 +01:00
|
|
|
|
|
|
|
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); }
|
2015-01-27 11:44:05 +01:00
|
|
|
|
|
|
|
var cDayObj = null
|
|
|
|
, year = {}
|
2015-01-26 10:27:48 +01:00
|
|
|
;
|
|
|
|
|
2015-01-27 11:44:05 +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-27 11:44:05 +01:00
|
|
|
});
|
2015-01-26 10:27:48 +01:00
|
|
|
|
|
|
|
callback(null, year);
|
2015-01-27 11:44:05 +01:00
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
GitStats.calendar = function (data, callback) {
|
|
|
|
GitStats.graph(data, function (err, graph) {
|
|
|
|
if (err) { return callback(err); }
|
2015-01-27 12:25:25 +01:00
|
|
|
var cal = { total: 0, days: {}, cStreak: 0, lStreak: 0 }
|
2015-01-27 11:44:05 +01:00
|
|
|
, cDay = null
|
|
|
|
, days = Object.keys(graph)
|
|
|
|
, max = 0
|
|
|
|
;
|
|
|
|
|
|
|
|
days.forEach(function (c) {
|
|
|
|
cDay = graph[c];
|
|
|
|
cal.total += cDay.c;
|
|
|
|
if (cDay.c > max) {
|
|
|
|
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;
|
|
|
|
}
|
2015-01-27 11:44:05 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
days.forEach(function (c) {
|
|
|
|
cDay = graph[c];
|
|
|
|
cal.days[c] = {
|
|
|
|
c: cDay.c
|
|
|
|
, level: cDay.c === 0 ? 0 : LEVELS - Math.floor(max / (cDay.c + 2))
|
|
|
|
};
|
|
|
|
});
|
|
|
|
|
|
|
|
callback(null, cal);
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
GitStats.ansiCalendar = function (data, callback) {
|
|
|
|
|
|
|
|
if (typeof data === "function") {
|
|
|
|
callback = data;
|
|
|
|
data = undefined;
|
|
|
|
}
|
2015-01-27 12:07:15 +01:00
|
|
|
|
|
|
|
var year = []
|
2015-01-27 12:13:59 +01:00
|
|
|
, cWeek = [" ", " ", " ", " ", " ", " ", " "]
|
2015-01-27 12:07:15 +01:00
|
|
|
, sDay = ""
|
|
|
|
, cDayObj = null
|
2015-01-27 12:13:59 +01:00
|
|
|
, strYear = ""
|
|
|
|
, w = 0
|
|
|
|
, d = 0
|
2015-01-27 12:07:15 +01:00
|
|
|
;
|
|
|
|
|
|
|
|
|
2015-01-27 11:44:05 +01:00
|
|
|
GitStats.calendar(data, function (err, cal) {
|
|
|
|
if (err) { return callback(err); }
|
2015-01-27 12:07:15 +01:00
|
|
|
GitStats.iterateDays(function (cDay, mDay) {
|
|
|
|
sDay = mDay.format("ddd");
|
|
|
|
cDayObj = cal.days[cDay];
|
|
|
|
|
|
|
|
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
|
|
|
}
|
|
|
|
|
|
|
|
if (!cDayObj) return;
|
|
|
|
|
|
|
|
cWeek[DAYS[sDay]] = SQUARES[cDayObj.level];
|
|
|
|
});
|
|
|
|
|
|
|
|
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-01-27 12:16:22 +01:00
|
|
|
strYear = strYear.trimRight().split("\n").map(function (c, i) {
|
|
|
|
return DAYS_ARR[i] + c;
|
|
|
|
}).join("\n");
|
2015-01-27 12:13:59 +01:00
|
|
|
|
2015-01-27 12:25:25 +01:00
|
|
|
strYear +=
|
|
|
|
"\n" + "Total commits: " + cal.total
|
|
|
|
+ "\n" + "Current Streak: " + cal.cStreak
|
|
|
|
+ "\n" + "Longest Streak: " + cal.lStreak
|
|
|
|
;
|
|
|
|
|
2015-01-27 11:44:05 +01:00
|
|
|
|
2015-01-27 12:13:59 +01:00
|
|
|
callback(null, strYear);
|
2015-01-26 10:27:48 +01:00
|
|
|
});
|
|
|
|
};
|