| #!/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 tweak_willdo { |
| my ($td) = @_; |
| my $desc = $td->{'desc'}; |
| my $text = $td->{'text'}; |
| |
| 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'."; |
| } |
| } |
| |
| # 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 ($what, $action, $topic) = @_; |
| if (!exists $what->{$action}) { |
| $what->{$action} = []; |
| } |
| push @{$what->{$action}}, $topic; |
| } |
| |
| sub section_action { |
| my ($section) = @_; |
| if ($section) { |
| for ($section) { |
| return if (/^Graduated to/ || /^Discarded$/); |
| return $_ if (/^Stalled$/); |
| } |
| } |
| return "Undecided"; |
| } |
| |
| sub wildo_flush_topic { |
| my ($in_section, $what, $topic) = @_; |
| if (defined $topic) { |
| my $action = section_action($in_section); |
| if ($action) { |
| wildo_queue($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 / || |
| /^Needs? / || /^Expecting / || /^May want to / || /^Under review/) { |
| return 1; |
| } |
| if (/^I think this is ready for /) { |
| 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_flush_topic($old_section, \%what, $topic); |
| $topic = $in_desc = undef; |
| next; |
| } |
| |
| if (/^\* (\S+) \(([-0-9]+)\) (\d+) commits?$/) { |
| wildo_flush_topic($in_section, \%what, $topic); |
| |
| # tip-date, next-date, topic, count, seen-count |
| $topic = [$2, $too_recent, $1, $3, 0]; |
| $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 (wildo_match($_)) { |
| wildo_queue(\%what, $_, $topic); |
| $topic = $in_desc = undef; |
| } |
| |
| if (/Originally merged to 'next' on ([-0-9]+)/) { |
| $topic->[1] = $1; |
| } |
| } |
| wildo_flush_topic($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) = @$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; |
| } |
| $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; |
| } |
| |
| 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(); |
| } |