You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
689 lines
15 KiB
689 lines
15 KiB
#!/usr/bin/perl -w |
|
# Maintain "what's cooking" messages |
|
|
|
use strict; |
|
|
|
my %reverts = ('next' => { |
|
map { $_ => 1 } qw( |
|
) }); |
|
|
|
%reverts = (); |
|
|
|
sub phrase_these { |
|
my (@u) = 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 (exists $topic_info->{'shares'}) { |
|
push @desc, ("is tangled 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=7), "--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; |
|
} |
|
} |
|
|
|
=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) = qw(master next pu); |
|
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+) (.*)$/; |
|
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'}{'pu'}) { |
|
$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=7), |
|
"--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'}; |
|
} |
|
my $list = join("\n", map { " " . $_ } @desc); |
|
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. Commits prefixed with '-' are |
|
only in 'pu' while commits prefixed with '+' are in 'next'. The ones |
|
marked with '.' do not appear in any of the integration branches, but I am |
|
still holding onto them. |
|
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 |
|
|
|
What's cooking in git.git ($mon $year, #$issue; $dow, $date) |
|
-------------------------------------------------- |
|
|
|
$text |
|
EOF |
|
$text =~ s/\n+\Z/\n/; |
|
return $text; |
|
} |
|
|
|
my $blurb_match = <<'EOF'; |
|
To: .* |
|
Subject: What's cooking in \S+ \((\w+) (\d+), #(\d+); (\w+), (\d+)\) |
|
X-master-at: ([0-9a-f]{40}) |
|
X-next-at: ([0-9a-f]{40}) |
|
|
|
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; |
|
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); |
|
|
|
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 = (); |
|
while (@{$ary}) { |
|
my $elem = shift @{$ary}; |
|
last if ($elem eq ''); |
|
push @desc, $elem; |
|
} |
|
$description{$branch} = +{ |
|
desc => join("\n", @desc), |
|
text => join("\n", @{$ary}), |
|
}; |
|
} |
|
} |
|
|
|
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"; |
|
for my $topic (@{$topic_list}) { |
|
my $d = $cooking->{'topic_description'}{$topic}; |
|
|
|
print $fh "\n", $d->{'desc'}, "\n"; |
|
if ($d->{'text'}) { |
|
print $fh "\n", $d->{'text'}, "\n"; |
|
} |
|
} |
|
} |
|
close($fh); |
|
} |
|
|
|
my $graduated = 'Graduated to "master"'; |
|
my $new_topics = 'New Topics'; |
|
my $old_new_topics = 'Old New Topics'; |
|
|
|
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 (!$incremental) { |
|
my $sd = $cooking->{'section_data'}; |
|
my $sl = $cooking->{'section_list'}; |
|
# Rename "New" to "Old New" and insert "New". |
|
# Move "New" to "Old New" |
|
my $i; |
|
my $doneso; |
|
for ($i = 0; $i < @{$sl}; $i++) { |
|
if ($sl->[$i] eq $new_topics) { |
|
$sl->[$i] = $old_new_topics; |
|
unshift @{$sl}, $new_topics; |
|
$doneso = 1; |
|
last; |
|
} |
|
} |
|
if ($doneso) { |
|
$sd->{$old_new_topics} = $sd->{$new_topics}; |
|
} |
|
$sd->{$new_topics} = []; |
|
} |
|
|
|
return $incremental; |
|
} |
|
|
|
sub topic_in_pu { |
|
my ($topic_desc) = @_; |
|
for my $line (split(/\n/, $topic_desc)) { |
|
if ($line =~ /^ [+-] /) { |
|
return 1; |
|
} |
|
} |
|
return 0; |
|
} |
|
|
|
sub merge_cooking { |
|
my ($cooking, $current) = @_; |
|
my $td = $cooking->{'topic_description'}; |
|
my $sd = $cooking->{'section_data'}; |
|
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_pu($current->{$topic}{'desc'})) { |
|
push @new_topic, $topic; |
|
} |
|
next; |
|
} |
|
my $n = $current->{$topic}{'desc'}; |
|
my $o = $td->{$topic}{'desc'}; |
|
if ($n ne $o) { |
|
$td->{$topic}{'desc'} = $n . "\n<<\n" . $o ."\n>>"; |
|
} |
|
} |
|
|
|
for my $topic (sort keys %{$td}) { |
|
next if ($topic eq $blurb); |
|
next if (!$incremental && |
|
grep { $topic eq $_ } @{$sd->{$graduated}}); |
|
if (!exists $current->{$topic}) { |
|
push @gone_topic, $topic; |
|
} |
|
} |
|
|
|
for (@new_topic) { |
|
push @{$sd->{$new_topics}}, $_; |
|
$td->{$_}{'desc'} = $current->{$_}{'desc'}; |
|
} |
|
|
|
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}}, $_; |
|
} |
|
} |
|
} |
|
|
|
################################################################ |
|
# WilDo |
|
sub wildo_queue { |
|
my ($what, $action, $topic) = @_; |
|
if (!exists $what->{$action}) { |
|
$what->{$action} = []; |
|
} |
|
push @{$what->{$action}}, $topic; |
|
} |
|
|
|
sub wildo { |
|
my (%what, $topic, $last_merge_to_next); |
|
my $too_recent = '9999-99-99'; |
|
while (<>) { |
|
chomp; |
|
|
|
next if (/^\[Graduated to/../^-{20,}$/); |
|
next if (/^\[Stalled\]/../^-{20,}$/); |
|
next if (/^\[Discarded\]/../^-{20,}$/); |
|
|
|
if (/^\* (\S+) \(([-0-9]+)\) (\d+) commits?$/) { |
|
if (defined $topic) { |
|
wildo_queue(\%what, "Undecided", $topic); |
|
} |
|
# tip-date, next-date, topic, count, pu-count |
|
$topic = [$2, $too_recent, $1, $3, 0]; |
|
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]++; |
|
} |
|
next if (/^ /); |
|
if (defined $topic && |
|
/Will (?:\S+ )?(merge|drop|discard|cook)[. ]/i) { |
|
wildo_queue(\%what, $_, $topic); |
|
$topic = undef; |
|
} |
|
} |
|
if (defined $topic) { |
|
wildo_queue(\%what, "Undecided", $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, $pu) = @$topic; |
|
my ($sign); |
|
$tip =~ s/^\d{4}-//; |
|
if (($next eq $too_recent) || (0 < $pu)) { |
|
$sign = "-"; |
|
$next = " " x 6; |
|
} else { |
|
$sign = "+"; |
|
$next =~ s|^\d{4}-|/|; |
|
} |
|
$count = "#$count"; |
|
printf " %s %-60s %s%s %5s\n", $sign, $name, $tip, $next, $count; |
|
} |
|
$ipbl = "\n"; |
|
} |
|
} |
|
|
|
################################################################ |
|
# WhatsCooking |
|
|
|
sub doit { |
|
my $topic = get_commit(); |
|
my $cooking = read_previous('Meta/whats-cooking.txt'); |
|
merge_cooking($cooking, $topic); |
|
write_cooking('Meta/whats-cooking.txt', $cooking); |
|
} |
|
|
|
################################################################ |
|
# Main |
|
|
|
use Getopt::Long; |
|
|
|
my $wildo; |
|
if (!GetOptions("wildo" => \$wildo)) { |
|
print STDERR "$0 [--wildo]"; |
|
exit 1; |
|
} |
|
|
|
if ($wildo) { |
|
if (!@ARGV) { |
|
push @ARGV, "Meta/whats-cooking.txt"; |
|
} |
|
wildo(); |
|
} else { |
|
doit(); |
|
}
|
|
|