From fe198b101840a442b67592c278386fa28ff36d83 Mon Sep 17 00:00:00 2001 From: Jonah Lawrence Date: Tue, 29 Mar 2022 02:09:34 +0000 Subject: [PATCH 1/3] fix: Display top contributors correctly when larger than chart --- lib/index.js | 618 ++++++++++++++++++++++++++++++++++++++++++++++++++- package.json | 3 +- 2 files changed, 616 insertions(+), 5 deletions(-) diff --git a/lib/index.js b/lib/index.js index 353fcf8..7cdbe0b 100644 --- a/lib/index.js +++ b/lib/index.js @@ -560,15 +560,16 @@ class GitStats { ; self.authors(options, function (err, authors) { + const maxAuthors = 2 * options.radius; if (err) { return callback(err); } - if (authors.length > 50) { - let others = { - value: authors.slice(50).reduce(function (a, b) { + if (authors.length > maxAuthors) { + var others = { + value: authors.slice(maxAuthors).reduce(function (a, b) { return a + b.value; }, 0) , label: "Others" }; - authors = authors.slice(0, 50); + authors = authors.slice(0, maxAuthors); authors.push(others); } @@ -676,4 +677,613 @@ GitStats.DEFAULT_CONFIG = { , global_activity: false }; +/** + * getConfig + * Fetches the configuration object from file (`~/.git-stats-config.js`). + * + * @name getConfig + * @function + * @param {Function} callback The callback function. + * @return {Object|Undefined} If no callback is provided, the configuration object will be returned. + */ +GitStats.prototype.getConfig = function (callback) { + var data = {} + , err = null + ; + + try { + data = require(CONFIG_PATH); + } catch (err) { + if (err.code === "MODULE_NOT_FOUND") { + err = null; + data = {}; + } + } + + if (callback) { + return callback(err, data); + } else { + if (err) { + throw err; + } + } + + return data; +}; + +/** + * initConfig + * Inits the configuration field (`this.config`). + * + * @name initConfig + * @function + * @param {Object|String} input The path to a custom git-stats configuration file or the configuration object. + * @param {Function} callback The callback function. + */ +GitStats.prototype.initConfig = function (input, callback) { + + var self = this; + + if (Typpy(input, Function)) { + callback = input; + input = null; + } + + input = input || CONFIG_PATH; + + // Handle object input + if (Typpy(input, Object)) { + this.config = Ul.deepMerge(input, GitStats.DEFAULT_CONFIG); + callback && callback(null, this.config); + return this.config; + } + + if (callback) { + this.getConfig(function (err, data) { + if (err) { return callback(err); } + self.initConfig(data, callback); + }); + } else { + this.initConfig(this.getConfig()); + } +}; + +/** + * record + * Records a new commit. + * + * @name record + * @function + * @param {Object} data The commit data containing: + * + * - `date` (String|Date): The date object or a string in a format that can be parsed. + * - `url` (String): The repository remote url. + * - `hash` (String): The commit hash. + * - `_data` (Object): If this field is provided, it should be the content of the git-stats data file as object. It will be modified in-memory and then returned. + * - `save` (Boolean): If `false`, the result will *not* be saved in the file. + * + * @param {Function} callback The callback function. + * @return {GitStats} The `GitStats` instance. + */ +GitStats.prototype.record = function (data, callback) { + + var self = this; + + // Validate data + callback = callback || function (err) { if (err) throw err; }; + data = Object(data); + + if (typeof data.date === "string") { + data.date = new Moment(new Date(data.date)); + } + + if (!/^moment|date$/.test(Typpy(data.date))) { + callback(new Error("The date field should be a string or a date object.")); + return GitStats; + } else if (Typpy(data.date, Date)) { + data.date = Moment(data.date); + } + + if (typeof data.hash !== "string" || !data.hash) { + callback(new Error("Invalid hash.")); + return GitStats; + } + + // This is not used, but remains here just in case we need + // it in the future + if (typeof data.url !== "string" || !data.url) { + delete data.url; + } + + function modify (err, stats) { + + var commits = stats.commits + , day = data.date.format(DATE_FORMAT) + , today = commits[day] = Object(commits[day]) + ; + + today[data.hash] = 1; + + if (data.save === false) { + callback(null, stats); + } else { + self.save(stats, callback); + } + + return stats; + } + + // Check if we have input data + if (data._data) { + return modify(null, data._data); + } else { + // Get stats + self.get(modify); + } + + return self; +}; + +/** + * removeCommit + * Deletes a specifc commit from the history. + * + * @name record + * @function + * @param {Object} data The commit data containing: + * + * - `date` (String|Date): The date object or a string in a format that can be parsed. If not provided, the hash object will be searched in all dates. + * - `hash` (String): The commit hash. + * - `_data` (Object): If this field is provided, it should be the content of the git-stats data file as object. It will be modified in-memory and then returned. + * - `save` (Boolean): If `false`, the result will *not* be saved in the file. + * + * @param {Function} callback The callback function. + * @return {GitStats} The `GitStats` instance. + */ +GitStats.prototype.removeCommit = function (data, callback) { + + var self = this; + + // Validate data + callback = callback || function (err) { if (err) throw err; }; + data = Object(data); + + if (typeof data.date === "string") { + data.date = new Moment(new Date(data.date)); + } + + if (!/^moment|date$/.test(Typpy(data.date))) { + data.date = null; + } else if (Typpy(data.date, Date)) { + data.date = Moment(data.date); + } + + if (typeof data.hash !== "string" || !data.hash) { + callback(new Error("Invalid hash.")); + return GitStats; + } + + function modify (err, stats) { + + if (err) { return callback(err); } + if (!data.date) { + IterateObject(stats.commits, function (todayObj) { + delete todayObj[data.hash]; + }); + } else { + var commits = stats.commits + , day = data.date.format(DATE_FORMAT) + , today = commits[day] = Object(commits[day]) + ; + + delete today[data.hash]; + } + + if (data.save === false) { + callback(null, stats); + } else { + self.save(stats, callback); + } + + return stats; + } + + // Check if we have input data + if (data._data) { + return modify(null, data._data); + } else { + // Get stats + self.get(modify); + } + + return self; +}; + +/** + * get + * Gets the git stats. + * + * @name get + * @function + * @param {Function} callback The callback function. + * @return {GitStats} The `GitStats` instance. + */ +GitStats.prototype.get = function (callback) { + var self = this; + ReadJson(self.path, function (err, data) { + + if (err && err.code === "ENOENT") { + return self.save(DEFAULT_DATA, function (err) { + callback(err, DEFAULT_DATA); + }); + } + + if (err) { return callback(err); } + callback(null, data); + }); + return self; +}; + +/** + * save + * Saves the provided stats. + * + * @name save + * @function + * @param {Object} stats The stats to be saved. + * @param {Function} callback The callback function. + * @return {GitStats} The `GitStats` instance. + */ +GitStats.prototype.save = function (stats, callback) { + WriteJson(this.path, stats, callback); + return this; +}; + +/** + * iterateDays + * Iterate through the days, calling the callback function on each day. + * + * @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. + * @return {GitStats} The `GitStats` instance. + */ +GitStats.prototype.iterateDays = function (data, callback) { + + if (typeof data === "function") { + callback = data; + data = undefined; + } + + // Merge the defaults + data.end = data.end || Moment(); + data.start = data.start || Moment().subtract(1, "years"); + data.format = data.format || DATE_FORMAT; + + var start = new Moment(data.start.format(DATE_FORMAT), DATE_FORMAT) + , end = new Moment(data.end.format(DATE_FORMAT), DATE_FORMAT) + , 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); + callback(cDay, start); + start.add(1, "days"); + } + + return this; +}; + +/** + * 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. + * @return {GitStats} The `GitStats` instance. + */ +GitStats.prototype.graph = function (data, callback) { + + if (typeof data === "function") { + callback = data; + data = undefined; + } + + var self = this; + + // Get commits + self.get(function (err, stats) { + if (err) { return callback(err); } + + var cDayObj = null + , year = {} + ; + + // Iterate days + self.iterateDays(data, function (cDay) { + cDayObj = Object(stats.commits[cDay]); + cDayObj = year[cDay] = { + _: cDayObj + , c: Object.keys(cDayObj).length + }; + }); + + callback(null, year); + }); + + return self; +}; + +/** + * 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. + * @return {GitStats} The `GitStats` instance. + */ +GitStats.prototype.calendar = function (data, callback) { + + var self = this; + + self.graph(data, function (err, graph) { + if (err) { return callback(err); } + + 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; + if (cDay.c > cal.max) { + cal.max = cDay.c; + } + + 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); + }); + return self; +}; + +/** + * ansiCalendar + * Creates the ANSI contributions calendar. + * + * @name ansiCalendar + * @function + * @param {Object} options The object passed to the `calendar` method. + * @param {Function} callback The callback function. + * @return {GitStats} The `GitStats` instance. + */ +GitStats.prototype.ansiCalendar = function (options, callback) { + + if (typeof options === "function") { + callback = options; + options = undefined; + } + + var self = this; + + self.graph(options, function (err, graph) { + var cal = [] + , data = { + theme: options.theme + , start: options.start + , end: options.end + , firstDay: options.firstDay + , cal: cal + , raw: options.raw + } + ; + + self.iterateDays(options, function (cDay) { + var cDayObj = graph[cDay]; + if (!cDayObj) { return; } + cal.push([cDay, cDayObj.c]); + }); + + callback(null, CliGhCal(cal, data)); + }); + + return self; +}; + +/** + * authors + * Creates an array with the authors of a git repository. + * + * @name authors + * @function + * @param {String|Object} options The repo path or an object containing the following fields: + * + * - `repo` (String): The repository path. + * - `start` (String): The start date. + * - `end` (String): The end date. + * + * @param {Function} callback The callback function. + * @return {GitStats} The `GitStats` instance. + */ +GitStats.prototype.authors = function (options, callback) { + var repo = new Gry(options.repo); + repo.exec(['shortlog', '-s', '-n', '--all', '--since', options.start.toString(), '--until', options.end.toString()], function (err, stdout) { + if (err) { return callback(err); } + var lines = stdout.split("\n"); + var pieData = stdout.split("\n").map(function (c) { + var splits = c.split("\t").map(function (cc) { + return cc.trim(); + }); + return { + value: parseInt(splits[0]) + , label: splits[1] + }; + }); + callback(null, pieData); + }); + return this; +}; + +/** + * authorsPie + * Creates the authors pie. + * + * @name authorsPie + * @function + * @param {String|Object} options The repo path or an object containing the following fields: + * + * - `repo` (String): The repository path. + * - `radius` (Number): The pie radius. + * - `no_ansi` (Boolean): If `true`, the pie will not contain ansi characters. + * - `raw` (Boolean): If `true`, the raw JSON will be displayed. + * + * @param {Function} callback The callback function. + * @return {GitStats} The `GitStats` instance. + */ +GitStats.prototype.authorsPie = function (options, callback) { + + const radius = Math.round(process.stdout.rows / 2) || 20; + + if (typeof options === "string") { + options = { + repo: options + }; + } + + options = Ul.merge(options, { radius }); + + if (!IsThere(options.repo)) { + return callback(new Error("The repository folder doesn't exist.")); + } + + var self = this + , repo = new Gry(options.repo) + , pie = null + , pieData = [] + ; + + self.authors(options, function (err, authors) { + if (err) { return callback(err); } + if (authors.length > radius) { + var others = { + value: authors.slice(radius).reduce(function (a, b) { + return a + b.value; + }, 0) + , label: "Others" + }; + authors = authors.slice(0, radius); + authors.push(others); + } + + var data = { + legend: true + , flat: true + , no_ansi: options.no_ansi + , authors: authors + }; + + callback(null, options.raw ? data : new CliPie(options.radius, authors, data).toString()); + }); + + return self; +}; + +/** + * globalActivity + * Creates the global contributions calendar (all commits made by all committers). + * + * @name globalActivity + * @function + * @param {String|Object} options The repo path or an object containing the following fields: + * + * - `repo` (String): The repository path. + * - `start` (String): The start date. + * - `end` (String): The end date. + * - `theme` (String|Object): The calendar theme. + * - `raw` (Boolean): If `true`, the raw JSON will be displayed. + * + * @param {Function} callback The callback function. + * @return {GitStats} The `GitStats` instance. + */ +GitStats.prototype.globalActivity = function (options, callback) { + + if (typeof options === "string") { + options = { + repo: options + }; + } + + options.repo = Abs(options.repo); + + if (!IsThere(options.repo)) { + return callback(new Error("The repository folder doesn't exist.")); + } + + var commits = {} + , today = null + , cal = [] + ; + + var logArgs = ["log","--since", options.start.format(DATE_FORMAT), "--until", options.end.format(DATE_FORMAT)] + if(options.author){ + logArgs = logArgs.concat(["--author", options.author]) + } + + GitLogParser(Spawn("git",logArgs , { cwd: options.repo }).stdout).on("commit", function(commit) { + if (!commit) { return; } + today = Moment(commit.date).format(DATE_FORMAT); + commits[today] = commits[today] || 0; + ++commits[today]; + }).on("error", function (err) { + callback(err); + }).on("finish", function () { + Object.keys(commits).forEach(function (c) { + cal.push([c, commits[c]]) + }); + var data = { + theme: options.theme + , start: options.start + , end: options.end + , cal: cal + , raw: options.raw + }; + callback(null, CliGhCal(cal, data)); + }); + + return this; +}; + module.exports = GitStats; diff --git a/package.json b/package.json index e4b6ad6..1ce3fda 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,8 @@ "contributors": [ "Gnab ", "William Boman ", - "Fabian Furger " + "Fabian Furger ", + "Jonah Lawrence " ], "license": "MIT", "repository": { From 95176aeed0143cfba66b8f1c3077b1cfc4c494c2 Mon Sep 17 00:00:00 2001 From: Jonah Lawrence Date: Tue, 29 Mar 2022 02:20:20 +0000 Subject: [PATCH 2/3] fix: show more authors --- lib/index.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/lib/index.js b/lib/index.js index 7cdbe0b..74650a7 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1175,15 +1175,15 @@ GitStats.prototype.authors = function (options, callback) { */ GitStats.prototype.authorsPie = function (options, callback) { - const radius = Math.round(process.stdout.rows / 2) || 20; - if (typeof options === "string") { options = { repo: options }; } - options = Ul.merge(options, { radius }); + options = Ul.merge(options, { + radius: process.stdout.rows / 2 || 20 + }); if (!IsThere(options.repo)) { return callback(new Error("The repository folder doesn't exist.")); @@ -1196,15 +1196,16 @@ GitStats.prototype.authorsPie = function (options, callback) { ; self.authors(options, function (err, authors) { + const maxAuthors = 2 * options.radius; if (err) { return callback(err); } - if (authors.length > radius) { + if (authors.length > maxAuthors) { var others = { - value: authors.slice(radius).reduce(function (a, b) { + value: authors.slice(maxAuthors).reduce(function (a, b) { return a + b.value; }, 0) , label: "Others" }; - authors = authors.slice(0, radius); + authors = authors.slice(0, maxAuthors); authors.push(others); } From 89f05df273e1d61fc64416c315d7d67a066b6198 Mon Sep 17 00:00:00 2001 From: Jonah Lawrence Date: Tue, 7 Jun 2022 08:40:37 -0600 Subject: [PATCH 3/3] fix rebase --- lib/index.js | 612 +-------------------------------------------------- 1 file changed, 1 insertion(+), 611 deletions(-) diff --git a/lib/index.js b/lib/index.js index 74650a7..70cf452 100644 --- a/lib/index.js +++ b/lib/index.js @@ -563,7 +563,7 @@ class GitStats { const maxAuthors = 2 * options.radius; if (err) { return callback(err); } if (authors.length > maxAuthors) { - var others = { + let others = { value: authors.slice(maxAuthors).reduce(function (a, b) { return a + b.value; }, 0) @@ -677,614 +677,4 @@ GitStats.DEFAULT_CONFIG = { , global_activity: false }; -/** - * getConfig - * Fetches the configuration object from file (`~/.git-stats-config.js`). - * - * @name getConfig - * @function - * @param {Function} callback The callback function. - * @return {Object|Undefined} If no callback is provided, the configuration object will be returned. - */ -GitStats.prototype.getConfig = function (callback) { - var data = {} - , err = null - ; - - try { - data = require(CONFIG_PATH); - } catch (err) { - if (err.code === "MODULE_NOT_FOUND") { - err = null; - data = {}; - } - } - - if (callback) { - return callback(err, data); - } else { - if (err) { - throw err; - } - } - - return data; -}; - -/** - * initConfig - * Inits the configuration field (`this.config`). - * - * @name initConfig - * @function - * @param {Object|String} input The path to a custom git-stats configuration file or the configuration object. - * @param {Function} callback The callback function. - */ -GitStats.prototype.initConfig = function (input, callback) { - - var self = this; - - if (Typpy(input, Function)) { - callback = input; - input = null; - } - - input = input || CONFIG_PATH; - - // Handle object input - if (Typpy(input, Object)) { - this.config = Ul.deepMerge(input, GitStats.DEFAULT_CONFIG); - callback && callback(null, this.config); - return this.config; - } - - if (callback) { - this.getConfig(function (err, data) { - if (err) { return callback(err); } - self.initConfig(data, callback); - }); - } else { - this.initConfig(this.getConfig()); - } -}; - -/** - * record - * Records a new commit. - * - * @name record - * @function - * @param {Object} data The commit data containing: - * - * - `date` (String|Date): The date object or a string in a format that can be parsed. - * - `url` (String): The repository remote url. - * - `hash` (String): The commit hash. - * - `_data` (Object): If this field is provided, it should be the content of the git-stats data file as object. It will be modified in-memory and then returned. - * - `save` (Boolean): If `false`, the result will *not* be saved in the file. - * - * @param {Function} callback The callback function. - * @return {GitStats} The `GitStats` instance. - */ -GitStats.prototype.record = function (data, callback) { - - var self = this; - - // Validate data - callback = callback || function (err) { if (err) throw err; }; - data = Object(data); - - if (typeof data.date === "string") { - data.date = new Moment(new Date(data.date)); - } - - if (!/^moment|date$/.test(Typpy(data.date))) { - callback(new Error("The date field should be a string or a date object.")); - return GitStats; - } else if (Typpy(data.date, Date)) { - data.date = Moment(data.date); - } - - if (typeof data.hash !== "string" || !data.hash) { - callback(new Error("Invalid hash.")); - return GitStats; - } - - // This is not used, but remains here just in case we need - // it in the future - if (typeof data.url !== "string" || !data.url) { - delete data.url; - } - - function modify (err, stats) { - - var commits = stats.commits - , day = data.date.format(DATE_FORMAT) - , today = commits[day] = Object(commits[day]) - ; - - today[data.hash] = 1; - - if (data.save === false) { - callback(null, stats); - } else { - self.save(stats, callback); - } - - return stats; - } - - // Check if we have input data - if (data._data) { - return modify(null, data._data); - } else { - // Get stats - self.get(modify); - } - - return self; -}; - -/** - * removeCommit - * Deletes a specifc commit from the history. - * - * @name record - * @function - * @param {Object} data The commit data containing: - * - * - `date` (String|Date): The date object or a string in a format that can be parsed. If not provided, the hash object will be searched in all dates. - * - `hash` (String): The commit hash. - * - `_data` (Object): If this field is provided, it should be the content of the git-stats data file as object. It will be modified in-memory and then returned. - * - `save` (Boolean): If `false`, the result will *not* be saved in the file. - * - * @param {Function} callback The callback function. - * @return {GitStats} The `GitStats` instance. - */ -GitStats.prototype.removeCommit = function (data, callback) { - - var self = this; - - // Validate data - callback = callback || function (err) { if (err) throw err; }; - data = Object(data); - - if (typeof data.date === "string") { - data.date = new Moment(new Date(data.date)); - } - - if (!/^moment|date$/.test(Typpy(data.date))) { - data.date = null; - } else if (Typpy(data.date, Date)) { - data.date = Moment(data.date); - } - - if (typeof data.hash !== "string" || !data.hash) { - callback(new Error("Invalid hash.")); - return GitStats; - } - - function modify (err, stats) { - - if (err) { return callback(err); } - if (!data.date) { - IterateObject(stats.commits, function (todayObj) { - delete todayObj[data.hash]; - }); - } else { - var commits = stats.commits - , day = data.date.format(DATE_FORMAT) - , today = commits[day] = Object(commits[day]) - ; - - delete today[data.hash]; - } - - if (data.save === false) { - callback(null, stats); - } else { - self.save(stats, callback); - } - - return stats; - } - - // Check if we have input data - if (data._data) { - return modify(null, data._data); - } else { - // Get stats - self.get(modify); - } - - return self; -}; - -/** - * get - * Gets the git stats. - * - * @name get - * @function - * @param {Function} callback The callback function. - * @return {GitStats} The `GitStats` instance. - */ -GitStats.prototype.get = function (callback) { - var self = this; - ReadJson(self.path, function (err, data) { - - if (err && err.code === "ENOENT") { - return self.save(DEFAULT_DATA, function (err) { - callback(err, DEFAULT_DATA); - }); - } - - if (err) { return callback(err); } - callback(null, data); - }); - return self; -}; - -/** - * save - * Saves the provided stats. - * - * @name save - * @function - * @param {Object} stats The stats to be saved. - * @param {Function} callback The callback function. - * @return {GitStats} The `GitStats` instance. - */ -GitStats.prototype.save = function (stats, callback) { - WriteJson(this.path, stats, callback); - return this; -}; - -/** - * iterateDays - * Iterate through the days, calling the callback function on each day. - * - * @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. - * @return {GitStats} The `GitStats` instance. - */ -GitStats.prototype.iterateDays = function (data, callback) { - - if (typeof data === "function") { - callback = data; - data = undefined; - } - - // Merge the defaults - data.end = data.end || Moment(); - data.start = data.start || Moment().subtract(1, "years"); - data.format = data.format || DATE_FORMAT; - - var start = new Moment(data.start.format(DATE_FORMAT), DATE_FORMAT) - , end = new Moment(data.end.format(DATE_FORMAT), DATE_FORMAT) - , 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); - callback(cDay, start); - start.add(1, "days"); - } - - return this; -}; - -/** - * 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. - * @return {GitStats} The `GitStats` instance. - */ -GitStats.prototype.graph = function (data, callback) { - - if (typeof data === "function") { - callback = data; - data = undefined; - } - - var self = this; - - // Get commits - self.get(function (err, stats) { - if (err) { return callback(err); } - - var cDayObj = null - , year = {} - ; - - // Iterate days - self.iterateDays(data, function (cDay) { - cDayObj = Object(stats.commits[cDay]); - cDayObj = year[cDay] = { - _: cDayObj - , c: Object.keys(cDayObj).length - }; - }); - - callback(null, year); - }); - - return self; -}; - -/** - * 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. - * @return {GitStats} The `GitStats` instance. - */ -GitStats.prototype.calendar = function (data, callback) { - - var self = this; - - self.graph(data, function (err, graph) { - if (err) { return callback(err); } - - 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; - if (cDay.c > cal.max) { - cal.max = cDay.c; - } - - 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); - }); - return self; -}; - -/** - * ansiCalendar - * Creates the ANSI contributions calendar. - * - * @name ansiCalendar - * @function - * @param {Object} options The object passed to the `calendar` method. - * @param {Function} callback The callback function. - * @return {GitStats} The `GitStats` instance. - */ -GitStats.prototype.ansiCalendar = function (options, callback) { - - if (typeof options === "function") { - callback = options; - options = undefined; - } - - var self = this; - - self.graph(options, function (err, graph) { - var cal = [] - , data = { - theme: options.theme - , start: options.start - , end: options.end - , firstDay: options.firstDay - , cal: cal - , raw: options.raw - } - ; - - self.iterateDays(options, function (cDay) { - var cDayObj = graph[cDay]; - if (!cDayObj) { return; } - cal.push([cDay, cDayObj.c]); - }); - - callback(null, CliGhCal(cal, data)); - }); - - return self; -}; - -/** - * authors - * Creates an array with the authors of a git repository. - * - * @name authors - * @function - * @param {String|Object} options The repo path or an object containing the following fields: - * - * - `repo` (String): The repository path. - * - `start` (String): The start date. - * - `end` (String): The end date. - * - * @param {Function} callback The callback function. - * @return {GitStats} The `GitStats` instance. - */ -GitStats.prototype.authors = function (options, callback) { - var repo = new Gry(options.repo); - repo.exec(['shortlog', '-s', '-n', '--all', '--since', options.start.toString(), '--until', options.end.toString()], function (err, stdout) { - if (err) { return callback(err); } - var lines = stdout.split("\n"); - var pieData = stdout.split("\n").map(function (c) { - var splits = c.split("\t").map(function (cc) { - return cc.trim(); - }); - return { - value: parseInt(splits[0]) - , label: splits[1] - }; - }); - callback(null, pieData); - }); - return this; -}; - -/** - * authorsPie - * Creates the authors pie. - * - * @name authorsPie - * @function - * @param {String|Object} options The repo path or an object containing the following fields: - * - * - `repo` (String): The repository path. - * - `radius` (Number): The pie radius. - * - `no_ansi` (Boolean): If `true`, the pie will not contain ansi characters. - * - `raw` (Boolean): If `true`, the raw JSON will be displayed. - * - * @param {Function} callback The callback function. - * @return {GitStats} The `GitStats` instance. - */ -GitStats.prototype.authorsPie = function (options, callback) { - - if (typeof options === "string") { - options = { - repo: options - }; - } - - options = Ul.merge(options, { - radius: process.stdout.rows / 2 || 20 - }); - - if (!IsThere(options.repo)) { - return callback(new Error("The repository folder doesn't exist.")); - } - - var self = this - , repo = new Gry(options.repo) - , pie = null - , pieData = [] - ; - - self.authors(options, function (err, authors) { - const maxAuthors = 2 * options.radius; - if (err) { return callback(err); } - if (authors.length > maxAuthors) { - var others = { - value: authors.slice(maxAuthors).reduce(function (a, b) { - return a + b.value; - }, 0) - , label: "Others" - }; - authors = authors.slice(0, maxAuthors); - authors.push(others); - } - - var data = { - legend: true - , flat: true - , no_ansi: options.no_ansi - , authors: authors - }; - - callback(null, options.raw ? data : new CliPie(options.radius, authors, data).toString()); - }); - - return self; -}; - -/** - * globalActivity - * Creates the global contributions calendar (all commits made by all committers). - * - * @name globalActivity - * @function - * @param {String|Object} options The repo path or an object containing the following fields: - * - * - `repo` (String): The repository path. - * - `start` (String): The start date. - * - `end` (String): The end date. - * - `theme` (String|Object): The calendar theme. - * - `raw` (Boolean): If `true`, the raw JSON will be displayed. - * - * @param {Function} callback The callback function. - * @return {GitStats} The `GitStats` instance. - */ -GitStats.prototype.globalActivity = function (options, callback) { - - if (typeof options === "string") { - options = { - repo: options - }; - } - - options.repo = Abs(options.repo); - - if (!IsThere(options.repo)) { - return callback(new Error("The repository folder doesn't exist.")); - } - - var commits = {} - , today = null - , cal = [] - ; - - var logArgs = ["log","--since", options.start.format(DATE_FORMAT), "--until", options.end.format(DATE_FORMAT)] - if(options.author){ - logArgs = logArgs.concat(["--author", options.author]) - } - - GitLogParser(Spawn("git",logArgs , { cwd: options.repo }).stdout).on("commit", function(commit) { - if (!commit) { return; } - today = Moment(commit.date).format(DATE_FORMAT); - commits[today] = commits[today] || 0; - ++commits[today]; - }).on("error", function (err) { - callback(err); - }).on("finish", function () { - Object.keys(commits).forEach(function (c) { - cal.push([c, commits[c]]) - }); - var data = { - theme: options.theme - , start: options.start - , end: options.end - , cal: cal - , raw: options.raw - }; - callback(null, CliGhCal(cal, data)); - }); - - return this; -}; - module.exports = GitStats;