lo-commit-stat: new --bugs-changelog option
[LibreOffice.git] / bin / lo-commit-stat
bloba965815288b5993235032689a491deb863e395ee
1 #!/usr/bin/perl
2 eval 'exec /usr/bin/perl -S $0 ${1+"$@"}'
3 if $running_under_some_shell;
4 #!/usr/bin/perl
6 use strict;
7 use LWP::UserAgent;
8 use utf8;
9 use File::Temp;
11 my %module_dirname = (
12 "core" => "",
13 "dictionaries" => "dictionaries",
14 "help" => "helpcontent2",
15 "translations" => "translations"
19 my %bugzillas = (
20 fdo => "https://bugs.freedesktop.org/show_bug.cgi?id=",
21 bnc => "https://bugzilla.novell.com/show_bug.cgi?id=",
22 rhbz => "https://bugzilla.redhat.com/show_bug.cgi?id=",
23 i => "https://issues.apache.org/ooo/show_bug.cgi?id=",
24 fate => "https://features.opensuse.org/",
27 sub search_bugs($$$$)
29 my ($pdata, $module, $commit_id, $line) = @_;
31 my $bug = "";
32 my $bug_orig;
33 while (defined $bug) {
35 # match fdo#123, rhz#123, i#123, #123
36 # but match only bug number with >= 4 digits
37 if ( $line =~ m/(\w+\#+\d{4,})/ ) {
38 $bug_orig = $1;
39 $bug = $1;
40 # default to issuezilla for the #123 variant
41 # but match only bug number with >= 4 digits
42 } elsif ( $line =~ m/(\#)(\d{4,})/ ) {
43 $bug_orig = $1 . $2;
44 $bug = "i#$2";
45 # match #i123#
46 } elsif ( $line =~ m/(\#i)(\d+)(\#)/ ) {
47 $bug_orig = $1 . $2 . $3;
48 $bug = "i#$2";
49 } else {
50 $bug = undef;
51 next;
54 # print " found $bug\n";
55 # remove bug number from the comment; it will be added later a standardized way
56 $bug_orig =~ s/\#/\\#/;
57 $line =~ s/[Rr]esolve[ds]:?\s*$bug_orig\s*//;
58 $line =~ s/\s*-\s*$bug_orig\s*//;
59 $line =~ s/\(?$bug_orig\)?\s*[:,-]?\s*//;
61 # bnc# is preferred over n# for novell bugs
62 $bug =~ s/^n\#/bnc#/;
63 # deb# is preferred over debian# for debian bugs
64 $bug =~ s/^debian\#/deb#/;
65 # easyhack# is sometimes used for fdo# - based easy hacks
66 $bug =~ s/^easyhack\#/fdo#/;
67 # someone mistyped fdo as fd0
68 $bug =~ s/^fd0\#/fdo#/;
69 # save the bug number
70 %{$pdata->{$module}{$commit_id}{'bugs'}} = () if (! defined %{$pdata->{$module}{$commit_id}{'bugs'}});
71 $pdata->{$module}{$commit_id}{'bugs'}{$bug} = 1;
74 return $line;
77 sub standardize_summary($)
79 my $line = shift;
81 $line =~ s/^\s*//;
82 $line =~ s/\s*$//;
84 # lower first letter if the word contains only lowercase letter
85 if ( $line =~ m/(^.[a-z]+\b)/ ) {
86 $line =~ m/(^.)/;
87 my $first_char = lc($1);
88 $line =~ s/^./$first_char/;
91 # FIXME: remove do at the end of line
92 # remove bug numbers
93 return $line;
96 sub generate_git_cherry_ids_log($$$$$)
98 my ($pdata, $repo_dir, $module, $branch_name, $git_args) = @_;
100 my $commit_ids_log;
101 my $commit_ids_log_fh;
102 $commit_ids_log_fh = File::Temp->new(TEMPLATE => 'lo-commit-stat-ids-XXXXXX',
103 DIR => '/tmp',
104 UNLINK => 0);
105 $commit_ids_log = $commit_ids_log_fh->filename;
107 print STDERR "Filtering cherry-picked commits in the git repo: $module...\n";
109 my $cmd = "cd $repo_dir; git cherry $git_args";
110 open (GIT, "$cmd 2>&1|") || die "Can't run $cmd: $!";
112 while (my $line = <GIT>) {
114 # skip cherry-picked commits
115 next if ( $line =~ m/^\-/ );
117 if ( $line =~ m/^\+ / ) {
118 $line =~ s/^\+ //;
119 print $commit_ids_log_fh $line;
123 close GIT;
124 close $commit_ids_log_fh;
126 return $commit_ids_log;
129 sub load_git_log($$$$$$$)
131 my ($pdata, $repo_dir, $module, $branch_name, $git_command, $git_cherry, $git_args) = @_;
133 my $cmd = "cd $repo_dir;";
134 my $commit_ids_log;
136 if ($git_cherry) {
137 $commit_ids_log = generate_git_cherry_ids_log($pdata, $repo_dir, $module, $branch_name, $git_args);
138 $cmd .= " cat $commit_ids_log | xargs -n 1 $git_command -1";
139 } else {
140 $cmd .= " $git_command $git_args";
143 my $commit_id;
144 my $summary;
146 print STDERR "Analyzing log from the git repo: $module...\n";
148 # FIXME: ./g pull move submodules in unnamed branches
149 # my $repo_branch_name = get_branch_name($repo_dir);
150 # if ( $branch_name ne $repo_branch_name ) {
151 # die "Error: mismatch of branches:\n" .
152 # " main repo is on the branch: $branch_name\n" .
153 # " $module repo is on the branch: $repo_branch_name\n";
156 open (GIT, "$cmd 2>&1|") || die "Can't run $cmd: $!";
157 %{$pdata->{$module}} = ();
159 while (my $line = <GIT>) {
160 chomp $line;
162 if ( $line =~ m/^commit ([0-9a-z]{20})/ ) {
163 $commit_id = "$1";
164 $summary=undef;
165 %{$pdata->{$module}{"$commit_id"}} = ();
166 next;
169 if ( $line =~ /^Author:\s*([^\<]*)\<([^\>]*)>/ ) {
170 # get rid of extra empty spaces;
171 my $name = "$1";
172 $name =~ s/\s+$//;
173 die "Error: Author already defined for the commit {$commit_id}\n" if defined ($pdata->{$module}{$commit_id}{'author'});
174 %{$pdata->{$module}{$commit_id}{'author'}} = ();
175 $pdata->{$module}{$commit_id}{'author'}{'name'} = "$name";
176 $pdata->{$module}{$commit_id}{'author'}{'email'} = "$2";
177 next;
180 if ( $line =~ /^Date:\s+/ ) {
181 # ignore date line
182 next;
185 if ( $line =~ /^\s*$/ ) {
186 # ignore empty line
187 next;
190 $line = search_bugs($pdata, $module, $commit_id, $line);
191 # FIXME: need to be implemented
192 # search_keywords($pdata, $line);
194 unless (defined $pdata->{$module}{$commit_id}{'summary'}) {
195 $summary = standardize_summary($line);
196 $pdata->{$module}{$commit_id}{'summary'} = $summary;
200 close GIT;
201 unlink $commit_ids_log if ($git_cherry);
204 sub get_repo_name($)
206 my $repo_dir = shift;
208 open (GIT_CONFIG, "$repo_dir/.git/config") ||
209 die "can't open \"$$repo_dir/.git/config\" for reading: $!\n";
211 while (my $line = <GIT_CONFIG>) {
212 chomp $line;
214 if ( $line =~ /^\s*url\s*=\s*(\S+)$/ ) {
215 my $repo_name = "$1";
216 $repo_name = s/.*\///g;
217 return "$repo_name";
220 die "Error: can't find repo name in \"$$repo_dir/.git/config\"\n";
223 sub load_data($$$$$$$)
225 my ($pdata, $top_dir, $p_module_dirname, $branch_name, $git_command, $git_cherry, $git_args) = @_;
227 foreach my $module (sort { $a cmp $b } keys %{$p_module_dirname}) {
228 load_git_log($pdata, "$top_dir/$p_module_dirname->{$module}", $module, $branch_name, $git_command, $git_cherry, $git_args);
232 sub get_branch_name($)
234 my ($top_dir) = @_;
236 my $branch;
237 my $cmd = "cd $top_dir && git branch";
239 open (GIT, "$cmd 2>&1|") || die "Can't run $cmd: $!";
241 while (my $line = <GIT>) {
242 chomp $line;
244 if ( $line =~ m/^\*\s*(\S+)/ ) {
245 $branch = "$1";
249 close GIT;
251 die "Error: did not detect git branch name in $top_dir\n" unless defined ($branch);
253 return $branch;
256 sub get_bug_list($$$)
258 my ($pdata, $pbugs, $check_bugzilla) = @_;
260 # associate bugs with their summaries and fixers
261 foreach my $module ( keys %{$pdata}) {
262 foreach my $id ( keys %{$pdata->{$module}}) {
263 foreach my $bug (keys %{$pdata->{$module}{$id}{'bugs'}}) {
264 %{$pbugs->{$bug}} = () if (! defined %{$pbugs->{$bug}});
265 my $author = $pdata->{$module}{$id}{'author'}{'name'};
266 my $summary = $pdata->{$module}{$id}{'summary'};
267 $pbugs->{$bug}{'summary'} = $summary;
268 $pbugs->{$bug}{'author'}{$author} = 1;
273 # try to replace summaries with bug names from bugzilla
274 if ($check_bugzilla) {
275 print "Getting bug titles:\n";
276 foreach my $bug ( sort { $a cmp $b } keys %{$pbugs}) {
277 $pbugs->{$bug}{'summary'} = get_bug_name($bug, $pbugs->{$bug}{'summary'});
282 sub open_log_file($$$$$$)
284 my ($log_dir, $log_prefix, $log_suffix, $top_dir, $branch_name, $wiki) = @_;
286 my $logfilename = "$log_prefix-$branch_name-$log_suffix";
287 $logfilename = "$log_dir/$logfilename" if (defined $log_dir);
288 if ($wiki) {
289 $logfilename .= ".wiki";
290 } else {
291 $logfilename .= ".log";
294 if (-f $logfilename) {
295 print "WARNING: The log file already exists: $logfilename\n";
296 print "Do you want to overwrite it? (Y/n)?\n";
297 my $answer = <STDIN>;
298 chomp $answer;
299 $answer = "y" unless ($answer);
300 die "Please, rename the file or choose another log suffix\n" if ( lc($answer) ne "y" );
303 my $log;
304 open($log, '>', $logfilename) || die "Can't open \"$logfilename\" for writing: $!\n";
306 return $log;
309 sub print_commit_summary($$$$$$)
311 my ($summary, $pmodule_title, $pbugs, $pauthors, $prefix, $log) = @_;
313 return if ( $summary eq "" );
315 # print module title if not done yet
316 if ( defined ${$pmodule_title} ) {
317 print $log "${$pmodule_title}\n";
318 ${$pmodule_title} = undef;
321 # finally print the summary line
322 my $bugs = "";
323 if ( %{$pbugs} ) {
324 $bugs = " (" . join (", ", keys %{$pbugs}) . ")";
327 my $authors = "";
328 if ( %{$pauthors} ) {
329 $authors = " [" . join (", ", keys %{$pauthors}) . "]";
332 print $log $prefix, $summary, $bugs, $authors, "\n";
335 sub print_commits($$$)
337 my ($pdata, $log, $wiki) = @_;
339 foreach my $module ( sort { $a cmp $b } keys %{$pdata}) {
340 # check if this module has any entries at all
341 my $module_title = "+ $module";
342 if ( %{$pdata->{$module}} ) {
343 my $old_summary="";
344 my %authors = ();
345 my %bugs = ();
346 foreach my $id ( sort { lc $pdata->{$module}{$a}{'summary'} cmp lc $pdata->{$module}{$b}{'summary'} } keys %{$pdata->{$module}}) {
347 my $summary = $pdata->{$module}{$id}{'summary'};
348 if ($summary ne $old_summary) {
349 print_commit_summary($old_summary, \$module_title, \%bugs, \%authors, " + ", $log);
350 $old_summary = $summary;
351 %authors = ();
352 %bugs = ();
354 # collect bug numbers
355 if (defined $pdata->{$module}{$id}{'bugs'}) {
356 foreach my $bug (keys %{$pdata->{$module}{$id}{'bugs'}}) {
357 $bugs{$bug} = 1;
360 # collect author names
361 my $author = $pdata->{$module}{$id}{'author'}{'name'};
362 $authors{$author} = 1;
364 print_commit_summary($old_summary, \$module_title, \%bugs, \%authors, " + ", $log);
369 sub get_bug_name($$)
371 my ($bug, $summary) = @_;
372 print "$bug: ";
374 $bug =~ m/(?:(\w*)\#+(\d+))/; # fdo#12345
375 my $bugzilla = $1; # fdo
376 my $bug_number = $2; # 12345
378 if ( $bugzillas{$bugzilla} ) {
379 my $url = $bugzillas{$bugzilla} . $bug_number;
380 my $ua = LWP::UserAgent->new;
381 $ua->timeout(10);
382 $ua->env_proxy;
383 my $response = $ua->get($url);
384 if ($response->is_success) {
385 my $title = $response->title;
386 if ( $title =~ s/^Bug \d+ \S+ // ) {
387 print "$title\n";
388 return $title;
389 } else {
390 print "warning: not found; using commit message (only got $title)\n";
392 } else {
393 print "\n";
395 } else {
396 print "\n";
399 return $summary;
402 sub print_bugs($$$$)
404 my ($pbugs, $log, $wiki) = @_;
406 foreach my $bug ( sort { $a cmp $b } keys %{$pbugs}) {
407 my $summary = $pbugs->{$bug}{'summary'};
409 my $authors = "";
410 if ( %{$pbugs->{$bug}{'author'}} ) {
411 $authors = " [" . join (", ", keys %{$pbugs->{$bug}{'author'}}) . "]";
414 $bug =~ s/(.*)\#(.*)/* {{$1|$2}}/ if ($wiki);
415 print $log $bug, " ", $summary, $authors, "\n";
419 sub print_bugs_changelog($$$$)
421 my ($pbugs, $log, $wiki) = @_;
423 foreach my $bug ( sort { $a cmp $b } keys %{$pbugs}) {
424 my $summary = $pbugs->{$bug}{'summary'};
426 my $authors = "";
427 if ( %{$pbugs->{$bug}{'author'}} ) {
428 $authors = " [" . join (", ", keys %{$pbugs->{$bug}{'author'}}) . "]";
431 print $log " + $summary ($bug)$authors\n";
435 sub print_bugnumbers($$$$)
437 my ($pbugs, $log, $wiki) = @_;
439 print $log join ("\n", sort { $a cmp $b } keys %{$pbugs}), "\n";
442 sub generate_log($$$$$$$$)
444 my ($pused_data, $print_func, $log_dir, $log_prefix, $log_suffix, $top_dir, $branch_name, $wiki) = @_;
446 my $log = open_log_file($log_dir, $log_prefix, $log_suffix, $top_dir, $branch_name, $wiki);
447 & {$print_func} ($pused_data, $log, $wiki);
448 close $log;
451 ########################################################################
452 # help
454 sub usage()
456 print "This script generates LO git commit summary\n\n" .
458 "Usage: lo-commit-stat [--help] [--no-submodules] [--module=<module>] --log-dir=<dir> --log-suffix=<string> topdir [git_arg...]\n\n" .
460 "Options:\n" .
461 " --help print this help\n" .
462 " --no-submodule read changes just from the main repository, ignore submodules\n" .
463 " --module=<module> summarize just changes from the given module, use \"core\"\n" .
464 " for the main module\n" .
465 " --log-dir=<dir> directory where to put the generated log\n" .
466 " --log-suffix=<string> suffix of the log file name; the result will be\n" .
467 " commit-log-<branch>-<log-name-suffix>.log; the branch name\n" .
468 " is detected automatically\n" .
469 " --commits generete log with all commits (default)\n" .
470 " --bugs generate log with bugzilla entries\n" .
471 " --bugs-changelog generate log with bugzilla entries, use changelog style\n" .
472 " --bugs-wiki generate log with bugzilla entries, use wiki markup\n" .
473 " --bugs-numbers generate log with bugzilla numbers\n" .
474 " --rev-list use \"git rev-list\" instead of \"git log\"; useful to check\n" .
475 " differences between branches\n" .
476 " --cherry use \"git cherry\" instead of \"git log\"; detects cherry-picked\n" .
477 " commits between branches\n" .
478 " topdir directory with the libreoffice/core clone\n" .
479 " git_arg extra parameters passed to the git command to define\n" .
480 " the area of interest; The default command is \"git log\" and\n" .
481 " parameters might be, for example, --after=\"2010-09-27\" or\n" .
482 " TAG..HEAD; with the option --rev-list, useful might be, for\n" .
483 " example origin/master ^origin/libreoffice-3-3; with the option\n" .
484 " --rev-list, useful might be, for example libreoffice-3.6.3.2\n" .
485 " libreoffice-3.6.4.1\n";
489 #######################################################################
490 #######################################################################
491 # MAIN
492 #######################################################################
493 #######################################################################
496 my $module;
497 my %generate_log = ();
498 my $top_dir;
499 my $log_dir;
500 my $log_suffix;
501 my $log;
502 my $list_bugs = 0;
503 my $check_bugzilla = 0;
504 my $branch_name;
505 my $git_command = "git log";
506 my $git_cherry;
507 my $git_args = "";
508 my $branch_name;
509 my %data;
510 my %bugs = ();
513 foreach my $arg (@ARGV) {
514 if ($arg eq '--help') {
515 usage();
516 exit;
517 } elsif ($arg eq '--no-submodule') {
518 $module = "core";
519 } elsif ($arg =~ m/--module=(.*)/) {
520 $module = $1;
521 } elsif ($arg =~ m/--log-suffix=(.*)/) {
522 $log_suffix = "$1";
523 } elsif ($arg =~ m/--log-dir=(.*)/) {
524 $log_dir = "$1";
525 } elsif ($arg eq '--commits') {
526 $generate_log{"commits"} = 1;
527 } elsif ($arg eq '--bugs') {
528 $generate_log{"bugs"} = 1;
529 $check_bugzilla = 1;
530 $list_bugs = 1;
531 } elsif ($arg eq '--bugs-changelog') {
532 $generate_log{"bugs-changelog"} = 1;
533 $check_bugzilla = 1;
534 $list_bugs = 1;
535 } elsif ($arg eq '--bugs-wiki' || $arg eq '--wikibugs') {
536 $generate_log{"bugs-wiki"} = 1;
537 $check_bugzilla = 1;
538 $list_bugs = 1;
539 } elsif ($arg eq '--bugs-numbers' || $arg eq '--bug-numbers') {
540 $generate_log{"bugs-numbers"} = 1;
541 $list_bugs = 1;
542 } elsif ($arg eq '--rev-list') {
543 $git_command = "git rev-list --pretty=medium"
544 } elsif ($arg eq '--cherry') {
545 $git_command = "git log";
546 $git_cherry = 1;
547 } else {
548 if (! defined $top_dir) {
549 $top_dir=$arg;
550 } else {
551 $git_args .= " $arg";
556 # default log
557 if (%generate_log == 0) {
558 $generate_log{"commits"} = 1;
561 # we want only one module
562 if ($module) {
563 my $name = $module_dirname{$module};
564 %module_dirname = ();
565 $module_dirname{$module} = $name;
568 (defined $top_dir) || die "Error: top directory is not defined\n";
569 (-d "$top_dir") || die "Error: not a directory: $top_dir\n";
570 (-f "$top_dir/.git/config") || die "Error: can't find $top_dir/.git/config\n";
572 (!defined $log_dir) || (-d $log_dir) || die "Error: directory does no exist: $log_dir\n";
574 (defined $log_suffix) || die "Error: define log suffix using --log-suffix=<string>\n";
576 $branch_name = get_branch_name($top_dir);
578 load_data(\%data, $top_dir, \%module_dirname, $branch_name, $git_command, $git_cherry, $git_args);
579 get_bug_list(\%data, \%bugs, $check_bugzilla) if ($list_bugs);
581 generate_log(\%data, \&print_commits, $log_dir, "commits", $log_suffix, $top_dir, $branch_name, 0) if (defined $generate_log{"commits"});
582 generate_log(\%bugs, \&print_bugs, $log_dir, "bugs", $log_suffix, $top_dir, $branch_name, 0) if (defined $generate_log{"bugs"});
583 generate_log(\%bugs, \&print_bugs, $log_dir, "bugs", $log_suffix, $top_dir, $branch_name, 1) if (defined $generate_log{"bugs-wiki"});
584 generate_log(\%bugs, \&print_bugs_changelog, $log_dir, "bugs-changelog", $log_suffix, $top_dir, $branch_name, 0) if (defined $generate_log{"bugs-changelog"});
585 generate_log(\%bugs, \&print_bugnumbers, $log_dir, "bug-numbers", $log_suffix, $top_dir, $branch_name, 0) if (defined $generate_log{"bugs-numbers"});