Skip to content

Commit f0c08b2

Browse files
committed
Merge branch 'feature-author-additions-deletions' of github.com:Happyesss/git-stats into new-version
2 parents 775c04a + a4e4fc5 commit f0c08b2

File tree

5 files changed

+280
-2
lines changed

5 files changed

+280
-2
lines changed

DOCUMENTATION.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,39 @@ Creates the authors pie.
158158
#### Return
159159
- **GitStats** The `GitStats` instance.
160160

161+
### `authorsStats(options, callback)`
162+
Creates an array with the authors and their additions/deletions statistics.
163+
164+
#### Params
165+
166+
- **String|Object** `options`: The repo path or an object containing the following fields:
167+
- `repo` (String): The repository path.
168+
- `start` (String): The start date.
169+
- `end` (String): The end date.
170+
- `mode` (String): 'additions', 'deletions', or 'both' (default: 'both').
171+
- **Function** `callback`: The callback function.
172+
173+
#### Return
174+
- **GitStats** The `GitStats` instance.
175+
176+
### `authorsStatsPie(options, callback)`
177+
Creates a pie chart showing author statistics (additions/deletions).
178+
179+
#### Params
180+
181+
- **String|Object** `options`: The repo path or an object containing the following fields:
182+
- `repo` (String): The repository path.
183+
- `start` (String): The start date.
184+
- `end` (String): The end date.
185+
- `mode` (String): 'additions', 'deletions', or 'both' (default: 'both').
186+
- `radius` (Number): The pie radius.
187+
- `no_ansi` (Boolean): If `true`, the pie will not contain ansi characters.
188+
- `raw` (Boolean): If `true`, the raw JSON will be displayed.
189+
- **Function** `callback`: The callback function.
190+
191+
#### Return
192+
- **GitStats** The `GitStats` instance.
193+
161194
### `globalActivity(options, callback)`
162195
Creates the global contributions calendar (all commits made by all committers).
163196

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,10 @@ Options:
103103
repository.
104104
-a, --authors Shows a pie chart with the author related
105105
contributions in the current repository.
106+
-S, --author-stats Shows a pie chart with author additions/deletions
107+
statistics.
108+
-M, --stats-mode <mode> Mode for author stats: 'additions', 'deletions', or
109+
'both' (default: 'both').
106110
-u, --until <date> Optional end date.
107111
-s, --since <date> Optional start date.
108112
--record <data> Records a new commit. Don't use this unless you are
@@ -116,6 +120,9 @@ Examples:
116120
$ git-stats -l # Light mode
117121
$ git-stats -s '1 January, 2012' # All the commits from 1 January 2012 to now
118122
$ git-stats -s '1 January, 2012' -u '31 December, 2012' # All the commits from 2012
123+
$ git-stats -S # Shows author additions/deletions statistics pie chart
124+
$ git-stats -S -M additions # Shows only additions statistics
125+
$ git-stats -S -M deletions # Shows only deletions statistics
119126
120127
Your commit history is kept in ~/.git-stats by default. You can create
121128
~/.git-stats-config.js to specify different defaults.

bin/git-stats

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,16 @@ new Tilda(`${__dirname}/../package.json`, {
5858
opts: ["A", "author"]
5959
, desc: "Filter author related contributions in the current repository."
6060
}
61+
, {
62+
opts: ["S", "author-stats"]
63+
, desc: "Shows a pie chart with author additions/deletions statistics."
64+
}
65+
, {
66+
opts: ["M", "stats-mode"]
67+
, desc: "Mode for author stats: 'additions', 'deletions', or 'both' (default: 'both')."
68+
, name: "mode"
69+
, default: "both"
70+
}
6171
, {
6272
opts: ["n", "disable-ansi"]
6373
, desc: "Forces the tool not to use ANSI styles."
@@ -86,6 +96,9 @@ new Tilda(`${__dirname}/../package.json`, {
8696
, "git-stats -l # Light mode"
8797
, "git-stats -s '1 January, 2012' # All the commits from 1 January 2012 to now"
8898
, "git-stats -s '1 January, 2012' -u '31 December, 2012' # All the commits from 2012"
99+
, "git-stats -S # Shows author additions/deletions statistics pie chart"
100+
, "git-stats -S -M additions # Shows only additions statistics"
101+
, "git-stats -S -M deletions # Shows only deletions statistics"
89102
]
90103
, notes: "Your commit history is kept in ~/.git-stats by default. You can create ~/.git-stats-config.js to specify different defaults."
91104
}).main(action => {
@@ -100,6 +113,8 @@ new Tilda(`${__dirname}/../package.json`, {
100113
, globalActivityOpt = action.options.globalActivity
101114
, rawOpt = action.options.raw
102115
, authorOpt = action.options.author
116+
, authorStatsOpt = action.options.authorStats
117+
, statsModeOpt = action.options.statsMode
103118
;
104119

105120
let options = {};
@@ -162,7 +177,7 @@ new Tilda(`${__dirname}/../package.json`, {
162177
}
163178

164179
// Add the repo path
165-
if (authorsOpt.is_provided || globalActivityOpt.is_provided) {
180+
if (authorsOpt.is_provided || globalActivityOpt.is_provided || authorStatsOpt.is_provided) {
166181
options.repo = process.cwd();
167182
}
168183

@@ -177,7 +192,14 @@ new Tilda(`${__dirname}/../package.json`, {
177192
options.radius = (process.stdout.rows / 2) - 4;
178193
}
179194

180-
if (!authorsOpt.is_provided || globalActivityOpt.is_provided) {
195+
// Handle author stats
196+
if (authorStatsOpt.is_provided) {
197+
options.no_ansi = noAnsiOpt.is_provided;
198+
options.radius = (process.stdout.rows / 2) - 4;
199+
options.mode = statsModeOpt.value || 'both';
200+
}
201+
202+
if (!authorsOpt.is_provided && !authorStatsOpt.is_provided || globalActivityOpt.is_provided) {
181203
// This can be a string or an object
182204
if (/^object|string$/.test(Typpy(GitStats.config.theme)) && !noAnsiOpt.is_provided && !lightOpt.is_provided) {
183205
options.theme = GitStats.config.theme;
@@ -205,6 +227,10 @@ new Tilda(`${__dirname}/../package.json`, {
205227
return GitStats.globalActivity(options, display);
206228
}
207229

230+
if (authorStatsOpt.is_provided) {
231+
return GitStats.authorsStatsPie(options, display);
232+
}
233+
208234
// Show the graphs
209235
GitStats[authorsOpt.is_provided ? "authorsPie" : "ansiCalendar"](options, display);
210236
});

example/author-stats.js

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Dependencies
2+
const GitStats = require("../lib");
3+
4+
// Create the GitStats instance
5+
const g1 = new GitStats();
6+
7+
console.log("Author Statistics Examples:\n");
8+
9+
// Display author statistics (both additions and deletions)
10+
console.log("1. Author statistics (additions + deletions):");
11+
g1.authorsStatsPie({
12+
repo: process.cwd(),
13+
mode: "both"
14+
}, function (err, data) {
15+
console.log(err || data);
16+
console.log("\n" + "=".repeat(50) + "\n");
17+
18+
// Display only additions
19+
console.log("2. Author statistics (additions only):");
20+
g1.authorsStatsPie({
21+
repo: process.cwd(),
22+
mode: "additions"
23+
}, function (err, data) {
24+
console.log(err || data);
25+
console.log("\n" + "=".repeat(50) + "\n");
26+
27+
// Display only deletions
28+
console.log("3. Author statistics (deletions only):");
29+
g1.authorsStatsPie({
30+
repo: process.cwd(),
31+
mode: "deletions"
32+
}, function (err, data) {
33+
console.log(err || data);
34+
console.log("\n" + "=".repeat(50) + "\n");
35+
36+
// Display raw JSON data
37+
console.log("4. Raw JSON data:");
38+
g1.authorsStats({
39+
repo: process.cwd(),
40+
mode: "both"
41+
}, function (err, data) {
42+
console.log(err || JSON.stringify(data, null, 2));
43+
});
44+
});
45+
});
46+
});

lib/index.js

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -612,6 +612,172 @@ class GitStats {
612612
return self;
613613
}
614614

615+
/**
616+
* authorsStats
617+
* Creates an array with the authors and their additions/deletions statistics.
618+
*
619+
* @name authorsStats
620+
* @function
621+
* @param {String|Object} options The repo path or an object containing the following fields:
622+
*
623+
* - `repo` (String): The repository path.
624+
* - `start` (String): The start date.
625+
* - `end` (String): The end date.
626+
* - `mode` (String): 'additions', 'deletions', or 'both' (default: 'both').
627+
*
628+
* @param {Function} callback The callback function.
629+
* @return {GitStats} The `GitStats` instance.
630+
*/
631+
authorsStats(options, callback) {
632+
if (typeof options === "string") {
633+
options = {
634+
repo: options
635+
};
636+
}
637+
638+
// Set default dates if not provided
639+
options = Ul.merge(options, {
640+
start: options.start || Moment().subtract(1, "years"),
641+
end: options.end || Moment()
642+
});
643+
644+
const repo = new Gry(options.repo);
645+
const mode = options.mode || 'both';
646+
647+
// Use git log with --numstat to get additions and deletions
648+
repo.exec(['log', '--pretty=format:%aN', '--numstat', '--since', options.start.toString(), '--until', options.end.toString()], function (err, stdout) {
649+
if (err) {
650+
return callback(err);
651+
}
652+
653+
const lines = stdout.split('\n');
654+
const authorStats = {};
655+
let currentAuthor = null;
656+
657+
lines.forEach(function(line) {
658+
line = line.trim();
659+
if (!line) return;
660+
661+
// Check if this line is an author name (doesn't start with numbers)
662+
if (!/^\d/.test(line)) {
663+
currentAuthor = line;
664+
if (!authorStats[currentAuthor]) {
665+
authorStats[currentAuthor] = { additions: 0, deletions: 0 };
666+
}
667+
} else if (currentAuthor) {
668+
// This is a numstat line (additions, deletions, filename)
669+
const parts = line.split('\t');
670+
if (parts.length >= 2) {
671+
const additions = parseInt(parts[0]) || 0;
672+
const deletions = parseInt(parts[1]) || 0;
673+
authorStats[currentAuthor].additions += additions;
674+
authorStats[currentAuthor].deletions += deletions;
675+
}
676+
}
677+
});
678+
679+
// Convert to array format and sort by the requested metric
680+
const result = Object.keys(authorStats).map(function(author) {
681+
const stats = authorStats[author];
682+
let value;
683+
let label = author;
684+
685+
if (mode === 'additions') {
686+
value = stats.additions;
687+
label += ` (+${stats.additions})`;
688+
} else if (mode === 'deletions') {
689+
value = stats.deletions;
690+
label += ` (-${stats.deletions})`;
691+
} else {
692+
value = stats.additions + stats.deletions;
693+
label += ` (+${stats.additions}/-${stats.deletions})`;
694+
}
695+
696+
return {
697+
value: value,
698+
label: label,
699+
author: author,
700+
additions: stats.additions,
701+
deletions: stats.deletions
702+
};
703+
}).filter(function(item) {
704+
return item.value > 0;
705+
}).sort(function(a, b) {
706+
return b.value - a.value;
707+
});
708+
709+
callback(null, result);
710+
});
711+
return this;
712+
}
713+
714+
/**
715+
* authorsStatsPie
716+
* Creates a pie chart showing author statistics (additions/deletions).
717+
*
718+
* @name authorsStatsPie
719+
* @function
720+
* @param {String|Object} options The repo path or an object containing the following fields:
721+
*
722+
* - `repo` (String): The repository path.
723+
* - `start` (String): The start date.
724+
* - `end` (String): The end date.
725+
* - `mode` (String): 'additions', 'deletions', or 'both' (default: 'both').
726+
* - `radius` (Number): The pie radius.
727+
* - `no_ansi` (Boolean): If `true`, the pie will not contain ansi characters.
728+
* - `raw` (Boolean): If `true`, the raw JSON will be displayed.
729+
*
730+
* @param {Function} callback The callback function.
731+
* @return {GitStats} The `GitStats` instance.
732+
*/
733+
authorsStatsPie(options, callback) {
734+
if (typeof options === "string") {
735+
options = {
736+
repo: options
737+
};
738+
}
739+
740+
options = Ul.merge(options, {
741+
radius: process.stdout.rows / 2 || 20,
742+
mode: 'both'
743+
});
744+
745+
if (!IsThere(options.repo)) {
746+
return callback(new Error("The repository folder doesn't exist."));
747+
}
748+
749+
let self = this;
750+
751+
self.authorsStats(options, function (err, authors) {
752+
if (err) {
753+
return callback(err);
754+
}
755+
756+
const maxAuthors = 2 * options.radius;
757+
if (authors.length > maxAuthors) {
758+
let others = {
759+
value: authors.slice(maxAuthors).reduce(function (a, b) {
760+
return a + b.value;
761+
}, 0),
762+
label: "Others"
763+
};
764+
authors = authors.slice(0, maxAuthors);
765+
authors.push(others);
766+
}
767+
768+
let data = {
769+
legend: true,
770+
flat: true,
771+
no_ansi: options.no_ansi,
772+
authors: authors
773+
};
774+
775+
callback(null, options.raw ? data : new CliPie(options.radius, authors, data).toString());
776+
});
777+
778+
return self;
779+
}
780+
615781
/**
616782
* globalActivity
617783
* Creates the global contributions calendar (all commits made by all committers).

0 commit comments

Comments
 (0)