What's cooking (2010/08 #05)
[alt-git.git] / cook
blobefc387cb4413a7a6fa01bfbb34983153b521260f
1 #!/usr/bin/perl -w
2 # Maintain "what's cooking" messages
4 use strict;
6 sub phrase_these {
7 my (@u) = sort @_;
8 my @d = ();
9 for (my $i = 0; $i < @u; $i++) {
10 push @d, $u[$i];
11 if ($i == @u - 2) {
12 push @d, " and ";
13 } elsif ($i < @u - 2) {
14 push @d, ", ";
17 return join('', @d);
20 sub describe_relation {
21 my ($topic_info) = @_;
22 my @desc;
24 if (exists $topic_info->{'used'}) {
25 push @desc, ("is used by " .
26 phrase_these(@{$topic_info->{'used'}}));
29 if (exists $topic_info->{'uses'}) {
30 push @desc, ("uses " .
31 phrase_these(@{$topic_info->{'uses'}}));
34 if (exists $topic_info->{'shares'}) {
35 push @desc, ("shares commits with " .
36 phrase_these(@{$topic_info->{'shares'}}));
39 if (!@desc) {
40 return "";
43 return "(this branch " . join("; ", @desc) . ".)";
46 sub forks_from {
47 my ($topic, $fork, $forkee, @overlap) = @_;
48 my %ovl = map { $_ => 1 } (@overlap, @{$topic->{$forkee}{'log'}});
50 push @{$topic->{$fork}{'uses'}}, $forkee;
51 push @{$topic->{$forkee}{'used'}}, $fork;
52 @{$topic->{$fork}{'log'}} = (grep { !exists $ovl{$_} }
53 @{$topic->{$fork}{'log'}});
56 sub topic_relation {
57 my ($topic, $one, $two) = @_;
59 my $fh;
60 open($fh, '-|',
61 qw(git log --abbrev=7), "--format=%m %h",
62 "$one...$two", "^master")
63 or die "$!: open log --left-right";
64 my (@left, @right);
65 while (<$fh>) {
66 my ($sign, $sha1) = /^(.) (.*)/;
67 if ($sign eq '<') {
68 push @left, $sha1;
69 } elsif ($sign eq '>') {
70 push @right, $sha1;
73 close($fh) or die "$!: close log --left-right";
75 if (!@left) {
76 if (@right) {
77 forks_from($topic, $two, $one);
79 } elsif (!@right) {
80 forks_from($topic, $one, $two);
81 } else {
82 if (@left < @right) {
83 forks_from($topic, $two, $one, @left);
84 } elsif (@right < @left) {
85 forks_from($topic, $one, $two, @right);
86 } else {
87 push @{$topic->{$one}{'shares'}}, $two;
88 push @{$topic->{$two}{'shares'}}, $one;
93 =head1
94 Inspect the current set of topics
96 Returns a hash:
98 $topic = {
99 $branchname => {
100 'tipdate' => date of the tip commit,
101 'desc' => description string,
102 'log' => [ $commit,... ],
106 =cut
108 sub get_commit {
109 my (@base) = qw(master next pu);
110 my $fh;
111 open($fh, '-|',
112 qw(git for-each-ref),
113 "--format=%(refname:short) %(authordate:iso8601)",
114 "refs/heads/??/*")
115 or die "$!: open for-each-ref";
116 my @topic;
117 my %topic;
119 while (<$fh>) {
120 chomp;
121 my ($branch, $date) = /^(\S+) (.*)$/;
122 push @topic, $branch;
123 $date =~ s/ .*//;
124 $topic{$branch} = +{
125 log => [],
126 tipdate => $date,
129 close($fh) or die "$!: close for-each-ref";
131 my %base = map { $_ => undef } @base;
132 my %commit;
133 my $show_branch_batch = 20;
135 while (@topic) {
136 my @t = (@base, splice(@topic, 0, $show_branch_batch));
137 my $header_delim = '-' x scalar(@t);
138 my $contain_pat = '.' x scalar(@t);
139 open($fh, '-|', qw(git show-branch --sparse --sha1-name),
140 map { "refs/heads/$_" } @t)
141 or die "$!: open show-branch";
142 while (<$fh>) {
143 chomp;
144 if ($header_delim) {
145 if (/^$header_delim$/) {
146 $header_delim = undef;
148 next;
150 my ($contain, $sha1, $log) =
151 ($_ =~ /^($contain_pat) \[([0-9a-f]+)\] (.*)$/);
153 for (my $i = 0; $i < @t; $i++) {
154 my $branch = $t[$i];
155 my $sign = substr($contain, $i, 1);
156 next if ($sign eq ' ');
157 next if (substr($contain, 0, 1) ne ' ');
159 if (!exists $commit{$sha1}) {
160 $commit{$sha1} = +{
161 branch => {},
162 log => $log,
165 my $co = $commit{$sha1};
166 $co->{'branch'}{$branch} = 1;
167 next if (exists $base{$branch});
168 push @{$topic{$branch}{'log'}}, $sha1;
171 close($fh) or die "$!: close show-branch";
174 my %shared;
175 for my $sha1 (keys %commit) {
176 my $sign;
177 my $co = $commit{$sha1};
178 if (exists $co->{'branch'}{'next'}) {
179 $sign = '+';
180 } elsif (exists $co->{'branch'}{'pu'}) {
181 $sign = '-';
182 } else {
183 $sign = '.';
185 $co->{'log'} = $sign . ' ' . $co->{'log'};
186 my @t = (sort grep { !exists $base{$_} }
187 keys %{$co->{'branch'}});
188 next if (@t < 2);
189 my $t = "@t";
190 $shared{$t} = 1;
193 for my $combo (keys %shared) {
194 my @combo = split(' ', $combo);
195 for (my $i = 0; $i < @combo - 1; $i++) {
196 for (my $j = $i + 1; $j < @combo; $j++) {
197 topic_relation(\%topic, $combo[$i], $combo[$j]);
202 open($fh, '-|',
203 qw(git log --first-parent --abbrev=7),
204 "--format=%ci %h %p :%s", "master..next")
205 or die "$!: open log master..next";
206 while (<$fh>) {
207 my ($date, $commit, $parent, $tips);
208 unless (($date, $commit, $parent, $tips) =
209 /^([-0-9]+) ..:..:.. .\d{4} (\S+) (\S+) ([^:]*):/) {
210 die "Oops: $_";
212 for my $tip (split(' ', $tips)) {
213 my $co = $commit{$tip};
214 $co->{'merged'} = " (merged to 'next' on $date at $commit)";
217 close($fh) or die "$!: close log master..next";
219 for my $branch (keys %topic) {
220 my @log = ();
221 my $n = scalar(@{$topic{$branch}{'log'}});
222 if (!$n) {
223 delete $topic{$branch};
224 next;
225 } elsif ($n == 1) {
226 $n = "1 commit";
227 } else {
228 $n = "$n commits";
230 my $d = $topic{$branch}{'tipdate'};
231 my $head = "* $branch ($d) $n\n";
232 my @desc;
233 for (@{$topic{$branch}{'log'}}) {
234 my $co = $commit{$_};
235 if (exists $co->{'merged'}) {
236 push @desc, $co->{'merged'};
238 push @desc, $commit{$_}->{'log'};
240 my $list = join("\n", map { " " . $_ } @desc);
241 my $relation = describe_relation($topic{$branch});
242 $topic{$branch}{'desc'} = $head . $list;
243 if ($relation) {
244 $topic{$branch}{'desc'} .= "\n $relation";
248 return \%topic;
251 sub blurb_text {
252 my ($mon, $year, $issue, $dow, $date,
253 $master_at, $next_at, $text) = @_;
255 my $now_string = localtime;
256 my ($current_dow, $current_mon, $current_date, $current_year) =
257 ($now_string =~ /^(\w+) (\w+) (\d+) [\d:]+ (\d+)$/);
259 $mon ||= $current_mon;
260 $year ||= $current_year;
261 $issue ||= "01";
262 $dow ||= $current_dow;
263 $date ||= $current_date;
264 $master_at ||= '0' x 40;
265 $next_at ||= '0' x 40;
266 $text ||= <<'EOF';
267 Here are the topics that have been cooking. Commits prefixed with '-' are
268 only in 'pu' while commits prefixed with '+' are in 'next'. The ones
269 marked with '.' do not appear in any of the integration branches, but I am
270 still holding onto them.
273 $text = <<EOF;
274 To: git\@vger.kernel.org
275 Subject: What's cooking in git.git ($mon $year, #$issue; $dow, $date)
276 X-master-at: $master_at
277 X-next-at: $next_at
279 What's cooking in git.git ($mon $year, #$issue; $dow, $date)
280 --------------------------------------------------
282 $text
284 $text =~ s/\n+\Z/\n/;
285 return $text;
288 my $blurb_match = <<'EOF';
289 To: .*
290 Subject: What's cooking in \S+ \((\w+) (\d+), #(\d+); (\w+), (\d+)\)
291 X-master-at: ([0-9a-f]{40})
292 X-next-at: ([0-9a-f]{40})
294 What's cooking in \S+ \(\1 \2, #\3; \4, \5\)
295 -{30,}
299 my $blurb = "b..l..u..r..b";
300 sub read_previous {
301 my ($fn) = @_;
302 my $fh;
303 my $section = undef;
304 my $serial = 1;
305 my $branch = $blurb;
306 my $last_empty = undef;
307 my (@section, %section, @branch, %branch, %description, @leader);
308 my $in_unedited_olde = 0;
310 if (!-r $fn) {
311 return +{
312 'section_list' => [],
313 'section_data' => {},
314 'topic_description' => {
315 $blurb => {
316 desc => undef,
317 text => blurb_text(),
323 open ($fh, '<', $fn) or die "$!: open $fn";
324 while (<$fh>) {
325 chomp;
326 if ($in_unedited_olde) {
327 if (/^>>$/) {
328 $in_unedited_olde = 0;
329 $_ = " | $_";
331 } elsif (/^<<$/) {
332 $in_unedited_olde = 1;
335 if ($in_unedited_olde) {
336 $_ = " | $_";
339 if (defined $section && /^-{20,}$/) {
340 $_ = "";
342 if (/^$/) {
343 $last_empty = 1;
344 next;
346 if (/^\[(.*)\]\s*$/) {
347 $section = $1;
348 $branch = undef;
349 if (!exists $section{$section}) {
350 push @section, $section;
351 $section{$section} = [];
353 next;
355 if (defined $section && /^\* (\S+) /) {
356 $branch = $1;
357 $last_empty = 0;
358 if (!exists $branch{$branch}) {
359 push @branch, [$branch, $section];
360 $branch{$branch} = 1;
362 push @{$section{$section}}, $branch;
364 if (defined $branch) {
365 my $was_last_empty = $last_empty;
366 $last_empty = 0;
367 if (!exists $description{$branch}) {
368 $description{$branch} = [];
370 if ($was_last_empty) {
371 push @{$description{$branch}}, "";
373 push @{$description{$branch}}, $_;
376 close($fh);
378 for my $branch (keys %description) {
379 my $ary = $description{$branch};
380 if ($branch eq $blurb) {
381 while (@{$ary} && $ary->[-1] =~ /^-{30,}$/) {
382 pop @{$ary};
384 $description{$branch} = +{
385 desc => undef,
386 text => join("\n", @{$ary}),
388 } else {
389 my @desc = ();
390 while (@{$ary}) {
391 my $elem = shift @{$ary};
392 last if ($elem eq '');
393 push @desc, $elem;
395 $description{$branch} = +{
396 desc => join("\n", @desc),
397 text => join("\n", @{$ary}),
402 return +{
403 section_list => \@section,
404 section_data => \%section,
405 topic_description => \%description,
409 sub write_cooking {
410 my ($fn, $cooking) = @_;
411 my $fh;
413 open($fh, '>', $fn) or die "$!: open $fn";
414 print $fh $cooking->{'topic_description'}{$blurb}{'text'};
416 for my $section_name (@{$cooking->{'section_list'}}) {
417 my $topic_list = $cooking->{'section_data'}{$section_name};
418 next if (!@{$topic_list});
420 print $fh "\n";
421 print $fh '-' x 50, "\n";
422 print $fh "[$section_name]\n";
423 for my $topic (@{$topic_list}) {
424 my $d = $cooking->{'topic_description'}{$topic};
426 print $fh "\n", $d->{'desc'}, "\n";
427 if ($d->{'text'}) {
428 print $fh "\n", $d->{'text'}, "\n";
432 close($fh);
435 my $graduated = 'Graduated to "master"';
436 my $new_topics = 'New Topics';
437 my $old_new_topics = 'Old New Topics';
439 sub update_issue {
440 my ($cooking) = @_;
441 my ($fh, $master_at, $next_at, $incremental);
443 open($fh, '-|',
444 qw(git for-each-ref),
445 "--format=%(refname:short) %(objectname)",
446 "refs/heads/master",
447 "refs/heads/next") or die "$!: open for-each-ref";
448 while (<$fh>) {
449 my ($branch, $at) = /^(\S+) (\S+)$/;
450 if ($branch eq 'master') { $master_at = $at; }
451 if ($branch eq 'next') { $next_at = $at; }
453 close($fh) or die "$!: close for-each-ref";
455 $incremental = ((-r "Meta/whats-cooking.txt") &&
456 system("cd Meta && " .
457 "git diff --quiet --no-ext-diff HEAD -- " .
458 "whats-cooking.txt"));
460 my $now_string = localtime;
461 my ($current_dow, $current_mon, $current_date, $current_year) =
462 ($now_string =~ /^(\w+) (\w+) +(\d+) [\d:]+ (\d+)$/);
464 my $btext = $cooking->{'topic_description'}{$blurb}{'text'};
465 if ($btext !~ s/\A$blurb_match//) {
466 die "match pattern broken?";
468 my ($mon, $year, $issue, $dow, $date) = ($1, $2, $3, $4, $5);
470 if ($current_mon ne $mon || $current_year ne $year) {
471 $issue = "01";
472 } elsif (!$incremental) {
473 $issue =~ s/^0*//;
474 $issue = sprintf "%02d", ($issue + 1);
476 $mon = $current_mon;
477 $year = $current_year;
478 $dow = $current_dow;
479 $date = $current_date;
481 $cooking->{'topic_description'}{$blurb}{'text'} =
482 blurb_text($mon, $year, $issue, $dow, $date,
483 $master_at, $next_at, $btext);
485 if (!$incremental) {
486 my $sd = $cooking->{'section_data'};
487 my $sl = $cooking->{'section_list'};
488 # Rename "New" to "Old New" and insert "New".
489 # Move "New" to "Old New"
490 my $i;
491 my $doneso;
492 for ($i = 0; $i < @{$sl}; $i++) {
493 if ($sl->[$i] eq $new_topics) {
494 $sl->[$i] = $old_new_topics;
495 unshift @{$sl}, $new_topics;
496 $doneso = 1;
497 last;
500 if ($doneso) {
501 $sd->{$old_new_topics} = $sd->{$new_topics};
503 $sd->{$new_topics} = [];
506 return $incremental;
509 sub merge_cooking {
510 my ($cooking, $current) = @_;
511 my $td = $cooking->{'topic_description'};
512 my $sd = $cooking->{'section_data'};
513 my $sl = $cooking->{'section_list'};
514 my (@new_topic, @gone_topic);
516 # Make sure "New Topics" and "Graduated" exists
517 if (!exists $sd->{$new_topics}) {
518 $sd->{$new_topics} = [];
519 unshift @{$sl}, $new_topics;
522 if (!exists $sd->{$graduated}) {
523 $sd->{$graduated} = [];
524 unshift @{$sl}, $graduated;
527 my $incremental = update_issue($cooking);
529 for my $topic (sort keys %{$current}) {
530 if (!exists $td->{$topic}) {
531 push @new_topic, $topic;
532 next;
534 my $n = $current->{$topic}{'desc'};
535 my $o = $td->{$topic}{'desc'};
536 if ($n ne $o) {
537 $td->{$topic}{'desc'} = $n . "\n<<\n" . $o ."\n>>";
541 for my $topic (sort keys %{$td}) {
542 next if ($topic eq $blurb);
543 next if (!$incremental &&
544 grep { $topic eq $_ } @{$sd->{$graduated}});
545 if (!exists $current->{$topic}) {
546 push @gone_topic, $topic;
550 for (@new_topic) {
551 push @{$sd->{$new_topics}}, $_;
552 $td->{$_}{'desc'} = $current->{$_}{'desc'};
555 if (!$incremental) {
556 $sd->{$graduated} = [];
559 if (@gone_topic) {
560 for my $topic (@gone_topic) {
561 for my $section (@{$sl}) {
562 my $pre = scalar(@{$sd->{$section}});
563 @{$sd->{$section}} = (grep { $_ ne $topic }
564 @{$sd->{$section}});
565 my $post = scalar(@{$sd->{$section}});
566 next if ($pre == $post);
569 for (@gone_topic) {
570 push @{$sd->{$graduated}}, $_;
575 ################################################################
576 # Main
578 my $topic = get_commit();
579 my $cooking = read_previous('Meta/whats-cooking.txt');
580 merge_cooking($cooking, $topic);
581 write_cooking('Meta/whats-cooking.txt', $cooking);