Files
git/cook
Junio C Hamano abe2305a84 Meta/cook: make it slightly easier to debug
When the blurb pattern finds no match, the program only said that it
does not match and died.  It would be very hard to make it report
that this and that token in the blurb text is misspelt and it would
match the pattern when that typo gets fixed, but at least it should
be able to dump the haystack it was looking for needles in to let
the human user do the guessing.
2026-07-26 21:08:42 -07:00

1241 lines
28 KiB
Perl
Executable File

#!/usr/bin/perl -w
# Maintain "what's cooking" messages
my $MASTER = 'master'; # for now
$::ENV{TZ} = "US/Pacific"; # for now
use strict;
my %reverts = ('next' => {
map { $_ => 1 } qw(
) });
%reverts = ();
sub phrase_these {
my %uniq = ();
my (@u) = grep { $uniq{$_}++ == 0 } sort @_;
my @d = ();
for (my $i = 0; $i < @u; $i++) {
push @d, $u[$i];
if ($i == @u - 2) {
push @d, " and ";
} elsif ($i < @u - 2) {
push @d, ", ";
}
}
return join('', @d);
}
sub describe_relation {
my ($topic_info) = @_;
my @desc;
if (exists $topic_info->{'used'}) {
push @desc, ("is used by " .
phrase_these(@{$topic_info->{'used'}}));
}
if (exists $topic_info->{'uses'}) {
push @desc, ("uses " .
phrase_these(@{$topic_info->{'uses'}}));
}
if (0 && exists $topic_info->{'shares'}) {
push @desc, ("shares commits with " .
phrase_these(@{$topic_info->{'shares'}}));
}
if (!@desc) {
return "";
}
return "(this branch " . join("; ", @desc) . ".)";
}
sub desc_from_merge {
my ($topic) = @_;
my ($fh, $accum);
open($fh, '-|',
qw(git cat-file commit), "seen^{/^Merge branch '$topic' into }")
or return undef;
$accum = undef;
while (<$fh>) {
if (!defined $accum) {
$accum = "" if (/^Merge branch '$topic' into /);
next;
}
last if (/^\* /);
next if ($accum eq "" && /^\s*$/);
$accum .= " $_";
}
for ($accum) {
s/^\s+//s;
s/\s*$//s;
if ($accum eq "") {
$_ = undef;
} else {
$_ = "\n $_";
}
}
return $accum;
}
sub forks_from {
my ($topic, $fork, $forkee, @overlap) = @_;
my %ovl = map { $_ => 1 } (@overlap, @{$topic->{$forkee}{'log'}});
push @{$topic->{$fork}{'uses'}}, $forkee;
push @{$topic->{$forkee}{'used'}}, $fork;
@{$topic->{$fork}{'log'}} = (grep { !exists $ovl{$_} }
@{$topic->{$fork}{'log'}});
}
sub topic_relation {
my ($topic, $one, $two) = @_;
my $fh;
open($fh, '-|',
qw(git log --abbrev), "--format=%m %h",
"$one...$two", "^$MASTER")
or die "$!: open log --left-right";
my (@left, @right);
while (<$fh>) {
my ($sign, $sha1) = /^(.) (.*)/;
if ($sign eq '<') {
push @left, $sha1;
} elsif ($sign eq '>') {
push @right, $sha1;
}
}
close($fh) or die "$!: close log --left-right";
if (!@left) {
if (@right) {
forks_from($topic, $two, $one);
}
} elsif (!@right) {
forks_from($topic, $one, $two);
} else {
push @{$topic->{$one}{'shares'}}, $two;
push @{$topic->{$two}{'shares'}}, $one;
}
}
my %msgid_to_in_reply_to = ();
sub get_message_parent {
my ($mid) = @_;
my $state = 0;
my ($message_id, $in_reply_to);
if (exists $msgid_to_in_reply_to{$mid}) {
return $msgid_to_in_reply_to{$mid};
}
open(my $fh, "-|", qw(b4 -q mbox -o-), "$mid");
while (<$fh>) {
chomp;
if ($state == 0) {
if (/^$/) {
if (defined $message_id && defined $in_reply_to) {
$msgid_to_in_reply_to{$message_id} = $in_reply_to;
}
$state = 1;
$message_id = $in_reply_to = undef;
} elsif (/^message-id:\s*<(.*)>\s*$/i) {
$message_id = $1;
} elsif (/^in-reply-to:\s*<(.*)>\s*/i) {
$in_reply_to = $1;
}
next;
} elsif ($state == 1) {
if (/^From /) {
$state = 0;
}
next;
}
}
close($fh);
if (exists $msgid_to_in_reply_to{$mid}) {
return $msgid_to_in_reply_to{$mid};
} else {
return ();
}
}
sub get_source {
my ($branch) = @_;
my @id = ();
my %msgs = ();
my @msgs = ();
my %children = ();
my %source = ();
my %skip_me = ();
open(my $fh, "-|",
qw(git log --notes=amlog --first-parent --format=%N ^master),
$branch);
while (<$fh>) {
if (s/^message-id:\s*<(.*)>\s*$/$1/i) {
my $msg = $_;
$msgs{$msg} = [get_message_parent($msg)];
push @msgs, $msg;
}
}
close($fh);
# Collect parent messages that are not in the series,
# as they are likely to be the cover letters.
# but of course the patch could be a reply to an
# ordinary message.
for my $msg (@msgs) {
for my $parent (@{$msgs{$msg}}) {
if (!exists $msgs{$parent}) {
$source{$parent}++;
$children{$parent} ||= [];
push @{$children{$parent}}, $msg;
}
}
}
reduce_sources(\@msgs, \%msgs, \%source, \%children);
map {
" source: <$_>";
}
(sort keys %source);
}
sub reduce_sources {
# Message-source specific hack
my ($msgs_array, $msgs_map, $src_map, $child_map) = @_;
# a singleton
if (@{$msgs_array} == 1) {
%{$src_map} = ($msgs_array->[0] => 1);
return;
}
# Is it from GGG?
my @ggg_source = ();
for my $msg (keys %$src_map) {
if ($msg =~ /^pull\.[^@]*\.gitgitgadget\@/) {
push @ggg_source, $msg;
}
}
if (@ggg_source == 1) {
%{$src_map} = ($ggg_source[0] => 1);
return;
}
# replace a parent with its sole child
my %replace = ();
for my $src (keys %$src_map) {
if (@{$child_map->{$src}} == 1) {
$replace{$child_map->{$src}->[0]} = 1;
} else {
$replace{$src} = $src_map->{$src};
}
}
%$src_map = %replace;
}
=head1
Inspect the current set of topics
Returns a hash:
$topic = {
$branchname => {
'tipdate' => date of the tip commit,
'desc' => description string,
'log' => [ $commit,... ],
},
}
=cut
sub get_commit {
my (@base) = ($MASTER, 'next', 'seen');
my $fh;
open($fh, '-|',
qw(git for-each-ref),
"--format=%(refname:short) %(authordate:format-local:%Y-%m-%d)",
"refs/heads/??/*")
or die "$!: open for-each-ref";
my @topic;
my %topic;
while (<$fh>) {
chomp;
my ($branch, $date) = /^(\S+) (.*)$/;
next if ($branch =~ m|^../wip-|);
push @topic, $branch;
$date =~ s/ .*//;
$topic{$branch} = +{
log => [],
tipdate => $date,
};
}
close($fh) or die "$!: close for-each-ref";
my %base = map { $_ => undef } @base;
my %commit;
my $show_branch_batch = 20;
while (@topic) {
my @t = (@base, splice(@topic, 0, $show_branch_batch));
my $header_delim = '-' x scalar(@t);
my $contain_pat = '.' x scalar(@t);
open($fh, '-|', qw(git show-branch --sparse --sha1-name),
map { "refs/heads/$_" } @t)
or die "$!: open show-branch";
while (<$fh>) {
chomp;
if ($header_delim) {
if (/^$header_delim$/) {
$header_delim = undef;
}
next;
}
my ($contain, $sha1, $log) =
($_ =~ /^($contain_pat) \[([0-9a-f]+)\] (.*)$/);
for (my $i = 0; $i < @t; $i++) {
my $branch = $t[$i];
my $sign = substr($contain, $i, 1);
next if ($sign eq ' ');
next if (substr($contain, 0, 1) ne ' ');
if (!exists $commit{$sha1}) {
$commit{$sha1} = +{
branch => {},
log => $log,
};
}
my $co = $commit{$sha1};
if (!exists $reverts{$branch}{$sha1}) {
$co->{'branch'}{$branch} = 1;
}
next if (exists $base{$branch});
push @{$topic{$branch}{'log'}}, $sha1;
}
}
close($fh) or die "$!: close show-branch";
}
my %shared;
for my $sha1 (keys %commit) {
my $sign;
my $co = $commit{$sha1};
if (exists $co->{'branch'}{'next'}) {
$sign = '+';
} elsif (exists $co->{'branch'}{'seen'}) {
$sign = '-';
} else {
$sign = '.';
}
$co->{'log'} = $sign . ' ' . $co->{'log'};
my @t = (sort grep { !exists $base{$_} }
keys %{$co->{'branch'}});
next if (@t < 2);
my $t = "@t";
$shared{$t} = 1;
}
for my $combo (keys %shared) {
my @combo = split(' ', $combo);
for (my $i = 0; $i < @combo - 1; $i++) {
for (my $j = $i + 1; $j < @combo; $j++) {
topic_relation(\%topic, $combo[$i], $combo[$j]);
}
}
}
open($fh, '-|',
qw(git log --first-parent --abbrev),
"--format=%ci %h %p :%s", "$MASTER..next")
or die "$!: open log $MASTER..next";
while (<$fh>) {
my ($date, $commit, $parent, $tips);
unless (($date, $commit, $parent, $tips) =
/^([-0-9]+) ..:..:.. .\d{4} (\S+) (\S+) ([^:]*):/) {
die "Oops: $_";
}
for my $tip (split(' ', $tips)) {
my $co = $commit{$tip};
next unless ($co->{'branch'}{'next'});
$co->{'merged'} = " (merged to 'next' on $date at $commit)";
}
}
close($fh) or die "$!: close log $MASTER..next";
for my $branch (keys %topic) {
my @log = ();
my $n = scalar(@{$topic{$branch}{'log'}});
if (!$n) {
delete $topic{$branch};
next;
} elsif ($n == 1) {
$n = "1 commit";
} else {
$n = "$n commits";
}
my $d = $topic{$branch}{'tipdate'};
my $head = "* $branch ($d) $n\n";
my @desc;
for (@{$topic{$branch}{'log'}}) {
my $co = $commit{$_};
if (exists $co->{'merged'}) {
push @desc, $co->{'merged'};
}
push @desc, $commit{$_}->{'log'};
}
if (100 < @desc) {
@desc = @desc[0..99];
push @desc, "- ...";
}
my $list = join("\n", map { " " . $_ } @desc);
# NEEDSWORK:
# This is done a bit too early. We grabbed all
# under refs/heads/??/* without caring if they are
# merged to 'seen' yet, and it is correct because
# we want to describe a topic that is in the old
# edition that is tentatively kicked out of 'seen'.
# However, we do not want to say a topic is used
# by a new topic that is not yet in 'seen'!
my $relation = describe_relation($topic{$branch});
$topic{$branch}{'desc'} = $head . $list;
if ($relation) {
$topic{$branch}{'desc'} .= "\n $relation";
}
}
return \%topic;
}
sub blurb_text {
my ($mon, $year, $issue, $dow, $date,
$master_at, $next_at, $text) = @_;
my $now_string = localtime;
my ($current_dow, $current_mon, $current_date, $current_year) =
($now_string =~ /^(\w+) (\w+) (\d+) [\d:]+ (\d+)$/);
$mon ||= $current_mon;
$year ||= $current_year;
$issue ||= "01";
$dow ||= $current_dow;
$date ||= $current_date;
$master_at ||= '0' x 40;
$next_at ||= '0' x 40;
$text ||= <<'EOF';
Here are the topics that have been cooking in my tree. Commits
prefixed with '+' are in 'next' (being in 'next' is a sign that a
topic is stable enough to be used and is a candidate to be in a
future release). Commits prefixed with '-' are only in 'seen', and
aren't considered "accepted" at all. They may be annotated with a URL
to a message that raises issues but they are by no means exhaustive.
A topic without enough support may be discarded after a long period
of no activity (of course, it can be resubmitted when new interest
arises).
Copies of the source code to Git live in many repositories, and the
following is a list of the ones I push into or their mirrors. Some
repositories have only a subset of branches.
With maint, master, next, seen, todo:
git://git.kernel.org/pub/scm/git/git.git/
git://repo.or.cz/alt-git.git/
https://kernel.googlesource.com/pub/scm/git/git/
https://github.com/git/git/
https://gitlab.com/git-scm/git/
With all the integration branches and topics broken out:
https://github.com/gitster/git/
Even though the preformatted documentation in HTML and man format
are not sources, they are published in these repositories for
convenience (replace "htmldocs" with "manpages" for the manual
pages):
git://git.kernel.org/pub/scm/git/git-htmldocs.git/
https://github.com/gitster/git-htmldocs.git/
Release tarballs are available at:
https://www.kernel.org/pub/software/scm/git/
EOF
$text = <<EOF;
To: git\@vger.kernel.org
Subject: What's cooking in git.git ($mon $year, #$issue)
X-$MASTER-at: $master_at
X-next-at: $next_at
Bcc: lwn\@lwn.net, gitster\@pobox.com
What's cooking in git.git ($mon $year, #$issue)
-----------------------------------------
$text
EOF
$text =~ s/\n+\Z/\n/;
return $text;
}
my $blurb_match = <<'EOF';
(?:(?i:\s*[a-z]+: .*|\s.*)\n)*Subject: What's cooking in \S+ \((\w+) (\d+), #(\d+)(?:; (\w+), (\d+))?\)
X-[a-z]*-at: ([0-9a-f]{40})
X-next-at: ([0-9a-f]{40})(?:\n(?i:\s*[a-z]+: .*|\s.*))*
What's cooking in \S+ \(\1 \2, #\3(?:;[^)]*)?\)
-{20,}
\n*
EOF
my $blurb = "b..l..u..r..b";
sub read_previous {
my ($fn) = @_;
my $fh;
my $section = undef;
my $serial = 1;
my $branch = $blurb;
my $last_empty = undef;
my (@section, %section, @branch, %branch, %description, @leader);
my (%section_description);
my $in_unedited_olde = 0;
if (!-r $fn) {
return +{
'section_list' => [],
'section_data' => {},
'topic_description' => {
$blurb => {
desc => undef,
text => blurb_text(),
},
},
};
}
open ($fh, '<', $fn) or die "$!: open $fn";
while (<$fh>) {
chomp;
s/\s+$//;
if ($in_unedited_olde) {
if (/^>>$/) {
$in_unedited_olde = 0;
$_ = " | $_";
}
} elsif (/^<<$/) {
$in_unedited_olde = 1;
}
if ($in_unedited_olde) {
$_ = " | $_";
}
if (defined $section && /^-{20,}$/) {
$_ = "";
}
if (/^$/) {
$last_empty = 1;
next;
}
if (/^\[(.*)\]\s*$/) {
$section = $1;
$branch = undef;
if (!exists $section{$section}) {
push @section, $section;
$section{$section} = [];
}
next;
}
if (defined $section &&
!defined $branch &&
!/^\* /) {
$section_description{$section} ||= "";
$section_description{$section} .= "$_\n";
next;
}
if (defined $section && /^\* (\S+) /) {
$branch = $1;
$last_empty = 0;
if (!exists $branch{$branch}) {
push @branch, [$branch, $section];
$branch{$branch} = 1;
}
push @{$section{$section}}, $branch;
}
if (defined $branch) {
my $was_last_empty = $last_empty;
$last_empty = 0;
if (!exists $description{$branch}) {
$description{$branch} = [];
}
if ($was_last_empty) {
push @{$description{$branch}}, "";
}
push @{$description{$branch}}, $_;
}
}
close($fh);
my $lead = " ";
for my $branch (keys %description) {
my $ary = $description{$branch};
if ($branch eq $blurb) {
while (@{$ary} && $ary->[-1] =~ /^-{30,}$/) {
pop @{$ary};
}
$description{$branch} = +{
desc => undef,
text => join("\n", @{$ary}),
};
} else {
my (@desc, @src, @txt) = ();
while (@{$ary}) {
my $elem = shift @{$ary};
last if ($elem eq '');
push @desc, $elem;
}
for (@{$ary}) {
s/^\s+//;
$_ = "$lead$_";
s/\s+$//;
if (/^${lead}source:/) {
push @src, $_;
} else {
push @txt, $_;
}
}
$description{$branch} = +{
desc => join("\n", @desc),
text => join("\n", @txt),
src => join("\n", @src),
};
}
}
return +{
section_list => \@section,
section_data => \%section,
topic_description => \%description,
section_description => \%section_description,
};
}
sub write_cooking {
my ($fn, $cooking) = @_;
my $fh;
open($fh, '>', $fn) or die "$!: open $fn";
print $fh $cooking->{'topic_description'}{$blurb}{'text'};
for my $section_name (@{$cooking->{'section_list'}}) {
my $topic_list = $cooking->{'section_data'}{$section_name};
next if (!@{$topic_list});
print $fh "\n";
print $fh '-' x 50, "\n";
print $fh "[$section_name]\n";
my $lead = "\n";
if ($cooking->{'section_description'}{$section_name}) {
print $fh "\n", $cooking->{'section_description'}{$section_name};
}
for my $topic (@{$topic_list}) {
my $d = $cooking->{'topic_description'}{$topic};
print $fh $lead, $d->{'desc'}, "\n";
if ($d->{'text'}) {
# Final clean-up. No leading or trailing
# blank lines, no multi-line gaps.
for ($d->{'text'}) {
s/^\n+//s;
s/\n{3,}/\n\n/s;
s/\n+$//s;
}
print $fh "\n", $d->{'text'}, "\n";
}
if ($d->{'src'}) {
if (!$d->{'text'}) {
print $fh "\n";
}
print $fh $d->{'src'}, "\n";
}
$lead = "\n\n";
}
}
close($fh);
}
my $graduated = "Graduated to '$MASTER'";
my $new_topics = 'New Topics';
my $discarded = 'Discarded';
my $cooking_topics = 'Cooking';
sub update_issue {
my ($cooking) = @_;
my ($fh, $master_at, $next_at, $incremental);
open($fh, '-|',
qw(git for-each-ref),
"--format=%(refname:short) %(objectname)",
"refs/heads/$MASTER",
"refs/heads/next") or die "$!: open for-each-ref";
while (<$fh>) {
my ($branch, $at) = /^(\S+) (\S+)$/;
if ($branch eq $MASTER) { $master_at = $at; }
if ($branch eq 'next') { $next_at = $at; }
}
close($fh) or die "$!: close for-each-ref";
$incremental = ((-r "Meta/whats-cooking.txt") &&
system("cd Meta && " .
"git diff --quiet --no-ext-diff HEAD -- " .
"whats-cooking.txt"));
my $now_string = localtime;
my ($current_dow, $current_mon, $current_date, $current_year) =
($now_string =~ /^(\w+) (\w+) +(\d+) [\d:]+ (\d+)$/);
my $btext = $cooking->{'topic_description'}{$blurb}{'text'};
if ($btext !~ s/\A$blurb_match//) {
for my $line (split(/\n/, $btext)) {
print STDERR ">>> $line\n";
}
die "match pattern broken?";
}
my ($mon, $year, $issue, $dow, $date) = ($1, $2, $3, $4, $5);
if ($current_mon ne $mon || $current_year ne $year) {
$issue = "01";
} elsif (!$incremental) {
$issue =~ s/^0*//;
$issue = sprintf "%02d", ($issue + 1);
}
$mon = $current_mon;
$year = $current_year;
$dow = $current_dow;
$date = $current_date;
$cooking->{'topic_description'}{$blurb}{'text'} =
blurb_text($mon, $year, $issue, $dow, $date,
$master_at, $next_at, $btext);
# If starting a new issue, move what used to be in
# new topics to cooking topics.
if (!$incremental) {
my $sd = $cooking->{'section_data'};
my $sl = $cooking->{'section_list'};
if (exists $sd->{$new_topics}) {
if (!exists $sd->{$cooking_topics}) {
$sd->{$cooking_topics} = [];
unshift @{$sl}, $cooking_topics;
}
unshift @{$sd->{$cooking_topics}}, @{$sd->{$new_topics}};
}
$sd->{$new_topics} = [];
}
return $incremental;
}
sub topic_in_seen {
my ($topic_desc) = @_;
for my $line (split(/\n/, $topic_desc)) {
if ($line =~ /^ [+-] /) {
return 1;
}
}
return 0;
}
my $mergetomaster;
sub prepare_mergetomaster {
if (!defined $mergetomaster) {
my $master = `git describe $MASTER`;
if ($master =~ /-rc(\d+)(-\d+-g[0-9a-f]+)?$/ && $1 != 0) {
$mergetomaster = "Will cook in 'next'.";
} else {
$mergetomaster = "Will merge to '$MASTER'.";
}
}
}
sub tweak_willdo {
my ($td) = @_;
my $desc = $td->{'desc'};
my $text = $td->{'text'};
# If updated description (i.e. the list of patches with
# merge trail to 'next') has 'merged to next', then
# tweak the topic to be slated to 'master'.
# NEEDSWORK: does this work correctly for a half-merged topic?
$desc =~ s/\n<<\n.*//s;
if ($desc =~ /^ \(merged to 'next'/m) {
$text =~ s/^ Will merge (back )?to 'next'\.$/ $mergetomaster/m;
$text =~ s/^ Will merge to and (then )?cook in 'next'\.$/ Will cook in 'next'./m;
$text =~ s/^ Will merge to 'next' and (then )?to '$MASTER'\.$/ Will merge to '$MASTER'./m;
}
$td->{'text'} = $text;
}
sub tweak_graduated {
my ($td) = @_;
# Rename the "Will merge" marker from topics that have graduated.
for ($td->{'text'}) {
s/\n Will merge to '$MASTER'\.(\n|$)/\n Graduated to '$MASTER'.$1/s;
}
}
sub merge_cooking {
my ($cooking, $current) = @_;
# A hash to find <desc, text> with a branch name or $blurb
my $td = $cooking->{'topic_description'};
# A hash to find a list of $td element given a section name
my $sd = $cooking->{'section_data'};
# A list of section names
my $sl = $cooking->{'section_list'};
my (@new_topic, @gone_topic);
# Make sure "New Topics" and "Graduated" exists
if (!exists $sd->{$new_topics}) {
$sd->{$new_topics} = [];
unshift @{$sl}, $new_topics;
}
if (!exists $sd->{$graduated}) {
$sd->{$graduated} = [];
unshift @{$sl}, $graduated;
}
my $incremental = update_issue($cooking);
for my $topic (sort keys %{$current}) {
if (!exists $td->{$topic}) {
# Ignore new topics without anything merged
if (topic_in_seen($current->{$topic}{'desc'})) {
push @new_topic, $topic;
# lazily find the source for a new topic.
$current->{$topic}{'src'} = join("\n", get_source($topic));
my $summary = desc_from_merge($topic);
if (defined $summary) {
$current->{$topic}{'desc'} .= "\n$summary";
}
}
next;
}
# Annotate if the contents of the topic changed
my $topic_changed = 0;
my $n = $current->{$topic}{'desc'};
my $o = $td->{$topic}{'desc'};
if ($n ne $o) {
$topic_changed = 1;
$td->{$topic}{'desc'} = $n . "\n<<\n" . $o ."\n>>";
tweak_willdo($td->{$topic});
}
# Keep the original source for unchanged topic
if ($topic_changed) {
# lazily find out the source for the latest round.
$current->{$topic}{'src'} = join("\n", get_source($topic));
$n = $current->{$topic}{'src'};
$o = $td->{$topic}{'src'};
if ($n ne $o) {
$o = join("\n",
map { s/^\s*//; "-$_"; }
split(/\n/, $o));
$n = join("\n",
map { s/^\s*//; "+$_"; }
split(/\n/, $n));
$td->{$topic}{'src'} = join("\n", "<<", $o, $n, ">>");
}
}
}
for my $topic (sort keys %{$td}) {
next if ($topic eq $blurb);
next if (!$incremental &&
grep { $topic eq $_ } @{$sd->{$graduated}});
next if (grep { $topic eq $_ } @{$sd->{$discarded}});
if (!exists $current->{$topic}) {
push @gone_topic, $topic;
}
}
for (@new_topic) {
push @{$sd->{$new_topics}}, $_;
$td->{$_}{'desc'} = $current->{$_}{'desc'};
$td->{$_}{'src'} = $current->{$_}{'src'};
}
if (!$incremental) {
$sd->{$graduated} = [];
}
if (@gone_topic) {
for my $topic (@gone_topic) {
for my $section (@{$sl}) {
my $pre = scalar(@{$sd->{$section}});
@{$sd->{$section}} = (grep { $_ ne $topic }
@{$sd->{$section}});
my $post = scalar(@{$sd->{$section}});
next if ($pre == $post);
}
}
for (@gone_topic) {
push @{$sd->{$graduated}}, $_;
tweak_graduated($td->{$_});
}
}
}
################################################################
# WilDo
sub wildo_queue {
my ($in_section, $what, $topic) = @_;
if (defined $topic) {
for ($in_section) {
return if (/^Graduated to/ || /^Discarded$/);
}
my $action = $topic->[6] || "Unclassified.";
if (!exists $what->{$action}) {
$what->{$action} = [];
}
push @{$what->{$action}}, $topic;
}
}
sub wildo_match {
# NEEDSWORK: unify with Reintegrate::annotate_merge
if (/^Will (?:\S+ ){0,2}(fast-track|hold|keep|merge|drop|discard|cook|kick|defer|eject|be re-?rolled|wait)[;,. ]/ ||
/^Not urgent/ || /^Not ready/ || /^Waiting for / || /^Under discussion/ ||
/^Can wait in / || /^Still / || /^Stuck / || /^On hold/ || /^Breaks / ||
/^Inviting / || /^Comments/ || /^Retracted/ ||
/^Needs? / || /^Expecting / || /^May want to / || /^Under review/) {
return 1;
}
return 0;
}
sub wildo {
my $fd = shift;
my (%what, $topic, $last_merge_to_next, $in_section, $in_desc);
my $too_recent = '9999-99-99';
while (<$fd>) {
chomp;
if (/^\[(.*)\]$/) {
my $old_section = $in_section;
$in_section = $1;
wildo_queue($old_section, \%what, $topic);
$topic = $in_desc = undef;
next;
}
if (/^\* (\S+) \(([-0-9]+)\) (\d+) commits?$/) {
wildo_queue($in_section, \%what, $topic);
# [0] tip-date
# [1] next-date
# [2] topic
# [3] count
# [4] seen-count
# [5] source
# [6] action
$topic = [$2, $too_recent, $1, $3, 0, [], undef];
$in_desc = undef;
next;
}
if (defined $topic &&
($topic->[1] eq $too_recent) &&
($topic->[4] == 0) &&
(/^ \(merged to 'next' on ([-0-9]+)/)) {
$topic->[1] = $1;
}
if (defined $topic && /^ - /) {
$topic->[4]++;
}
if (defined $topic && /^$/) {
$in_desc = 1;
next;
}
next unless defined $topic && $in_desc;
s/^\s+//;
if (/Originally merged to 'next' on ([-0-9]+)/) {
$topic->[1] = $1;
next;
}
if (wildo_match($_)) {
$topic->[6] = $_;
next;
}
if (/^(?:source:|cf\.)\s+(.*)$/) {
$topic->[5] ||= [];
push @{$topic->[5]}, $1;
next;
}
}
wildo_queue($in_section, \%what, $topic);
my $ipbl = "";
for my $what (sort keys %what) {
print "$ipbl$what\n";
for $topic (sort { (($a->[1] cmp $b->[1]) ||
($a->[0] cmp $b->[0])) }
@{$what{$what}}) {
my ($tip, $next, $name, $count, $seen, $source) = @$topic;
my ($sign);
$tip =~ s/^\d{4}-//;
if (($next eq $too_recent) || (0 < $seen)) {
$sign = "-";
$next = " " x 6;
} else {
$sign = "+";
$next =~ s|^\d{4}-|/|;
}
$count = "#$count";
printf " %s %-60s %s%s %5s\n", $sign, $name, $tip, $next, $count;
if ($what =~ /^Will merge to '\w+'/ && $what !~ /\?$/ ||
$what eq $mergetomaster) {
next;
}
for my $s (@$source) {
if (0 && $s =~ /^<(.*)>$/) {
$s = "https://lore.kernel.org/git/$1/";
}
printf " $s\n";
}
}
$ipbl = "\n";
}
}
################################################################
# HavDone
sub havedone_show {
my $topic = shift;
my $str = shift;
my $prefix = " * ";
$str =~ s/\A\n+//;
$str =~ s/\n+\Z//;
print "($topic)\n";
for $str (split(/\n/, $str)) {
print "$prefix$str\n";
$prefix = " ";
}
}
sub havedone_count {
my @range = @_;
my $cnt = `git rev-list --count @range`;
chomp $cnt;
return $cnt;
}
sub havedone {
my $fh;
my %topic = ();
my @topic = ();
my ($topic, $to_maint, %to_maint, %merged, $in_desc);
if (!@ARGV) {
open($fh, '-|',
qw(git rev-list --first-parent -1), $MASTER,
qw(-- Documentation/RelNotes RelNotes))
or die "$!: open rev-list";
my ($rev) = <$fh>;
close($fh) or die "$!: close rev-list";
chomp $rev;
@ARGV = ("$rev..$MASTER");
}
open($fh, '-|',
qw(git log --first-parent --oneline --reverse), @ARGV)
or die "$!: open log --first-parent";
while (<$fh>) {
my ($sha1, $branch) = /^([0-9a-f]+) Merge branch '(.*)'$/;
next unless $branch;
$topic{$branch} = "";
$merged{$branch} = $sha1;
push @topic, $branch;
}
close($fh) or die "$!: close log --first-parent";
open($fh, "<", "Meta/whats-cooking.txt")
or die "$!: open whats-cooking";
while (<$fh>) {
chomp;
if (/^\[(.*)\]$/) {
# section header
$in_desc = $topic = undef;
next;
}
if (/^\* (\S+) \([-0-9]+\) \d+ commits?$/) {
if (exists $topic{$1}) {
$topic = $1;
$to_maint = 0;
} else {
$in_desc = $topic = undef;
}
next;
}
if (defined $topic && /^$/) {
$in_desc = 1;
next;
}
next unless defined $topic && $in_desc;
s/^\s+//;
if (wildo_match($_)) {
next;
}
$topic{$topic} .= "$_\n";
}
close($fh) or die "$!: close whats-cooking";
for $topic (@topic) {
my $merged = $merged{$topic};
my $in_master = havedone_count("$merged^1..$merged^2");
my $not_in_maint = havedone_count("maint..$merged^2");
if ($in_master == $not_in_maint) {
$to_maint{$topic} = 1;
}
}
my $shown = 0;
for $topic (@topic) {
next if (exists $to_maint{$topic});
havedone_show($topic, $topic{$topic});
print "\n";
$shown++;
}
if ($shown) {
print "-" x 64, "\n";
}
for $topic (@topic) {
next unless (exists $to_maint{$topic});
havedone_show($topic, $topic{$topic});
my $sha1 = `git rev-parse --short $topic`;
chomp $sha1;
print " (merge $sha1 $topic later to maint).\n";
print "\n";
}
}
################################################################
# WhatsCooking
sub doit {
my $cooking = read_previous('Meta/whats-cooking.txt');
my $topic = get_commit($cooking);
merge_cooking($cooking, $topic);
write_cooking('Meta/whats-cooking.txt', $cooking);
}
################################################################
################################################################
# Newer one exists?
sub newer {
my $fn = 'Meta/whats-cooking.txt';
my $fh;
my $last_topic;
open ($fh, '<', $fn) or die "$!: open $fn";
while (<$fh>) {
chomp;
if (/^[*] (\S+) \(\d\d\d\d-\d\d-\d\d\)/) {
$last_topic = $1;
next;
}
if (/^ \(a newer iteration/) {
print "$last_topic\n$_\n";
}
}
close ($fh);
}
################################################################
# Main
use Getopt::Long;
my ($wildo, $havedone, $newer);
if (!GetOptions("wildo" => \$wildo,
"newer" => \$newer,
"havedone" => \$havedone)) {
print STDERR "$0 [--wildo|--havedone]\n";
exit 1;
}
prepare_mergetomaster;
if ($wildo) {
my $fd;
if (!@ARGV) {
open($fd, "<", "Meta/whats-cooking.txt");
} elsif (@ARGV != 1) {
print STDERR "$0 --wildo [filename|HEAD|-]\n";
exit 1;
} elsif ($ARGV[0] eq '-') {
$fd = \*STDIN;
} elsif ($ARGV[0] =~ /^HEAD/) {
open($fd, "-|",
qw(git --git-dir=Meta/.git cat-file -p),
"$ARGV[0]:whats-cooking.txt");
} elsif ($ARGV[0] eq ":") {
open($fd, "-|",
qw(git --git-dir=Meta/.git cat-file -p),
":whats-cooking.txt");
} else {
open($fd, "<", $ARGV[0]);
}
wildo($fd);
} elsif ($newer) {
newer();
} elsif ($havedone) {
havedone();
} elsif (@ARGV) {
print STDERR "$0 does not take extra args: @ARGV\n";
exit 1;
} else {
doit();
}