What's cooking (2014/05 #03)
[git.git] / worklog
blobc2ee60ce034d495f479423e2bb0307c89d4e4633
1 #!/usr/bin/perl
3 use strict;
4 use warnings;
5 use Getopt::Long;
6 use Time::Local;
8 ################################################################
10 sub seconds_in_a_week () {
11 return 7 * 24 * 3600;
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]);
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";
31 return timelocal(0, 0, 0, $mday, $mon - 1, $year - 1900);
34 sub next_date {
35 my $datestring = shift;
36 my $time = date_to_seconds($datestring);
37 return seconds_to_date($time + 3600 * 36);
40 sub prev_date {
41 my $datestring = shift;
42 my $time = date_to_seconds($datestring);
43 return seconds_to_date($time - 3600 * 12);
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;
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);
65 my ($datestring, $dow) = seconds_to_date($time);
66 return beginning_of_reporting_week($datestring, $bow);
69 sub date_within {
70 my ($date, $bottom, $top) = @_;
71 return ($bottom le $date && $date le $top);
74 ################################################################
76 my $verbose = 0;
77 my $quiet = 0;
78 my $reporting_date;
79 my $weeks;
80 my $bow = 0;
81 my $bottom_date;
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;
94 if ($verbose && $quiet) {
95 print STDERR "Which? Verbose, or Quiet?\n";
96 exit 1;
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);
106 $bottom_date = beginning_of_reporting_week($reporting_date, $bow);
108 if (!defined $weeks || $weeks < 0) {
109 $weeks = 0;
112 for (my $i = 0; $i < $weeks; $i++) {
113 for (my $j = 0; $j < 7; $j++) {
114 ($bottom_date, undef) = prev_date($bottom_date);
117 ($bottom_date, undef) = next_date($bottom_date);
119 my $cull_old = "--since=" . date_to_seconds($bottom_date);
121 sub plural {
122 my ($number, $singular, $plural) = @_;
123 return ($number == 1) ? "$number $singular": "$number $plural";
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);
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;
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;
148 $width += $len;
149 push @result, $item;
150 } else {
151 if ($width) {
152 push @result, "\n";
153 $width = 0;
155 $width += $wl;
156 push @result, $leader;
157 $width += $len;
158 push @result, $item;
161 push @result, "\n" unless ($result[-1] eq "\n");
162 return join("", @result);
165 ################################################################
166 # Collection
168 # Map sha1 to patch information [$sha1, $author, $subject]
169 # or merge information [$sha1, undef, $branch].
170 my %patch;
172 # $dates{"YYYY-MM-DD"} exists iff something happened
173 my %dates;
175 # List of $sha1 of patches applied, grouped by date
176 my %patch_by_date;
178 # List of $sha1 of merges, grouped by date
179 my %merge_by_branch_date;
181 # List of tags, grouped by date
182 my %tag_by_date;
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'],
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;
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);
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}++;
206 close (I) or die;
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/;
222 next if (grep { $_ eq $msg } map { $_->[0] } @integrate);
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}++;
230 close (I) or die;
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}$/;
239 if (!defined $tagdate ||
240 !date_within($tagdate, $bottom_date, $reporting_date)) {
241 next;
243 $dates{$tagdate}++;
244 $tag_by_date{$tagdate} ||= [];
245 push @{$tag_by_date{$tagdate}}, $tagname;
248 ################################################################
249 # Summarize
251 my $sep = "";
252 my @dates = sort keys %dates;
254 sub day_summary {
255 my ($date, $total_names, $total_merges, $total_patches, $total_tags) = @_;
256 return if (!exists $dates{$date});
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);
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}++;
274 my $people = scalar @{[keys %names]};
275 $$total_patches += $count;
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";
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;
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);
304 $sep = "\n" if (!$quiet);
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);
311 print "$sep$range $bottom..$last_date\n";
313 if ($total_t) {
314 my $count = plural($total_t, "release", "releases");
315 print "Tagged $count.\n";
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";
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";
328 $sep = "\n";
331 sub weekly_summary {
332 my ($bottom, $total_names, $total_merges,
333 $total_patches, $total_tags) = @_;
334 my $date = $bottom;
335 my $shown = 0;
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);
343 for my $name (keys %total_n) {
344 $total_names->{$name}++;
345 $shown++;
347 for my $merge (keys %total_m) {
348 $total_merges->{$merge}++;
349 $shown++;
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);
357 return $date;
360 my %total_names;
361 my %total_merges;
362 my $total_patches = 0;
363 my $total_tags = 0;
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);
371 if ($weeks) {
372 range_summary("Between", $bottom_date, $date,
373 \%total_names, \%total_merges,
374 $total_patches, $total_tags);