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