1132 lines
25 KiB
Perl
Executable File
1132 lines
25 KiB
Perl
Executable File
#!/usr/bin/perl -w
|
|
# Maintain "what's cooking" messages
|
|
|
|
my $MASTER = 'master'; # 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 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;
|
|
}
|
|
}
|
|
|
|
sub get_message_parent {
|
|
my ($mid) = @_;
|
|
my @line = ();
|
|
my %irt = ();
|
|
|
|
open(my $fh, "-|", qw(curl -s),
|
|
"https://lore.kernel.org/git/" . "$mid" . "/raw");
|
|
while (<$fh>) {
|
|
last if (/^$/);
|
|
chomp;
|
|
if (/^\s/) {
|
|
$line[-1] .= $_;
|
|
} else {
|
|
push @line, $_;
|
|
}
|
|
}
|
|
while (<$fh>) { # slurp
|
|
}
|
|
close($fh);
|
|
for (@line) {
|
|
if (s/^in-reply-to:\s*//i) {
|
|
while (/\s*<([^<]*)>\s*(.*)/) {
|
|
$irt{$1} = $1;
|
|
$_ = $2;
|
|
}
|
|
}
|
|
}
|
|
keys %irt;
|
|
}
|
|
|
|
sub get_source {
|
|
my ($branch) = @_;
|
|
my @id = ();
|
|
my %msgs = ();
|
|
my @msgs = ();
|
|
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.
|
|
for my $msg (@msgs) {
|
|
for my $parent (@{$msgs{$msg}}) {
|
|
if (!exists $msgs{$parent}) {
|
|
$source{$parent}++;
|
|
}
|
|
}
|
|
}
|
|
|
|
reduce_sources(\@msgs, \%msgs, \%source);
|
|
|
|
map {
|
|
" source: <$_>";
|
|
}
|
|
(sort keys %source);
|
|
}
|
|
|
|
sub reduce_sources {
|
|
# Message-source specific hack
|
|
my ($msgs_array, $msgs_map, $src_map) = @_;
|
|
|
|
# messages without parent, or a singleton patch
|
|
if ((! %$src_map && @{$msgs_array}) || (@{$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;
|
|
}
|
|
|
|
}
|
|
|
|
=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) %(committerdate:iso8601)",
|
|
"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 are candidate to be in a future
|
|
release). Commits prefixed with '-' are only in 'seen', and aren't
|
|
considered "accepted" at all and may be annotated with an URL to a
|
|
message that raises issues but they are no means exhaustive. A
|
|
topic without enough support may be discarded after a long period of
|
|
no activity.
|
|
|
|
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; $dow, $date)
|
|
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; $dow, $date)
|
|
--------------------------------------------------
|
|
|
|
$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; \4, \5\)
|
|
-{30,}
|
|
\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 $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 && /^\* (\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,
|
|
};
|
|
}
|
|
|
|
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";
|
|
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//) {
|
|
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) = @_;
|
|
|
|
# Remove the "Will merge" marker from topics that have graduated.
|
|
for ($td->{'text'}) {
|
|
s/\n Will merge to '$MASTER'\.(\n|$)/ /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));
|
|
}
|
|
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] || "Under discussion.";
|
|
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/ ||
|
|
/^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);
|
|
}
|
|
|
|
################################################################
|
|
# Main
|
|
|
|
use Getopt::Long;
|
|
|
|
my ($wildo, $havedone);
|
|
if (!GetOptions("wildo" => \$wildo,
|
|
"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 ($havedone) {
|
|
havedone();
|
|
} elsif (@ARGV) {
|
|
print STDERR "$0 does not take extra args: @ARGV\n";
|
|
exit 1;
|
|
} else {
|
|
doit();
|
|
}
|