Raw
1 #!/usr/bin/perl -w
2 # Maintain "what's cooking" messages
3
4 my $MASTER = 'master'; # for now
5
6 $::ENV{TZ} = "US/Pacific"; # for now
7
8 use strict;
9
10 my %reverts = ('next' => {
11 map { $_ => 1 } qw(
12 ) });
13
14 %reverts = ();
15
16 sub phrase_these {
17 my %uniq = ();
18 my (@u) = grep { $uniq{$_}++ == 0 } sort @_;
19 my @d = ();
20 for (my $i = 0; $i < @u; $i++) {
21 push @d, $u[$i];
22 if ($i == @u - 2) {
23 push @d, " and ";
24 } elsif ($i < @u - 2) {
25 push @d, ", ";
26 }
27 }
28 return join('', @d);
29 }
30
31 sub describe_relation {
32 my ($topic_info) = @_;
33 my @desc;
34
35 if (exists $topic_info->{'used'}) {
36 push @desc, ("is used by " .
37 phrase_these(@{$topic_info->{'used'}}));
38 }
39
40 if (exists $topic_info->{'uses'}) {
41 push @desc, ("uses " .
42 phrase_these(@{$topic_info->{'uses'}}));
43 }
44
45 if (0 && exists $topic_info->{'shares'}) {
46 push @desc, ("shares commits with " .
47 phrase_these(@{$topic_info->{'shares'}}));
48 }
49
50 if (!@desc) {
51 return "";
52 }
53
54 return "(this branch " . join("; ", @desc) . ".)";
55 }
56
57 sub desc_from_merge {
58 my ($topic) = @_;
59 my ($fh, $accum);
60 open($fh, '-|',
61 qw(git cat-file commit), "seen^{/^Merge branch '$topic' into }")
62 or return undef;
63 $accum = undef;
64
65 while (<$fh>) {
66 if (!defined $accum) {
67 $accum = "" if (/^Merge branch '$topic' into /);
68 next;
69 }
70 last if (/^\* /);
71 next if ($accum eq "" && /^\s*$/);
72 $accum .= " $_";
73 }
74 for ($accum) {
75 s/^\s+//s;
76 s/\s*$//s;
77 if ($accum eq "") {
78 $_ = undef;
79 } else {
80 $_ = "\n $_";
81 }
82 }
83 return $accum;
84 }
85
86 sub forks_from {
87 my ($topic, $fork, $forkee, @overlap) = @_;
88 my %ovl = map { $_ => 1 } (@overlap, @{$topic->{$forkee}{'log'}});
89
90 push @{$topic->{$fork}{'uses'}}, $forkee;
91 push @{$topic->{$forkee}{'used'}}, $fork;
92 @{$topic->{$fork}{'log'}} = (grep { !exists $ovl{$_} }
93 @{$topic->{$fork}{'log'}});
94 }
95
96 sub topic_relation {
97 my ($topic, $one, $two) = @_;
98
99 my $fh;
100 open($fh, '-|',
101 qw(git log --abbrev), "--format=%m %h",
102 "$one...$two", "^$MASTER")
103 or die "$!: open log --left-right";
104 my (@left, @right);
105 while (<$fh>) {
106 my ($sign, $sha1) = /^(.) (.*)/;
107 if ($sign eq '<') {
108 push @left, $sha1;
109 } elsif ($sign eq '>') {
110 push @right, $sha1;
111 }
112 }
113 close($fh) or die "$!: close log --left-right";
114
115 if (!@left) {
116 if (@right) {
117 forks_from($topic, $two, $one);
118 }
119 } elsif (!@right) {
120 forks_from($topic, $one, $two);
121 } else {
122 push @{$topic->{$one}{'shares'}}, $two;
123 push @{$topic->{$two}{'shares'}}, $one;
124 }
125 }
126
127 my %msgid_to_in_reply_to = ();
128
129 sub get_message_parent {
130 my ($mid) = @_;
131 my $state = 0;
132 my ($message_id, $in_reply_to);
133
134 if (exists $msgid_to_in_reply_to{$mid}) {
135 return $msgid_to_in_reply_to{$mid};
136 }
137
138 open(my $fh, "-|", qw(b4 -q mbox -o-), "$mid");
139 while (<$fh>) {
140 chomp;
141 if ($state == 0) {
142 if (/^$/) {
143 if (defined $message_id && defined $in_reply_to) {
144 $msgid_to_in_reply_to{$message_id} = $in_reply_to;
145 }
146 $state = 1;
147 $message_id = $in_reply_to = undef;
148 } elsif (/^message-id:\s*<(.*)>\s*$/i) {
149 $message_id = $1;
150 } elsif (/^in-reply-to:\s*<(.*)>\s*/i) {
151 $in_reply_to = $1;
152 }
153 next;
154 } elsif ($state == 1) {
155 if (/^From /) {
156 $state = 0;
157 }
158 next;
159 }
160 }
161 close($fh);
162
163 if (exists $msgid_to_in_reply_to{$mid}) {
164 return $msgid_to_in_reply_to{$mid};
165 } else {
166 return ();
167 }
168 }
169
170 sub get_source {
171 my ($branch) = @_;
172 my @id = ();
173 my %msgs = ();
174 my @msgs = ();
175 my %children = ();
176 my %source = ();
177 my %skip_me = ();
178
179 open(my $fh, "-|",
180 qw(git log --notes=amlog --first-parent --format=%N ^master),
181 $branch);
182 while (<$fh>) {
183 if (s/^message-id:\s*<(.*)>\s*$/$1/i) {
184 my $msg = $_;
185 $msgs{$msg} = [get_message_parent($msg)];
186 push @msgs, $msg;
187 }
188 }
189 close($fh);
190
191 # Collect parent messages that are not in the series,
192 # as they are likely to be the cover letters.
193 # but of course the patch could be a reply to an
194 # ordinary message.
195 for my $msg (@msgs) {
196 for my $parent (@{$msgs{$msg}}) {
197 if (!exists $msgs{$parent}) {
198 $source{$parent}++;
199 $children{$parent} ||= [];
200 push @{$children{$parent}}, $msg;
201 }
202 }
203 }
204
205 reduce_sources(\@msgs, \%msgs, \%source, \%children);
206
207 map {
208 " source: <$_>";
209 }
210 (sort keys %source);
211 }
212
213 sub reduce_sources {
214 # Message-source specific hack
215 my ($msgs_array, $msgs_map, $src_map, $child_map) = @_;
216
217 # a singleton
218 if (@{$msgs_array} == 1) {
219 %{$src_map} = ($msgs_array->[0] => 1);
220 return;
221 }
222
223 # Is it from GGG?
224 my @ggg_source = ();
225 for my $msg (keys %$src_map) {
226 if ($msg =~ /^pull\.[^@]*\.gitgitgadget\@/) {
227 push @ggg_source, $msg;
228 }
229 }
230 if (@ggg_source == 1) {
231 %{$src_map} = ($ggg_source[0] => 1);
232 return;
233 }
234
235 # replace a parent with its sole child
236 my %replace = ();
237 for my $src (keys %$src_map) {
238 if (@{$child_map->{$src}} == 1) {
239 $replace{$child_map->{$src}->[0]} = 1;
240 } else {
241 $replace{$src} = $src_map->{$src};
242 }
243 }
244 %$src_map = %replace;
245 }
246
247 =head1
248 Inspect the current set of topics
249
250 Returns a hash:
251
252 $topic = {
253 $branchname => {
254 'tipdate' => date of the tip commit,
255 'desc' => description string,
256 'log' => [ $commit,... ],
257 },
258 }
259
260 =cut
261
262 sub get_commit {
263 my (@base) = ($MASTER, 'next', 'seen');
264 my $fh;
265 open($fh, '-|',
266 qw(git for-each-ref),
267 "--format=%(refname:short) %(authordate:format-local:%Y-%m-%d)",
268 "refs/heads/??/*")
269 or die "$!: open for-each-ref";
270 my @topic;
271 my %topic;
272
273 while (<$fh>) {
274 chomp;
275 my ($branch, $date) = /^(\S+) (.*)$/;
276
277 next if ($branch =~ m|^../wip-|);
278 push @topic, $branch;
279 $date =~ s/ .*//;
280 $topic{$branch} = +{
281 log => [],
282 tipdate => $date,
283 };
284 }
285 close($fh) or die "$!: close for-each-ref";
286
287 my %base = map { $_ => undef } @base;
288 my %commit;
289 my $show_branch_batch = 20;
290
291 while (@topic) {
292 my @t = (@base, splice(@topic, 0, $show_branch_batch));
293 my $header_delim = '-' x scalar(@t);
294 my $contain_pat = '.' x scalar(@t);
295 open($fh, '-|', qw(git show-branch --sparse --sha1-name),
296 map { "refs/heads/$_" } @t)
297 or die "$!: open show-branch";
298 while (<$fh>) {
299 chomp;
300 if ($header_delim) {
301 if (/^$header_delim$/) {
302 $header_delim = undef;
303 }
304 next;
305 }
306 my ($contain, $sha1, $log) =
307 ($_ =~ /^($contain_pat) \[([0-9a-f]+)\] (.*)$/);
308
309 for (my $i = 0; $i < @t; $i++) {
310 my $branch = $t[$i];
311 my $sign = substr($contain, $i, 1);
312 next if ($sign eq ' ');
313 next if (substr($contain, 0, 1) ne ' ');
314
315 if (!exists $commit{$sha1}) {
316 $commit{$sha1} = +{
317 branch => {},
318 log => $log,
319 };
320 }
321 my $co = $commit{$sha1};
322 if (!exists $reverts{$branch}{$sha1}) {
323 $co->{'branch'}{$branch} = 1;
324 }
325 next if (exists $base{$branch});
326 push @{$topic{$branch}{'log'}}, $sha1;
327 }
328 }
329 close($fh) or die "$!: close show-branch";
330 }
331
332 my %shared;
333 for my $sha1 (keys %commit) {
334 my $sign;
335 my $co = $commit{$sha1};
336 if (exists $co->{'branch'}{'next'}) {
337 $sign = '+';
338 } elsif (exists $co->{'branch'}{'seen'}) {
339 $sign = '-';
340 } else {
341 $sign = '.';
342 }
343 $co->{'log'} = $sign . ' ' . $co->{'log'};
344 my @t = (sort grep { !exists $base{$_} }
345 keys %{$co->{'branch'}});
346 next if (@t < 2);
347 my $t = "@t";
348 $shared{$t} = 1;
349 }
350
351 for my $combo (keys %shared) {
352 my @combo = split(' ', $combo);
353 for (my $i = 0; $i < @combo - 1; $i++) {
354 for (my $j = $i + 1; $j < @combo; $j++) {
355 topic_relation(\%topic, $combo[$i], $combo[$j]);
356 }
357 }
358 }
359
360 open($fh, '-|',
361 qw(git log --first-parent --abbrev),
362 "--format=%ci %h %p :%s", "$MASTER..next")
363 or die "$!: open log $MASTER..next";
364 while (<$fh>) {
365 my ($date, $commit, $parent, $tips);
366 unless (($date, $commit, $parent, $tips) =
367 /^([-0-9]+) ..:..:.. .\d{4} (\S+) (\S+) ([^:]*):/) {
368 die "Oops: $_";
369 }
370 for my $tip (split(' ', $tips)) {
371 my $co = $commit{$tip};
372 next unless ($co->{'branch'}{'next'});
373 $co->{'merged'} = " (merged to 'next' on $date at $commit)";
374 }
375 }
376 close($fh) or die "$!: close log $MASTER..next";
377
378 for my $branch (keys %topic) {
379 my @log = ();
380 my $n = scalar(@{$topic{$branch}{'log'}});
381 if (!$n) {
382 delete $topic{$branch};
383 next;
384 } elsif ($n == 1) {
385 $n = "1 commit";
386 } else {
387 $n = "$n commits";
388 }
389 my $d = $topic{$branch}{'tipdate'};
390 my $head = "* $branch ($d) $n\n";
391 my @desc;
392 for (@{$topic{$branch}{'log'}}) {
393 my $co = $commit{$_};
394 if (exists $co->{'merged'}) {
395 push @desc, $co->{'merged'};
396 }
397 push @desc, $commit{$_}->{'log'};
398 }
399
400 if (100 < @desc) {
401 @desc = @desc[0..99];
402 push @desc, "- ...";
403 }
404
405 my $list = join("\n", map { " " . $_ } @desc);
406
407 # NEEDSWORK:
408 # This is done a bit too early. We grabbed all
409 # under refs/heads/??/* without caring if they are
410 # merged to 'seen' yet, and it is correct because
411 # we want to describe a topic that is in the old
412 # edition that is tentatively kicked out of 'seen'.
413 # However, we do not want to say a topic is used
414 # by a new topic that is not yet in 'seen'!
415 my $relation = describe_relation($topic{$branch});
416 $topic{$branch}{'desc'} = $head . $list;
417 if ($relation) {
418 $topic{$branch}{'desc'} .= "\n $relation";
419 }
420 }
421
422 return \%topic;
423 }
424
425 sub blurb_text {
426 my ($mon, $year, $issue, $dow, $date,
427 $master_at, $next_at, $text) = @_;
428
429 my $now_string = localtime;
430 my ($current_dow, $current_mon, $current_date, $current_year) =
431 ($now_string =~ /^(\w+) (\w+) (\d+) [\d:]+ (\d+)$/);
432
433 $mon ||= $current_mon;
434 $year ||= $current_year;
435 $issue ||= "01";
436 $dow ||= $current_dow;
437 $date ||= $current_date;
438 $master_at ||= '0' x 40;
439 $next_at ||= '0' x 40;
440 $text ||= <<'EOF';
441 Here are the topics that have been cooking in my tree. Commits
442 prefixed with '+' are in 'next' (being in 'next' is a sign that a
443 topic is stable enough to be used and is a candidate to be in a
444 future release). Commits prefixed with '-' are only in 'seen', and
445 aren't considered "accepted" at all. They may be annotated with a URL
446 to a message that raises issues but they are by no means exhaustive.
447 A topic without enough support may be discarded after a long period
448 of no activity (of course, it can be resubmitted when new interest
449 arises).
450
451 Copies of the source code to Git live in many repositories, and the
452 following is a list of the ones I push into or their mirrors. Some
453 repositories have only a subset of branches.
454
455 With maint, master, next, seen, todo:
456
457 git://git.kernel.org/pub/scm/git/git.git/
458 git://repo.or.cz/alt-git.git/
459 https://kernel.googlesource.com/pub/scm/git/git/
460 https://github.com/git/git/
461 https://gitlab.com/git-scm/git/
462
463 With all the integration branches and topics broken out:
464
465 https://github.com/gitster/git/
466
467 Even though the preformatted documentation in HTML and man format
468 are not sources, they are published in these repositories for
469 convenience (replace "htmldocs" with "manpages" for the manual
470 pages):
471
472 git://git.kernel.org/pub/scm/git/git-htmldocs.git/
473 https://github.com/gitster/git-htmldocs.git/
474
475 Release tarballs are available at:
476
477 https://www.kernel.org/pub/software/scm/git/
478 EOF
479
480 $text = <<EOF;
481 To: git\@vger.kernel.org
482 Subject: What's cooking in git.git ($mon $year, #$issue)
483 X-$MASTER-at: $master_at
484 X-next-at: $next_at
485 Bcc: lwn\@lwn.net, gitster\@pobox.com
486
487 What's cooking in git.git ($mon $year, #$issue)
488 -----------------------------------------
489
490 $text
491 EOF
492 $text =~ s/\n+\Z/\n/;
493 return $text;
494 }
495
496 my $blurb_match = <<'EOF';
497 (?:(?i:\s*[a-z]+: .*|\s.*)\n)*Subject: What's cooking in \S+ \((\w+) (\d+), #(\d+)(?:; (\w+), (\d+))?\)
498 X-[a-z]*-at: ([0-9a-f]{40})
499 X-next-at: ([0-9a-f]{40})(?:\n(?i:\s*[a-z]+: .*|\s.*))*
500
501 What's cooking in \S+ \(\1 \2, #\3(?:;[^)]*)?\)
502 -{20,}
503 \n*
504 EOF
505
506 my $blurb = "b..l..u..r..b";
507 sub read_previous {
508 my ($fn) = @_;
509 my $fh;
510 my $section = undef;
511 my $serial = 1;
512 my $branch = $blurb;
513 my $last_empty = undef;
514 my (@section, %section, @branch, %branch, %description, @leader);
515 my (%section_description);
516 my $in_unedited_olde = 0;
517
518 if (!-r $fn) {
519 return +{
520 'section_list' => [],
521 'section_data' => {},
522 'topic_description' => {
523 $blurb => {
524 desc => undef,
525 text => blurb_text(),
526 },
527 },
528 };
529 }
530
531 open ($fh, '<', $fn) or die "$!: open $fn";
532 while (<$fh>) {
533 chomp;
534 s/\s+$//;
535 if ($in_unedited_olde) {
536 if (/^>>$/) {
537 $in_unedited_olde = 0;
538 $_ = " | $_";
539 }
540 } elsif (/^<<$/) {
541 $in_unedited_olde = 1;
542 }
543
544 if ($in_unedited_olde) {
545 $_ = " | $_";
546 }
547
548 if (defined $section && /^-{20,}$/) {
549 $_ = "";
550 }
551 if (/^$/) {
552 $last_empty = 1;
553 next;
554 }
555 if (/^\[(.*)\]\s*$/) {
556 $section = $1;
557 $branch = undef;
558 if (!exists $section{$section}) {
559 push @section, $section;
560 $section{$section} = [];
561 }
562 next;
563 }
564 if (defined $section &&
565 !defined $branch &&
566 !/^\* /) {
567 $section_description{$section} ||= "";
568 $section_description{$section} .= "$_\n";
569 next;
570 }
571
572 if (defined $section && /^\* (\S+) /) {
573 $branch = $1;
574 $last_empty = 0;
575 if (!exists $branch{$branch}) {
576 push @branch, [$branch, $section];
577 $branch{$branch} = 1;
578 }
579 push @{$section{$section}}, $branch;
580 }
581 if (defined $branch) {
582 my $was_last_empty = $last_empty;
583 $last_empty = 0;
584 if (!exists $description{$branch}) {
585 $description{$branch} = [];
586 }
587 if ($was_last_empty) {
588 push @{$description{$branch}}, "";
589 }
590 push @{$description{$branch}}, $_;
591 }
592 }
593 close($fh);
594
595 my $lead = " ";
596 for my $branch (keys %description) {
597 my $ary = $description{$branch};
598 if ($branch eq $blurb) {
599 while (@{$ary} && $ary->[-1] =~ /^-{30,}$/) {
600 pop @{$ary};
601 }
602 $description{$branch} = +{
603 desc => undef,
604 text => join("\n", @{$ary}),
605 };
606 } else {
607 my (@desc, @src, @txt) = ();
608
609 while (@{$ary}) {
610 my $elem = shift @{$ary};
611 last if ($elem eq '');
612 push @desc, $elem;
613 }
614 for (@{$ary}) {
615 s/^\s+//;
616 $_ = "$lead$_";
617 s/\s+$//;
618 if (/^${lead}source:/) {
619 push @src, $_;
620 } else {
621 push @txt, $_;
622 }
623 }
624
625 $description{$branch} = +{
626 desc => join("\n", @desc),
627 text => join("\n", @txt),
628 src => join("\n", @src),
629 };
630 }
631 }
632
633 return +{
634 section_list => \@section,
635 section_data => \%section,
636 topic_description => \%description,
637 section_description => \%section_description,
638 };
639 }
640
641 sub write_cooking {
642 my ($fn, $cooking) = @_;
643 my $fh;
644
645 open($fh, '>', $fn) or die "$!: open $fn";
646 print $fh $cooking->{'topic_description'}{$blurb}{'text'};
647
648 for my $section_name (@{$cooking->{'section_list'}}) {
649 my $topic_list = $cooking->{'section_data'}{$section_name};
650 next if (!@{$topic_list});
651
652 print $fh "\n";
653 print $fh '-' x 50, "\n";
654 print $fh "[$section_name]\n";
655 my $lead = "\n";
656
657 if ($cooking->{'section_description'}{$section_name}) {
658 print $fh "\n", $cooking->{'section_description'}{$section_name};
659 }
660
661 for my $topic (@{$topic_list}) {
662 my $d = $cooking->{'topic_description'}{$topic};
663
664 print $fh $lead, $d->{'desc'}, "\n";
665 if ($d->{'text'}) {
666 # Final clean-up. No leading or trailing
667 # blank lines, no multi-line gaps.
668 for ($d->{'text'}) {
669 s/^\n+//s;
670 s/\n{3,}/\n\n/s;
671 s/\n+$//s;
672 }
673 print $fh "\n", $d->{'text'}, "\n";
674 }
675 if ($d->{'src'}) {
676 if (!$d->{'text'}) {
677 print $fh "\n";
678 }
679 print $fh $d->{'src'}, "\n";
680 }
681 $lead = "\n\n";
682 }
683 }
684 close($fh);
685 }
686
687 my $graduated = "Graduated to '$MASTER'";
688 my $new_topics = 'New Topics';
689 my $discarded = 'Discarded';
690 my $cooking_topics = 'Cooking';
691
692 sub update_issue {
693 my ($cooking) = @_;
694 my ($fh, $master_at, $next_at, $incremental);
695
696 open($fh, '-|',
697 qw(git for-each-ref),
698 "--format=%(refname:short) %(objectname)",
699 "refs/heads/$MASTER",
700 "refs/heads/next") or die "$!: open for-each-ref";
701 while (<$fh>) {
702 my ($branch, $at) = /^(\S+) (\S+)$/;
703 if ($branch eq $MASTER) { $master_at = $at; }
704 if ($branch eq 'next') { $next_at = $at; }
705 }
706 close($fh) or die "$!: close for-each-ref";
707
708 $incremental = ((-r "Meta/whats-cooking.txt") &&
709 system("cd Meta && " .
710 "git diff --quiet --no-ext-diff HEAD -- " .
711 "whats-cooking.txt"));
712
713 my $now_string = localtime;
714 my ($current_dow, $current_mon, $current_date, $current_year) =
715 ($now_string =~ /^(\w+) (\w+) +(\d+) [\d:]+ (\d+)$/);
716
717 my $btext = $cooking->{'topic_description'}{$blurb}{'text'};
718 if ($btext !~ s/\A$blurb_match//) {
719 for my $line (split(/\n/, $btext)) {
720 print STDERR ">>> $line\n";
721 }
722 die "match pattern broken?";
723 }
724 my ($mon, $year, $issue, $dow, $date) = ($1, $2, $3, $4, $5);
725
726 if ($current_mon ne $mon || $current_year ne $year) {
727 $issue = "01";
728 } elsif (!$incremental) {
729 $issue =~ s/^0*//;
730 $issue = sprintf "%02d", ($issue + 1);
731 }
732 $mon = $current_mon;
733 $year = $current_year;
734 $dow = $current_dow;
735 $date = $current_date;
736
737 $cooking->{'topic_description'}{$blurb}{'text'} =
738 blurb_text($mon, $year, $issue, $dow, $date,
739 $master_at, $next_at, $btext);
740
741 # If starting a new issue, move what used to be in
742 # new topics to cooking topics.
743 if (!$incremental) {
744 my $sd = $cooking->{'section_data'};
745 my $sl = $cooking->{'section_list'};
746
747 if (exists $sd->{$new_topics}) {
748 if (!exists $sd->{$cooking_topics}) {
749 $sd->{$cooking_topics} = [];
750 unshift @{$sl}, $cooking_topics;
751 }
752 unshift @{$sd->{$cooking_topics}}, @{$sd->{$new_topics}};
753 }
754 $sd->{$new_topics} = [];
755 }
756
757 return $incremental;
758 }
759
760 sub topic_in_seen {
761 my ($topic_desc) = @_;
762 for my $line (split(/\n/, $topic_desc)) {
763 if ($line =~ /^ [+-] /) {
764 return 1;
765 }
766 }
767 return 0;
768 }
769
770 my $mergetomaster;
771 sub prepare_mergetomaster {
772 if (!defined $mergetomaster) {
773 my $master = `git describe $MASTER`;
774 if ($master =~ /-rc(\d+)(-\d+-g[0-9a-f]+)?$/ && $1 != 0) {
775 $mergetomaster = "Will cook in 'next'.";
776 } else {
777 $mergetomaster = "Will merge to '$MASTER'.";
778 }
779 }
780 }
781
782 sub tweak_willdo {
783 my ($td) = @_;
784 my $desc = $td->{'desc'};
785 my $text = $td->{'text'};
786
787 # If updated description (i.e. the list of patches with
788 # merge trail to 'next') has 'merged to next', then
789 # tweak the topic to be slated to 'master'.
790 # NEEDSWORK: does this work correctly for a half-merged topic?
791 $desc =~ s/\n<<\n.*//s;
792 if ($desc =~ /^ \(merged to 'next'/m) {
793 $text =~ s/^ Will merge (back )?to 'next'\.$/ $mergetomaster/m;
794 $text =~ s/^ Will merge to and (then )?cook in 'next'\.$/ Will cook in 'next'./m;
795 $text =~ s/^ Will merge to 'next' and (then )?to '$MASTER'\.$/ Will merge to '$MASTER'./m;
796 }
797 $td->{'text'} = $text;
798 }
799
800 sub tweak_graduated {
801 my ($td) = @_;
802
803 # Rename the "Will merge" marker from topics that have graduated.
804 for ($td->{'text'}) {
805 s/\n Will merge to '$MASTER'\.(\n|$)/\n Graduated to '$MASTER'.$1/s;
806 }
807 }
808
809 sub merge_cooking {
810 my ($cooking, $current) = @_;
811
812 # A hash to find <desc, text> with a branch name or $blurb
813 my $td = $cooking->{'topic_description'};
814
815 # A hash to find a list of $td element given a section name
816 my $sd = $cooking->{'section_data'};
817
818 # A list of section names
819 my $sl = $cooking->{'section_list'};
820
821 my (@new_topic, @gone_topic);
822
823 # Make sure "New Topics" and "Graduated" exists
824 if (!exists $sd->{$new_topics}) {
825 $sd->{$new_topics} = [];
826 unshift @{$sl}, $new_topics;
827 }
828
829 if (!exists $sd->{$graduated}) {
830 $sd->{$graduated} = [];
831 unshift @{$sl}, $graduated;
832 }
833
834 my $incremental = update_issue($cooking);
835
836 for my $topic (sort keys %{$current}) {
837 if (!exists $td->{$topic}) {
838 # Ignore new topics without anything merged
839 if (topic_in_seen($current->{$topic}{'desc'})) {
840 push @new_topic, $topic;
841 # lazily find the source for a new topic.
842 $current->{$topic}{'src'} = join("\n", get_source($topic));
843 my $summary = desc_from_merge($topic);
844 if (defined $summary) {
845 $current->{$topic}{'desc'} .= "\n$summary";
846 }
847 }
848 next;
849 }
850
851 # Annotate if the contents of the topic changed
852 my $topic_changed = 0;
853 my $n = $current->{$topic}{'desc'};
854 my $o = $td->{$topic}{'desc'};
855 if ($n ne $o) {
856 $topic_changed = 1;
857 $td->{$topic}{'desc'} = $n . "\n<<\n" . $o ."\n>>";
858 tweak_willdo($td->{$topic});
859 }
860
861 # Keep the original source for unchanged topic
862 if ($topic_changed) {
863 # lazily find out the source for the latest round.
864 $current->{$topic}{'src'} = join("\n", get_source($topic));
865
866 $n = $current->{$topic}{'src'};
867 $o = $td->{$topic}{'src'};
868 if ($n ne $o) {
869 $o = join("\n",
870 map { s/^\s*//; "-$_"; }
871 split(/\n/, $o));
872 $n = join("\n",
873 map { s/^\s*//; "+$_"; }
874 split(/\n/, $n));
875 $td->{$topic}{'src'} = join("\n", "<<", $o, $n, ">>");
876 }
877 }
878 }
879
880 for my $topic (sort keys %{$td}) {
881 next if ($topic eq $blurb);
882 next if (!$incremental &&
883 grep { $topic eq $_ } @{$sd->{$graduated}});
884 next if (grep { $topic eq $_ } @{$sd->{$discarded}});
885 if (!exists $current->{$topic}) {
886 push @gone_topic, $topic;
887 }
888 }
889
890 for (@new_topic) {
891 push @{$sd->{$new_topics}}, $_;
892 $td->{$_}{'desc'} = $current->{$_}{'desc'};
893 $td->{$_}{'src'} = $current->{$_}{'src'};
894 }
895
896 if (!$incremental) {
897 $sd->{$graduated} = [];
898 }
899
900 if (@gone_topic) {
901 for my $topic (@gone_topic) {
902 for my $section (@{$sl}) {
903 my $pre = scalar(@{$sd->{$section}});
904 @{$sd->{$section}} = (grep { $_ ne $topic }
905 @{$sd->{$section}});
906 my $post = scalar(@{$sd->{$section}});
907 next if ($pre == $post);
908 }
909 }
910 for (@gone_topic) {
911 push @{$sd->{$graduated}}, $_;
912 tweak_graduated($td->{$_});
913 }
914 }
915 }
916
917 ################################################################
918 # WilDo
919 sub wildo_queue {
920 my ($in_section, $what, $topic) = @_;
921 if (defined $topic) {
922 for ($in_section) {
923 return if (/^Graduated to/ || /^Discarded$/);
924 }
925 my $action = $topic->[6] || "Unclassified.";
926 if (!exists $what->{$action}) {
927 $what->{$action} = [];
928 }
929 push @{$what->{$action}}, $topic;
930 }
931 }
932
933 sub wildo_match {
934 # NEEDSWORK: unify with Reintegrate::annotate_merge
935 if (/^Will (?:\S+ ){0,2}(fast-track|hold|keep|merge|drop|discard|cook|kick|defer|eject|be re-?rolled|wait)[;,. ]/ ||
936 /^Not urgent/ || /^Not ready/ || /^Waiting for / || /^Under discussion/ ||
937 /^Can wait in / || /^Still / || /^Stuck / || /^On hold/ || /^Breaks / ||
938 /^Inviting / || /^Comments/ || /^Retracted/ ||
939 /^Needs? / || /^Expecting / || /^May want to / || /^Under review/) {
940 return 1;
941 }
942 return 0;
943 }
944
945 sub wildo {
946 my $fd = shift;
947 my (%what, $topic, $last_merge_to_next, $in_section, $in_desc);
948 my $too_recent = '9999-99-99';
949
950 while (<$fd>) {
951 chomp;
952
953 if (/^\[(.*)\]$/) {
954 my $old_section = $in_section;
955 $in_section = $1;
956 wildo_queue($old_section, \%what, $topic);
957 $topic = $in_desc = undef;
958 next;
959 }
960
961 if (/^\* (\S+) \(([-0-9]+)\) (\d+) commits?$/) {
962 wildo_queue($in_section, \%what, $topic);
963
964 # [0] tip-date
965 # [1] next-date
966 # [2] topic
967 # [3] count
968 # [4] seen-count
969 # [5] source
970 # [6] action
971 $topic = [$2, $too_recent, $1, $3, 0, [], undef];
972 $in_desc = undef;
973 next;
974 }
975
976 if (defined $topic &&
977 ($topic->[1] eq $too_recent) &&
978 ($topic->[4] == 0) &&
979 (/^ \(merged to 'next' on ([-0-9]+)/)) {
980 $topic->[1] = $1;
981 }
982 if (defined $topic && /^ - /) {
983 $topic->[4]++;
984 }
985
986 if (defined $topic && /^$/) {
987 $in_desc = 1;
988 next;
989 }
990
991 next unless defined $topic && $in_desc;
992
993 s/^\s+//;
994
995 if (/Originally merged to 'next' on ([-0-9]+)/) {
996 $topic->[1] = $1;
997 next;
998 }
999
1000 if (wildo_match($_)) {
1001 $topic->[6] = $_;
1002 next;
1003 }
1004
1005 if (/^(?:source:|cf\.)\s+(.*)$/) {
1006 $topic->[5] ||= [];
1007 push @{$topic->[5]}, $1;
1008 next;
1009 }
1010
1011 }
1012 wildo_queue($in_section, \%what, $topic);
1013
1014 my $ipbl = "";
1015 for my $what (sort keys %what) {
1016 print "$ipbl$what\n";
1017 for $topic (sort { (($a->[1] cmp $b->[1]) ||
1018 ($a->[0] cmp $b->[0])) }
1019 @{$what{$what}}) {
1020 my ($tip, $next, $name, $count, $seen, $source) = @$topic;
1021 my ($sign);
1022 $tip =~ s/^\d{4}-//;
1023 if (($next eq $too_recent) || (0 < $seen)) {
1024 $sign = "-";
1025 $next = " " x 6;
1026 } else {
1027 $sign = "+";
1028 $next =~ s|^\d{4}-|/|;
1029 }
1030 $count = "#$count";
1031 printf " %s %-60s %s%s %5s\n", $sign, $name, $tip, $next, $count;
1032 if ($what =~ /^Will merge to '\w+'/ && $what !~ /\?$/ ||
1033 $what eq $mergetomaster) {
1034 next;
1035 }
1036
1037 for my $s (@$source) {
1038 if (0 && $s =~ /^<(.*)>$/) {
1039 $s = "https://lore.kernel.org/git/$1/";
1040 }
1041 printf " $s\n";
1042 }
1043 }
1044 $ipbl = "\n";
1045 }
1046 }
1047
1048 ################################################################
1049 # HavDone
1050 sub havedone_show {
1051 my $topic = shift;
1052 my $str = shift;
1053 my $prefix = " * ";
1054 $str =~ s/\A\n+//;
1055 $str =~ s/\n+\Z//;
1056
1057 print "($topic)\n";
1058 for $str (split(/\n/, $str)) {
1059 print "$prefix$str\n";
1060 $prefix = " ";
1061 }
1062 }
1063
1064 sub havedone_count {
1065 my @range = @_;
1066 my $cnt = `git rev-list --count @range`;
1067 chomp $cnt;
1068 return $cnt;
1069 }
1070
1071 sub havedone {
1072 my $fh;
1073 my %topic = ();
1074 my @topic = ();
1075 my ($topic, $to_maint, %to_maint, %merged, $in_desc);
1076 if (!@ARGV) {
1077 open($fh, '-|',
1078 qw(git rev-list --first-parent -1), $MASTER,
1079 qw(-- Documentation/RelNotes RelNotes))
1080 or die "$!: open rev-list";
1081 my ($rev) = <$fh>;
1082 close($fh) or die "$!: close rev-list";
1083 chomp $rev;
1084 @ARGV = ("$rev..$MASTER");
1085 }
1086 open($fh, '-|',
1087 qw(git log --first-parent --oneline --reverse), @ARGV)
1088 or die "$!: open log --first-parent";
1089 while (<$fh>) {
1090 my ($sha1, $branch) = /^([0-9a-f]+) Merge branch '(.*)'$/;
1091 next unless $branch;
1092 $topic{$branch} = "";
1093 $merged{$branch} = $sha1;
1094 push @topic, $branch;
1095 }
1096 close($fh) or die "$!: close log --first-parent";
1097 open($fh, "<", "Meta/whats-cooking.txt")
1098 or die "$!: open whats-cooking";
1099 while (<$fh>) {
1100 chomp;
1101 if (/^\[(.*)\]$/) {
1102 # section header
1103 $in_desc = $topic = undef;
1104 next;
1105 }
1106 if (/^\* (\S+) \([-0-9]+\) \d+ commits?$/) {
1107 if (exists $topic{$1}) {
1108 $topic = $1;
1109 $to_maint = 0;
1110 } else {
1111 $in_desc = $topic = undef;
1112 }
1113 next;
1114 }
1115 if (defined $topic && /^$/) {
1116 $in_desc = 1;
1117 next;
1118 }
1119
1120 next unless defined $topic && $in_desc;
1121
1122 s/^\s+//;
1123 if (wildo_match($_)) {
1124 next;
1125 }
1126 $topic{$topic} .= "$_\n";
1127 }
1128 close($fh) or die "$!: close whats-cooking";
1129
1130 for $topic (@topic) {
1131 my $merged = $merged{$topic};
1132 my $in_master = havedone_count("$merged^1..$merged^2");
1133 my $not_in_maint = havedone_count("maint..$merged^2");
1134 if ($in_master == $not_in_maint) {
1135 $to_maint{$topic} = 1;
1136 }
1137 }
1138
1139 my $shown = 0;
1140 for $topic (@topic) {
1141 next if (exists $to_maint{$topic});
1142 havedone_show($topic, $topic{$topic});
1143 print "\n";
1144 $shown++;
1145 }
1146
1147 if ($shown) {
1148 print "-" x 64, "\n";
1149 }
1150
1151 for $topic (@topic) {
1152 next unless (exists $to_maint{$topic});
1153 havedone_show($topic, $topic{$topic});
1154 my $sha1 = `git rev-parse --short $topic`;
1155 chomp $sha1;
1156 print " (merge $sha1 $topic later to maint).\n";
1157 print "\n";
1158 }
1159 }
1160
1161 ################################################################
1162 # WhatsCooking
1163
1164 sub doit {
1165 my $cooking = read_previous('Meta/whats-cooking.txt');
1166 my $topic = get_commit($cooking);
1167 merge_cooking($cooking, $topic);
1168 write_cooking('Meta/whats-cooking.txt', $cooking);
1169 }
1170
1171 ################################################################
1172
1173 ################################################################
1174 # Newer one exists?
1175
1176 sub newer {
1177 my $fn = 'Meta/whats-cooking.txt';
1178 my $fh;
1179 my $last_topic;
1180
1181 open ($fh, '<', $fn) or die "$!: open $fn";
1182 while (<$fh>) {
1183 chomp;
1184 if (/^[*] (\S+) \(\d\d\d\d-\d\d-\d\d\)/) {
1185 $last_topic = $1;
1186 next;
1187 }
1188 if (/^ \(a newer iteration/) {
1189 print "$last_topic\n$_\n";
1190 }
1191 }
1192 close ($fh);
1193 }
1194
1195 ################################################################
1196 # Main
1197
1198 use Getopt::Long;
1199
1200 my ($wildo, $havedone, $newer);
1201 if (!GetOptions("wildo" => \$wildo,
1202 "newer" => \$newer,
1203 "havedone" => \$havedone)) {
1204 print STDERR "$0 [--wildo|--havedone]\n";
1205 exit 1;
1206 }
1207
1208 prepare_mergetomaster;
1209
1210 if ($wildo) {
1211 my $fd;
1212 if (!@ARGV) {
1213 open($fd, "<", "Meta/whats-cooking.txt");
1214 } elsif (@ARGV != 1) {
1215 print STDERR "$0 --wildo [filename|HEAD|-]\n";
1216 exit 1;
1217 } elsif ($ARGV[0] eq '-') {
1218 $fd = \*STDIN;
1219 } elsif ($ARGV[0] =~ /^HEAD/) {
1220 open($fd, "-|",
1221 qw(git --git-dir=Meta/.git cat-file -p),
1222 "$ARGV[0]:whats-cooking.txt");
1223 } elsif ($ARGV[0] eq ":") {
1224 open($fd, "-|",
1225 qw(git --git-dir=Meta/.git cat-file -p),
1226 ":whats-cooking.txt");
1227 } else {
1228 open($fd, "<", $ARGV[0]);
1229 }
1230 wildo($fd);
1231 } elsif ($newer) {
1232 newer();
1233 } elsif ($havedone) {
1234 havedone();
1235 } elsif (@ARGV) {
1236 print STDERR "$0 does not take extra args: @ARGV\n";
1237 exit 1;
1238 } else {
1239 doit();
1240 }