What's cooking (2020/11 #01)
[git] / worklog
1 #!/usr/bin/perl
2
3 use strict;
4 use warnings;
5 use Getopt::Long;
6 use Time::Local;
7
8 ################################################################
9
10 sub seconds_in_a_week () {
11         return 7 * 24 * 3600;
12 }
13
14 # Convert seconds from epoch to datestring and dow
15 sub seconds_to_date {
16         my $time = shift;
17         my @time = localtime($time);
18         return (sprintf("%04d-%02d-%02d",
19                         $time[5]+1900, $time[4]+1, $time[3]),
20                 $time[6]);
21 }
22
23 sub date_to_seconds {
24         my $datestring = shift;
25         my ($year, $mon, $mday) = ($datestring =~ /^(\d{4})-(\d{2})-(\d{2})$/);
26         unless (defined $year && defined $mon && defined $mday &&
27                 1 <= $mon && $mon <= 12 &&
28                 1 <= $mday && $mday <= 31) {
29                 die "Bad datestring specification: $datestring";
30         }
31         return timelocal(0, 0, 0, $mday, $mon - 1, $year - 1900);
32 }
33
34 sub next_date {
35         my $datestring = shift;
36         my $time = date_to_seconds($datestring);
37         return seconds_to_date($time + 3600 * 36);
38 }
39
40 sub prev_date {
41         my $datestring = shift;
42         my $time = date_to_seconds($datestring);
43         return seconds_to_date($time - 3600 * 12);
44 }
45
46 sub beginning_of_reporting_week {
47         my ($datestring, $bow) = @_;
48         my ($date, $dow) = seconds_to_date(date_to_seconds($datestring));
49         do {
50                 ($date, $dow) = prev_date($date);
51         } while ($dow != $bow);
52         return $date;
53 }
54
55 sub bow {
56         my ($week, $bow) = @_;
57         my $time;
58         if (!defined $week) {
59                 $time = time;
60         } elsif ($week =~ /^\d+$/) {
61                 $time = time - seconds_in_a_week * $week;
62         } else {
63                 $time = date_to_seconds($week);
64         }
65         my ($datestring, $dow) = seconds_to_date($time);
66         return beginning_of_reporting_week($datestring, $bow);
67 }
68
69 sub date_within {
70         my ($date, $bottom, $top) = @_;
71         return ($bottom le $date && $date le $top);
72 }
73
74 ################################################################
75
76 my $verbose = 0;
77 my $quiet = 0;
78 my $reporting_date;
79 my $weeks;
80 my $bow = 0;
81 my $bottom_date;
82
83 if (!GetOptions(
84              "verbose!" => \$verbose,
85              "quiet!" => \$quiet,
86              "date=s" => \$reporting_date,
87              "weeks=i" => \$weeks,
88              "bow=i" => \$bow,
89     )) {
90         print STDERR "$0 [-v|-q] [-d date] [-w weeks] [-b bow]\n";
91         exit 1;
92 }
93
94 if ($verbose && $quiet) {
95         print STDERR "Which? Verbose, or Quiet?\n";
96         exit 1;
97 }
98
99 if (!defined $reporting_date) {
100         ($reporting_date, my $dow) = seconds_to_date(time);
101         while ($dow != $bow) {
102                 ($reporting_date, $dow) = next_date($reporting_date);
103         }
104 }
105
106 $bottom_date = beginning_of_reporting_week($reporting_date, $bow);
107
108 if (!defined $weeks || $weeks < 0) {
109         $weeks = 0;
110 }
111
112 for (my $i = 0; $i < $weeks; $i++) {
113         for (my $j = 0; $j < 7; $j++) {
114                 ($bottom_date, undef) = prev_date($bottom_date);
115         }
116 }
117 ($bottom_date, undef) = next_date($bottom_date);
118
119 my $cull_old = "--since=" . date_to_seconds($bottom_date);
120
121 sub plural {
122         my ($number, $singular, $plural) = @_;
123         return ($number == 1) ? "$number $singular": "$number $plural";
124 }
125
126 sub fmt_join {
127         my ($leader, $limit, $joiner, @ary) = @_;
128         my @result = ();
129         my $width = 0;
130         my $wj = length($joiner);
131         my $wl = length($leader);
132
133         for my $item (@ary) {
134                 my $need_joiner;
135                 if ($width == 0) {
136                         $width += $wl;
137                         push @result, $leader;
138                         $need_joiner = 0;
139                 } else {
140                         $need_joiner = 1;
141                 }
142                 my $len = length($item);
143                 if (($need_joiner ? $wj : 0) + $len + $width < $limit) {
144                         if ($need_joiner) {
145                                 $width += $wj;
146                                 push @result, $joiner;
147                         }
148                         $width += $len;
149                         push @result, $item;
150                 } else {
151                         if ($width) {
152                                 push @result, "\n";
153                                 $width = 0;
154                         }
155                         $width += $wl;
156                         push @result, $leader;
157                         $width += $len;
158                         push @result, $item;
159                 }
160         }
161         push @result, "\n" unless ($result[-1] eq "\n");
162         return join("", @result);
163 }
164
165 ################################################################
166 # Collection
167
168 # Map sha1 to patch information [$sha1, $author, $subject]
169 # or merge information [$sha1, undef, $branch].
170 my %patch;
171
172 # $dates{"YYYY-MM-DD"} exists iff something happened
173 my %dates;
174
175 # List of $sha1 of patches applied, grouped by date
176 my %patch_by_date;
177
178 # List of $sha1 of merges, grouped by date
179 my %merge_by_branch_date;
180
181 # List of tags, grouped by date
182 my %tag_by_date;
183
184 # List of integration branches.
185 my @integrate = (['master', ', to include in the next release'],
186                  ['next', ' for public testing'],
187                  ['maint', ', to include in the maintenance release'],
188 );
189
190 # Collect individial patch application
191 open I, "-|", ("git", "log",
192                "--pretty=%ci %H %an <%ae>\001%s",
193                $cull_old, "--glob=refs/heads",
194                "--no-merges") or die;
195
196 while (<I>) {
197         my ($date, $sha1, $rest) = /^([-0-9]+) [:0-9]+ [-+][0-9]{4} ([0-9a-f]+) (.*)$/;
198         next unless date_within($date, $bottom_date, $reporting_date);
199
200         $patch_by_date{$date} ||= [];
201         push @{$patch_by_date{$date}}, $sha1;
202         my ($name, $subject) = split(/\001/, $rest, 2);
203         $patch{$sha1} = [$sha1, $name, $subject];
204         $dates{$date}++;
205 }
206 close (I) or die;
207
208 for my $branch (map { $_->[0] } @integrate) {
209         open I, "-|", ("git", "log", "--pretty=%ci %H %s",
210                        $cull_old,
211                        "--first-parent",
212                        "--merges",
213                        $branch) or die;
214         while (<I>) {
215                 my ($date, $sha1, $rest) = /^([-0-9]+) [:0-9]+ [-+][0-9]{4} ([0-9a-f]+) (.*)$/;
216                 next unless date_within($date, $bottom_date, $reporting_date);
217                 my $msg = $rest;
218                 $msg =~ s/^Merge branch //;
219                 $msg =~ s/ into \Q$branch\E$//;
220                 $msg =~ s/^'(.*)'$/$1/;
221
222                 next if (grep { $_ eq $msg } map { $_->[0] } @integrate);
223
224                 $merge_by_branch_date{$branch} ||= {};
225                 $merge_by_branch_date{$branch}{$date} ||= [];
226                 push @{$merge_by_branch_date{$branch}{$date}}, $sha1;
227                 $patch{$sha1} = [$sha1, undef, $msg];
228                 $dates{$date}++;
229         }
230         close (I) or die;
231 }
232
233 open I, "-|", ("git", "for-each-ref",
234                "--format=%(refname:short) %(taggerdate:iso)",
235                "refs/tags") or die;
236 while (<I>) {
237         my ($tagname, $tagdate) = /^(\S+) ([-0-9]+) [:0-9]+ [-+][0-9]{4}$/;
238         
239         if (!defined $tagdate || 
240             !date_within($tagdate, $bottom_date, $reporting_date)) {
241                 next;
242         }
243         $dates{$tagdate}++;
244         $tag_by_date{$tagdate} ||= [];
245         push @{$tag_by_date{$tagdate}}, $tagname;
246 }
247
248 ################################################################
249 # Summarize
250
251 my $sep = ""; 
252 my @dates = sort keys %dates;
253
254 sub day_summary {
255         my ($date, $total_names, $total_merges, $total_patches, $total_tags) = @_;
256         return if (!exists $dates{$date});
257
258         print "$sep$date\n" if (!$quiet);
259         if (exists $tag_by_date{$date}) {
260                 for my $tagname (@{$tag_by_date{$date}}) {
261                         $$total_tags++;
262                         print "Tagged $tagname.\n" if (!$quiet);
263                 }
264         }
265
266         if (exists $patch_by_date{$date}) {
267                 my $count = scalar @{$patch_by_date{$date}};
268                 my %names = ();
269                 for my $patch (map { $patch{$_} } (@{$patch_by_date{$date}})) {
270                         my $name = $patch->[1];
271                         $names{$name}++;
272                         $total_names->{$name}++;
273                 }
274                 my $people = scalar @{[keys %names]};
275                 $$total_patches += $count;
276
277                 $count = plural($count, "patch", "patches");
278                 $people = plural($people, "person", "people");
279                 print "Queued $count from $people.\n" if (!$quiet);
280                 if ($verbose) {
281                         for my $patch (map { $patch{$_} } @{$patch_by_date{$date}}) {
282                                 print "  $patch->[2]\n";
283                         }
284                 }
285         }
286
287         for my $branch_data (@integrate) {
288                 my ($branch, $purpose) = @{$branch_data};
289                 next unless (exists $merge_by_branch_date{$branch}{$date});
290                 my $merges = $merge_by_branch_date{$branch}{$date};
291                 my $count = scalar @$merges;
292                 next unless $count;
293
294                 $total_merges->{$branch} ||= 0;
295                 $total_merges->{$branch} += $count;
296                 $count = plural($count, "topic", "topics");
297                 print "Merged $count to '$branch' branch$purpose.\n" if (!$quiet);
298                 if ($verbose) {
299                         my @pieces = map { $patch{$_}->[2] . "," } @$merges;
300                         $pieces[-1] =~ s/,$/./;
301                         print fmt_join("  ", 72, " ", @pieces);
302                 }
303         }
304         $sep = "\n" if (!$quiet);
305 }
306
307 sub range_summary {
308         my ($range, $bottom, $date, $total_n, $total_m, $total_p, $total_t) = @_;
309         (my $last_date, undef) = prev_date($date);
310
311         print "$sep$range $bottom..$last_date\n";
312
313         if ($total_t) {
314                 my $count = plural($total_t, "release", "releases");
315                 print "Tagged $count.\n";
316         }
317         if ($total_p) {
318                 my $people = plural(scalar @{[keys %{$total_n}]}, "person", "people");
319                 my$count = plural($total_p, "patch", "patches");
320                 print "Queued $count from $people.\n";
321         }
322         for my $branch_data (@integrate) {
323                 my ($branch, $purpose) = @{$branch_data};
324                 next unless $total_m->{$branch};
325                 my $count = plural($total_m->{$branch}, "merge", "merges");
326                 print "Made $count to '$branch' branch$purpose.\n";
327         }
328         $sep = "\n";
329 }
330
331 sub weekly_summary {
332         my ($bottom, $total_names, $total_merges,
333             $total_patches, $total_tags) = @_;
334         my $date = $bottom;
335         my $shown = 0;
336
337         my ($total_p, $total_t, %total_n, %total_m) = (0, 0);
338         for (my $i = 0; $i < 7; $i++) {
339                 day_summary($date, \%total_n, \%total_m,
340                             \$total_p, \$total_t);
341                 ($date, undef) = next_date($date);
342         }
343         for my $name (keys %total_n) {
344                 $total_names->{$name}++;
345                 $shown++;
346         }
347         for my $merge (keys %total_m) {
348                 $total_merges->{$merge}++;
349                 $shown++;
350         }
351         $$total_patches += $total_p;
352         $$total_tags += $total_t;
353         if ($shown) {
354                 range_summary("Week of", $bottom, $date,
355                               \%total_n, \%total_m, $total_p, $total_t);
356         }
357         return $date;
358 }
359
360 my %total_names;
361 my %total_merges;
362 my $total_patches = 0;
363 my $total_tags = 0;
364
365 my $date;
366 for ($date = $bottom_date; $date le $reporting_date; ) {
367         $date = weekly_summary($date, \%total_names, \%total_merges,
368                                \$total_patches, \$total_tags);
369 }
370
371 if ($weeks) {
372         range_summary("Between", $bottom_date, $date,
373                       \%total_names, \%total_merges,
374                       $total_patches, $total_tags);
375 }