mirror of
https://github.com/git/git.git
synced 2025-12-12 20:36:24 +01:00
Also force US/Pacific timezone, so that "1 calendar week" can consistently be computed (of course 'cal' needs to be run with the same timezone).
1149 lines
26 KiB
Perl
Executable File
1149 lines
26 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 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) %(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 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)
|
|
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//) {
|
|
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] || "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/ ||
|
|
/^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();
|
|
}
|