blob: 94681579e1911691a325999c83f37a064d6aac3b [file] [log] [blame]
Martin Roth0ad5fbd2020-12-24 12:06:38 -07001#!/usr/bin/env perl
Martin Rothce005da2016-04-01 14:30:33 -06002#
Patrick Georgi7333a112020-05-08 20:48:04 +02003# SPDX-License-Identifier: GPL-2.0-only
Martin Rothce005da2016-04-01 14:30:33 -06004
5package gerrit_stats;
6
7# To install any needed modules install the cpanm app, and use it to install the required modules:
8# sudo cpan App::cpanminus
9# sudo /usr/local/bin/cpanm JSON::Util Net::OpenSSH DateTime Devel::Size
10
11use strict;
12use warnings;
13use English qw( -no_match_vars );
14use File::Find;
15use File::Path;
16use Getopt::Long;
17use Getopt::Std;
18use JSON::Util;
19use Net::OpenSSH;
20use Data::Dumper qw(Dumper);
21use DateTime;
22use Devel::Size qw(size total_size);
23
24my $old_version;
25my $new_version;
26my $infodir="$ENV{'HOME'}/.commit_info/" . `git config -l | grep remote.origin.url | sed 's|.*@||' | sed 's|:.*||'`;
27chomp($infodir);
28my $URL_WITH_USER;
29my $SKIP_GERRIT_CHECK;
30my $print_commit_list = 1;
31
32#disable print buffering
33$OUTPUT_AUTOFLUSH = 1;
34binmode STDOUT, ":utf8";
35
36Main();
37
38#-------------------------------------------------------------------------------
39# Main
40#-------------------------------------------------------------------------------
41sub Main {
42 check_arguments();
43
44 my %submitters = ();
45 my %authors = ();
46 my %owners = ();
47 my %reviewers = ();
48 my %author_added = ();
49 my %author_removed = ();
50 my $total_added = 0;
51 my $total_removed = 0;
52 my $number_of_commits = 0;
53 my $number_of_submitters = 0;
54 my $submit_epoch = "";
55 my $first_submit_epoch = "";
56 if (!$URL_WITH_USER) {
57 get_user()
58 }
59
60 # make sure the versions exist
61 check_versions();
62
63 #fetch patches if needed. Get ids of first and last commits
64 my @commits = `git log --pretty=%h "$old_version..$new_version" 2>/dev/null`;
65 get_commits(@commits);
66 my $last_commit_id = $commits[0];
67 my $first_commit_id = $commits[@commits - 1];
68 chomp $last_commit_id;
69 chomp $first_commit_id;
70
71 print "Statistics from commit $first_commit_id to commit $last_commit_id\n";
72 print "Patch, Date, Owner, Author, Submitter, Inserted lines, Deleted lines, Subject, Reviewers\n";
73
74 #loop through all commits
75 for my $commit_id (@commits) {
76 $commit_id =~ s/^\s+|\s+$//g;
77
78 my $submitter = "";
79 my %patch_reviewers = ();
80 my $info;
81 my $owner;
82 my $author;
83 my $author_email;
84 my $inserted_lines = 0;
85 my $deleted_lines = 0;
86 my $subject;
87
88 $number_of_commits++;
89 print "\"$commit_id\", ";
90
91 #read the data file for the current commit
92 if (-f "$infodir/$commit_id" && -s "$infodir/$commit_id" > 20) {
93 open( my $HANDLE, "<", "$infodir/$commit_id" ) or die "Error: could not open file '$infodir/$commit_id'\n";
94 $info = <$HANDLE>;
95 close $HANDLE;
96
97 my $commit_info = JSON::Util->decode($info);
98
99 #get the easy data
100 $owner = $commit_info->{'owner'}{'name'};
101 if (! $owner) {
102 $owner = $commit_info->{'owner'}{'username'};
103 }
104 if (! $owner) {
105 $owner = "";
106 }
107 $author = $commit_info->{'currentPatchSet'}{'author'}{'name'};
108 $author_email = $commit_info->{'currentPatchSet'}{'author'}{'email'};
109 if (! $author) {
110 $author = $commit_info->{'currentPatchSet'}{'author'}{'username'};
111 }
112
113 $inserted_lines = $commit_info->{'currentPatchSet'}{'sizeInsertions'};
114 $deleted_lines = $commit_info->{'currentPatchSet'}{'sizeDeletions'};
115 $subject = $commit_info->{'subject'};
116
117 #get the patch's submitter
118 my $approvals = $commit_info->{'currentPatchSet'}{'approvals'};
119 for my $approval (@$approvals) {
120 if ($approval->{'type'} eq "SUBM") {
121 $submit_epoch = $approval->{'grantedOn'};
122 $submitter = $approval->{'by'}{'name'};
123 }
124 }
125
126 #get all the reviewers for all patch revisions
127 my $patchsets = $commit_info->{'patchSets'};
128 for my $patch (@$patchsets) {
129 if (! $author) {
130 $author = $patch->{'author'}{'name'};
131 }
132 my $approvals = $patch->{'approvals'};
133 for my $approval (@$approvals) {
134
135 if ( (! $submitter) && ($approval->{'type'} eq "SUBM")) {
136 $submit_epoch = $approval->{'grantedOn'};
137 $submitter = $approval->{'by'}{'name'};
138 }
139
140 if ($approval->{'type'} eq "Code-Review") {
141 my $patch_reviewer = $approval->{'by'}{'name'};
142 if ($patch_reviewer) {
143 if (exists $patch_reviewers{$patch_reviewer}) {
144 $patch_reviewers{$patch_reviewer}++;
145 } else {
146 $patch_reviewers{$patch_reviewer} = 1;
147 }
148 }
149 }
150 }
151 }
152
153 } else {
154 # get the info from git
155 my $logline = `git log --pretty="%ct@@@%s@@@%an@@@%aE@@@%cn" $commit_id^..$commit_id --`;
156 $logline =~ m/^(.*)@@@(.*)@@@(.*)@@@(.*)@@@(.*)\n/;
157 ($submit_epoch, $subject, $author, $author_email, $submitter) = ($1, $2, $3, $4, $5);
158 $owner = $author;
159 $logline = `git log --pretty= --shortstat $commit_id^..$commit_id --`;
160 if ($logline =~ m/\s+(\d+)\s+insertion/) {
161 $inserted_lines = $1;
162 }
163 if ($logline =~ m/\s+(\d+)\s+deletion/) {
164 $deleted_lines = $1 * -1;
165 }
166 my @loglines = `git log $commit_id^..$commit_id -- | grep '\\sReviewed-by:'`;
167 for my $line (@loglines){
168 if ($line =~ m/.*:\s+(.*)\s</) {
169 my $patch_reviewer = $1;
170 if ($patch_reviewer) {
171 if (exists $patch_reviewers{$patch_reviewer}) {
172 $patch_reviewers{$patch_reviewer}++;
173 } else {
174 $patch_reviewers{$patch_reviewer} = 1;
175 }
176 }
177 }
178 }
179
180 }
181
182 # Not entirely certain why this is needed, but for a number of patches have been submitted
183 # the submit time in gerrit is set to April 9, 2015.
184 if ($submit_epoch == 1428586219){
185 my $logline = `git log --pretty="%ct" $commit_id^..$commit_id --`;
186 $logline =~ m/^(.*)\n/;
187 $submit_epoch = $1;
188 }
189
190 #add the count and owner to the submitter hash
191 if (exists $submitters{$submitter}) {
192 $submitters{$submitter}++;
193 } else {
194 $submitters{$submitter} = 1;
195 $number_of_submitters++;
196 }
197
198 #create a readable date
199 my $dt = DateTime->from_epoch(epoch => $submit_epoch);
200 $dt->set_time_zone( 'Europe/Paris' );
201 my $submit_time = $dt->strftime('%Y/%m/%d %H:%M:%S');
202 if (!$first_submit_epoch) {
203 $first_submit_epoch = $submit_epoch;
204 }
205
206 #create the list of reviewers to print
207 my $reviewerlist = "";
208 foreach my $reviewer (keys %patch_reviewers) {
209 if ($reviewerlist eq "") {
210 $reviewerlist = $reviewer;
211 } else {
212 $reviewerlist .= ", $reviewer";
213 }
214
215 if (exists $reviewers{$reviewer}) {
216 $reviewers{$reviewer}++;
217 } else {
218 $reviewers{$reviewer} = 1;
219 }
220 }
221 if (! $reviewerlist) {
222 $reviewerlist = "-"
223 }
224
225 if ($print_commit_list) {
226 print "$submit_time, $owner, $author, $submitter, $inserted_lines, $deleted_lines, \"$subject\", \"$reviewerlist\"\n";
227 } else {
228 print "$number_of_commits\n";
229 }
230 $total_added += $inserted_lines;
231 $total_removed += $deleted_lines;
232 if (exists $owners{$owner}) {
233 $owners{$owner}++;
234 } else {
235 $owners{$owner} = 1;
236 }
237
238 if (exists $authors{$author}{"num"}) {
239 $authors{$author}{"num"}++;
240 $author_added{$author} += $inserted_lines;
241 $author_removed{$author} += $deleted_lines;
242 $authors{$author}{"earliest_commit"}=$submit_time;
243 } else {
244 $authors{$author}{"num"} = 1;
245 $authors{$author}{"latest_commit"}=$submit_time;
246 $authors{$author}{"earliest_commit"}=$submit_time;
247 $author_added{$author} = $inserted_lines;
248 $author_removed{$author} = $deleted_lines;
249 }
250 if (! exists $authors{$author}{email} && $author_email) {
251 $authors{$author}{email} = "$author_email";
252 }
253 }
254 my $Days = ($first_submit_epoch - $submit_epoch) / 86400;
255 if (($first_submit_epoch - $submit_epoch) % 86400) {
256 $Days += 1;
257 }
258
259 print "- Total Commits: $number_of_commits\n";
260 printf "- Average Commits per day: %.2f\n", $number_of_commits / $Days;
261 print "- Total lines added: $total_added\n";
262 print "- Total lines removed: $total_removed\n";
263 print "- Total difference: " . ($total_added + $total_removed) . "\n\n";
264
265 print "=== Authors - Number of commits ===\n";
266 my $number_of_authors = 0;
267 foreach my $author (sort { $authors{$b}{num} <=> $authors{$a}{num} } (keys %authors) ) {
268 if (! exists $authors{$author}{"email"}) {
269 $authors{$author}{"email"} = "-";
270 }
271 printf "%-25s %5d %-40s (%2.2f%%) {%s / %s}\n",$author, $authors{$author}{"num"}, $authors{$author}{"email"}, $authors{$author}{"num"} / $number_of_commits * 100, $authors{$author}{"latest_commit"}, $authors{$author}{"earliest_commit"};
272 $number_of_authors++;
273 }
274 print "Total Authors: $number_of_authors\n\n";
275
276 print "=== Authors - Lines added ===\n";
277 foreach my $author (sort { $author_added{$b} <=> $author_added{$a} } (keys %author_added) ) {
278 if ($author_added{$author}) {
279 printf "%-25s %5d (%2.3f%%)\n",$author, $author_added{$author}, $author_added{$author} / $total_added * 100;
280 }
281 }
282 print "\n";
283
284 print "=== Authors - Lines removed ===\n";
285 foreach my $author (sort { $author_removed{$a} <=> $author_removed{$b} } (keys %author_removed) ) {
286 if ($author_removed{$author}) {
287 printf "%-25s %5d (%2.3f%%)\n",$author,$author_removed{$author} * -1, $author_removed{$author} / $total_removed * 100;
288 }
289 }
290 print "\n";
291
292 print "=== Reviewers - Number of patches reviewed ===\n";
293 my $number_of_reviewers = 0;
294 foreach my $reviewer (sort { $reviewers{$b} <=> $reviewers{$a} } (keys %reviewers) ) {
295 printf "%-25s %5d (%2.3f%%)\n",$reviewer, $reviewers{$reviewer}, $reviewers{$reviewer} / $number_of_commits * 100;
296 $number_of_reviewers++;
297 }
298 print "Total Reviewers: $number_of_reviewers\n\n";
299
300 print "=== Submitters - Number of patches submitted ===\n";
301 foreach my $submitter (sort { $submitters{$b} <=> $submitters{$a} } (keys %submitters) ) {
302 printf "%-25s %5d (%2.3f%%)\n",$submitter, $submitters{$submitter}, $submitters{$submitter} / $number_of_commits * 100;
303 }
304 print "Total Submitters: $number_of_submitters\n\n";
305
306 print "Commits, Ave, Added, Removed, Diff, Authors, Reviewers, Submitters\n";
307 printf "$number_of_commits, %.2f, $total_added, $total_removed, " . ($total_added + $total_removed) . ", $number_of_authors, $number_of_reviewers, $number_of_submitters\n", $number_of_commits / $Days;
308}
309
310#-------------------------------------------------------------------------------
311#-------------------------------------------------------------------------------
312sub check_versions {
313 `git cat-file -e $old_version^{commit} 2>/dev/null`;
314 if (${^CHILD_ERROR_NATIVE}){
315 print "Error: Old version ($old_version) does not exist.\n";
316 exit 1;
317 }
318
319 `git cat-file -e $new_version^{commit} 2>/dev/null`;
320 if (${^CHILD_ERROR_NATIVE}){
321 print "Error: New version ($new_version) does not exist.\n";
322 exit 1;
323 }
324}
325
326#-------------------------------------------------------------------------------
327#-------------------------------------------------------------------------------
328sub get_user {
329 my $url=`git config -l | grep remote.origin.url`;
330
331 if ($url =~ /.*url=ssh:\/\/(\w+@[a-zA-Z][a-zA-Z0-9\.]+:\d+)/)
332 {
333 $URL_WITH_USER = $1;
334 } else {
335 print "Error: Could not get a ssh url with a username from gitconfig.\n";
336 print " use the -u option to set a url.\n";
337 exit 1;
338 }
339}
340
341#-------------------------------------------------------------------------------
342#-------------------------------------------------------------------------------
343sub get_commits {
344 my @commits = @_;
345 my $submit_time = "";
346 if (defined $SKIP_GERRIT_CHECK) {
347 return;
348 }
349 my $ssh = Net::OpenSSH->new("$URL_WITH_USER", );
350 $ssh->error and die "Couldn't establish SSH connection to $URL_WITH_USER:". $ssh->error;
351
352 print "Using URL: ssh://$URL_WITH_USER\n";
353
354 if (! -d $infodir) {
355 mkpath($infodir)
356 }
357
358 for my $commit_id (@commits) {
359 $commit_id =~ s/^\s+|\s+$//g;
360 $submit_time = "";
361 my $gerrit_review;
362
363 # Quit if we've reeached the last coreboot commit supporting these queries
364 if ($commit_id =~ /^7309709/) {
365 last;
366 }
367
368 if (-f "$infodir/$commit_id") {
369 $gerrit_review = 1;
370 } else {
371 $gerrit_review = `git log $commit_id^..$commit_id | grep '\\sReviewed-on:\\s'`;
372 }
373
374 if ($gerrit_review && $commit_id && (! -f "$infodir/$commit_id") ) {
375 print "Downloading $commit_id";
376 my @info = $ssh->capture("gerrit query --format=JSON --comments --files --current-patch-set --all-approvals --submit-records --dependencies commit:$commit_id");
377 $ssh->error and die "remote ls command failed: " . $ssh->error;
378
379 my $commit_info = JSON::Util->decode($info[0]);
380 my $rowcount = $commit_info->{'rowCount'};
381 if (defined $rowcount && ($rowcount eq "0")) {
382 print " - no gerrit commit for that id.\n";
383 open( my $HANDLE, ">", "$infodir/$commit_id" ) or die "Error: could not open file '$infodir/$commit_id'\n";
384 print $HANDLE "No gerrit commit";
385 close $HANDLE;
386 next;
387 }
388 my $approvals = $commit_info->{'currentPatchSet'}{'approvals'};
389
390 for my $approval (@$approvals) {
391 if ($approval->{'type'} eq "SUBM") {
392 $submit_time = $approval->{'grantedOn'}
393 }
394 }
395 my $dt="";
396 if ($submit_time) {
397 $dt = DateTime->from_epoch(epoch => $submit_time);
398 } else {
399 print " - no submit time for that id.\n";
400 open( my $HANDLE, ">", "$infodir/$commit_id" ) or die "Error: could not open file '$infodir/$commit_id'\n";
401 print $HANDLE "No submit time";
402 close $HANDLE;
403
404 next;
405 }
406
407 open( my $HANDLE, ">", "$infodir/$commit_id" ) or die "Error: could not open file '$infodir/$commit_id'\n";
408 print $HANDLE $info[0];
409 close $HANDLE;
410
411 $dt->set_time_zone( 'Europe/Paris' );
412 print " - submit time: " . $dt->strftime('%Y/%m/%d %H:%M:%S') . "\n";
413 } elsif ($commit_id && (! -f "$infodir/$commit_id")) {
414 print "No gerrit commit for $commit_id\n";
415 open( my $HANDLE, ">", "$infodir/$commit_id" ) or die "Error: could not open file '$infodir/$commit_id'\n";
416 print $HANDLE "No gerrit commit";
417 close $HANDLE;
418 }
419 }
420 print "\n";
421}
422
423#-------------------------------------------------------------------------------
424# check_arguments parse the command line arguments
425#-------------------------------------------------------------------------------
426sub check_arguments {
427 my $show_usage = 0;
428 GetOptions(
429 'help|?' => sub { usage() },
430 'url|u=s' => \$URL_WITH_USER,
431 'skip|s' => \$SKIP_GERRIT_CHECK,
432 );
433 # strip ssh:// from url if passed in.
434 if (defined $URL_WITH_USER) {
435 $URL_WITH_USER =~ s|ssh://||;
436 }
437 if (@ARGV) {
438 ($old_version, $new_version) = @ARGV;
439 } else {
440 usage();
441 }
442}
443
444#-------------------------------------------------------------------------------
445# usage - Print the arguments for the user
446#-------------------------------------------------------------------------------
447sub usage {
448 print "gerrit_stats <options> [Old version] [New version]\n";
449 print "Old version should be a tag (4.1), a branch (origin/4.1), or a commit id\n";
450 print "New version can be 'HEAD' a branch (origin/master) a tag (4.2), or a commit id\n";
451 print " Options:\n";
452 print " u | url [url] url with username.\n";
453 print "Example: \"$0 -u Gaumless\@review.coreboot.org:29418 origin/4.1 4.2\"\n";
454 exit(0);
455}
456
4571;