Staging
v0.5.1
https://github.com/git/git
Raw File
Tip revision: 181785487ecd48c7a3760ca31a6a245aabb2cbc8 authored by Junio C Hamano on 22 January 2022, 03:05:07 UTC
What's cooking (2022/01 #06)
Tip revision: 1817854
cook
#!/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 %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)];
			if (!%source) {
				$source{$msg} = $msg;
			}
		}
	}
	close($fh);

	# Collect parent messages that are not in the series,
	# as they are likely to be the cover letters.
	for my $msg (keys %msgs) {
		for my $parent (@{$msgs{$msg}}) {
			if (!exists $msgs{$parent}) {
				$source{$parent} = 1;
			}
		}
	}

	map {
		" source: <$_>";
	}
	(keys %source);
}

=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',
which means nothing more than that I have found them of interest for
some reason (like "it may have hard-to-resolve conflicts with
another topic already in flight" or "this may turn out to be
useful").  Do not read too much into a topic being in (or not in)
'seen'.  The ones marked with '.' do not appear in any of the
integration branches, but I am still holding onto them.


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-vcs/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
Bcc: lwn\@lwn.net
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';
(?:(?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})

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]+)?$/) {
			$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 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/ ||
	    /^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();
} else {
	doit();
}
back to top