Raw
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 }